HydraIssues

Portrait streaming: orientation contract implementation + remaining robustness improvements
open improvement Project: hydrabody Reporter: anonymous 13 May 2026 22:08

Description

### What was done

This tracks the full portrait streaming orientation work for cosmic-pretzel-98 (and all multi-orientation bodies). The root cause was a visible artifact: a landscape kiosk overlay showing behind a portrait Unreal Engine app when a portrait stream started.

Root cause breakdown:
1. kioskoverlay did not respond to WM_DISPLAYCHANGE -- overlay stayed landscape when VDD switched to portrait
2. handleStreamStarted did not receive orientation from the prep-cmd; it had to resolve it from cache with no explicit contract
3. handleStreamEnded was not implemented -- the Undo hook silently 404'd, leaving VDD at the last stream's resolution
4. Hardcoded resolution strings scattered across multiple files
5. No explicit pause between writing dd_manual_resolution and Sunshine beginning capture
6. --no-game-optimization flag in Moonlight suppressed optimizeGameSettings=true, preventing Sunshine auto mode from switching VDD resolution

#### Architecture shift (root fix)

Changed from dd_resolution_option=manual (hydrabody writes dd_manual_resolution per-session in prep-cmd) to dd_resolution_option=auto (Sunshine switches VDD to the client-requested resolution at RTSP negotiation time). This required removing --no-game-optimization from hydraheadflatscreen (v2.0.41).

#### Three-layer orientation contract

| Layer | Controls | Owner |
|-------|----------|-------|
| 1 -- Stream encoding | Resolution Moonlight client requests from Sunshine | Head (hydraheadflatscreen / hydraneckwebrtc) |
| 2 -- Display resolution | dd_resolution_option=auto in sunshine.conf, Sunshine switches VDD | Sunshine (triggered by optimizeGameSettings=true) |
| 3 -- App window | UE launch args registered in Sunshine app cmd | sunshine.go:tickSunshineApps |

#### Changes shipped (hydrabody v1.11.32 - v1.11.34)

- kioskoverlay: WM_DISPLAYCHANGE handler resizes overlay window on resolution change
- experiences.go: Resolution field on liveExperience + experienceInstallInfo; streamResolution helper as single source of truth
- sunshine_provider.go: desiredApp struct; buildOrientationHooks baking orientation into prep-cmd JSON body; post-sync verification
- sunshine.go: Experience state loaded before head loop; orientation resolved by StreamAppID
- httpserver.go: Orientation field in streamEventRequest; stream-started returns orientation for caller verification; layer 2 now owned by Sunshine auto
- vdd_windows.go: dd_resolution_option=auto in sunshine.conf template; 60Hz portrait mode variants in vdd_settings.xml
- Mutex on setSunshineConfig (defensive, race-safe)
- Custom resolution tracking from experience library (ensureVDDResolutions)

#### Changes shipped (hydraheadflatscreen v2.0.41)

- Removed --no-game-optimization from Moonlight launch args. This flag suppressed the optimizeGameSettings signal that Moonlight sends to Sunshine; without it Sunshine refuses to switch the display resolution regardless of dd_resolution_option.

### Remaining improvements

#### 1. Mutex on setSunshineConfig (already done in v1.11.32)

setSunshineConfig does read-merge-write on sunshine.conf. Mutex added to prevent race on concurrent prep-cmd calls.

#### 2. Custom resolutions in vdd_settings.xml (already done in v1.11.32)

ensureVDDResolutions() added: reads experience state, extracts custom Resolution values, hashes against stored fingerprint, rewrites vdd_settings.xml if changed. Logs warning that VDD reinstall is needed for new resolutions to take effect.

### Verification

Verified end-to-end on cosmic-pretzel-98 (bxl1-test district):
- VDD switches to 1080x1920 when portrait stream starts (Win32_VideoController confirmed)
- Mercator56-Win64-Shipping runs with -ResX=1080 -ResY=1920 -windowed -ForceRes
- prep-cmd bakes orientation=portrait in stream-started hook
- hydraneckwebrtc relay session confirmed portrait stream delivery