fix(windows): correct shader selection for sRGB-encoded FP16 framebuffer (Win11 SDR)#5106
Open
eastginza wants to merge 1 commit into
Open
fix(windows): correct shader selection for sRGB-encoded FP16 framebuffer (Win11 SDR)#5106eastginza wants to merge 1 commit into
eastginza wants to merge 1 commit into
Conversation
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Description
Fixes white-out / blown-out highlights when streaming an SDR session on Windows 11 where the desktop framebuffer ends up in FP16 storage with gamma-encoded (sRGB / G22) pixel values.
Symptom
This affects any Windows 11 22H2+ host where the active display causes the desktop to be composed in FP16 storage with gamma-encoded (sRGB / G22) values rather than linear scRGB. The same FP16 + G22 state is produced under multiple OS-level triggers:
The trigger is OS-level (Win11 composition behavior + Sunshine's existing FP16 shader assumptions); it is independent of GPU vendor, display type, host hardware, or whether the host runs on bare metal or in a VM. The affected user population is expected to grow as both ACM rollout and Sunshine-based remote-development setups (which often rely on virtual displays) become more common.
The Moonlight client shows the streamed desktop with whites and bright tones clipped against the top of the range. The affected code path in
display_vram.cppis the shader-selection logic shared by all five Windows capture formats (NV12, P010, AYUV, Y410, R16_UINT); the patch covers all five. Empirical verification in this report is the NV12 path (HEVC NVENC SDR session).Root cause
Under the triggers listed above, Windows 11 composites the desktop framebuffer in FP16 storage (
DXGI_FORMAT_R16G16B16A16_FLOAT) even when both the application and the display are SDR. The values stored in that FP16 buffer are still sRGB-encoded (DXGI reportsDXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709), not linear scRGB.The existing capture path in
display_vram.cpptreats every FP16 source as linear scRGB and routes it through the linear shader chain. That chain ends inconvert_linear_base.hlsl:This applies the sRGB encoding curve to values that are already sRGB-encoded, producing a double-encoded output. Highlights are crushed against the top of the range and the streamed image looks blown out on the client.
Log signature of an affected session:
Note that
Max Luminancecan be SDR-class (here, 270 nits) and the colorspace isG22_NONE_P709(gamma-encoded sRGB) — both are SDR signals — yet theDesktop formatis still the FP16 buffer. That mismatch is the defining fingerprint of this bug class.Historical context
This change extends the case matrix opened by #1178 (cgutman, 2023), which added the linear-FP16 capture path for Advanced Color SDR displays — the case where Windows delivers SDR content in FP16 storage as linear scRGB. Windows 11 22H2+ has since introduced a distinct FP16 SDR variant under multiple triggers (ACM, HDR-class virtual display EDID, etc.): the framebuffer still uses
DXGI_FORMAT_R16G16B16A16_FLOATstorage, but the pixel values are gamma-encoded (G22), not linear. The existing shader-selection logic does not distinguish between these two FP16 cases, so the gamma-encoded FP16 variant is incorrectly routed through the linear shader chain. This PR adds the discriminator (is_source_gamma_encoded_fp16()) that the new variant requires.Fix
Detect the gamma-encoded FP16 case via DXGI ColorSpace (
G22_NONE_P709/G22_NONE_P2020) and route those sources through the non-linear shader path (whoseconvert_base.hlslusessaturateonly, with no additional sRGB curve).src/platform/windows/display.h— new helperbool is_source_gamma_encoded_fp16()declared alongsideis_hdr().src/platform/windows/display_base.cpp— implementation that queriesIDXGIOutput6::GetDesc1()and returns true forG22_NONE_P709/G22_NONE_P2020.src/platform/windows/display_vram.cpp— every existing "FP16 → linear shader" decision now also checksis_source_gamma_encoded_fp16()and, when true, binds the non-linear shader to the FP16 slot. Covers all five capture formats (NV12 / P010 / R16_UINT / AYUV / Y410), nine call sites total.Backward compatibility
The new
is_source_gamma_encoded_fp16()helper returns false unless the source delivers FP16 storage withG22_NONE_P709orG22_NONE_P2020colorspace. All other configurations — HDR streaming (G10 / G2084), Advanced Color SDR (the case fixed by #1178), pre-22H2 Windows installs, and non-FP16 capture paths — retain the existing shader-selection behavior unchanged. The helper also returns false whenIDXGIOutput6is unavailable on the host, so older Windows/GPU configurations fall back safely to the existing path. There is no behavioural change for HDR streaming.Alternatives considered
Shader-only fix (earlier iteration): an initial attempt simply removed
ApplySRGBCurvefromconvert_linear_base.hlsl. That resolves the gamma-encoded FP16 case but breaks the linear HDR path (G10 / G2084 sources lose their sRGB encoding step). The current C++ approach keepsconvert_linear_base.hlslunchanged and routes only the gamma-encoded sources to the non-linear shader, leaving HDR streaming intact.External workarounds were exhaustively evaluated by this contributor at the driver / OS / EDID layer and all failed empirically. They are documented here because reviewers seeing similar wash-out reports may otherwise assume one of them is sufficient:
Desktop format: DXGI_FORMAT_R16G16B16A16_FLOATstate still persisted — most likely because the EDID continued to advertise BT.2020 wide-gamut colorimetry via the CTA Colorimetry Data Block, which is sufficient on its own to trigger the FP16 promote in Win11 22H2+.<CustomEdid>false</CustomEdid>invdd_settings.xml): same outcome — the auto-generated EDID still produced the FP16 + G22 capture state.IddSampleDriver, signing it with a WDK test certificate, installing it, and promoting it to the primary display path. Empirically confirmed: even with an SDR-only EDID, no CEA extension,AdapterCaps.Flags = 0, and the base IddCx 1.02 API,DXGI_FORMAT_R16G16B16A16_FLOATcapture persisted. This is strong evidence that no driver- or EDID-layer fix can suppress the FP16 promote in this Win11 build family — the decision happens above the IddCx surface. Additionally, this approach requires Windows test signing mode (bcdedit /set testsigning on) and permanently displays a desktop watermark, making it unsuitable as an end-user workaround even if it had worked.The exhaustive failure of all driver- / EDID- / encoder-layer workarounds is what narrowed the root cause to Sunshine's shader-selection logic. The C++ patch in this PR fixes that root cause once for all FP16 + G22 SDR sources, regardless of which OS-level trigger (ACM, HDR-class luminance EDID, BT.2020 wide-gamut EDID, or future Win11 behaviors) put the framebuffer in that state.
Reproduction
a. ACM trigger: enable Auto Color Management (
Settings → System → Display → Color profile → Automatically manage colour for apps); orb. HDR-class luminance trigger: run an Indirect Display Driver whose EDID advertises HDR-class max luminance (commonly seen with VDD-based virtual monitors used for remote development / VM setups), with ACM disabled; or
c. BT.2020 wide-gamut trigger: run a display (physical or virtual) whose EDID advertises BT.2020 colorimetry via the CTA Colorimetry Data Block, even with HDR Static Metadata stripped and Max Luminance reduced to SDR-class.
config/sunshine.log) that theDesktop formatisDXGI_FORMAT_R16G16B16A16_FLOATand theColorspaceisDXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709(see "Log signature" above).Empirically verified on: Windows 11 25H2 with ACM disabled + a VDD-based virtual display whose custom EDID had its HDR Static Metadata block stripped (Max Luminance reduced from ~1670 nits to 270 nits, SDR-class) but still advertised BT.2020 wide-gamut colorimetry via the CTA Colorimetry Data Block, NVIDIA RTX 5090, AORUS FO32U2P 4K@240Hz QD-OLED, NV12 HEVC NVENC 8-bit Rec. 709 SDR session. The same
display_vram.cppshader-selection path is exercised by every FP16 + G22 capture regardless of which trigger produced that state, so the ACM-triggered variant, the HDR-class-luminance-triggered variant, and other GPU vendors (AMD/Intel) are expected to exhibit identical behavior. Several long-standing wash-out reports (e.g. #1853, #4377) describe similar symptoms on Windows but each has its own underlying trigger; this PR does not claim to resolve them, but reporters whose setup matches the FP16 + G22 SDR case documented above may find that this patch addresses their specific instance.Screenshot
Before (Photoshop reconstruction — the patched binary is already deployed on this contributor's host so a live before-capture is not easily obtainable; the reconstruction approximates the on-wire highlight-clipping visual of the bug, consistent with the symptom described in the wash-out reports cited in Reproduction):
After (patched binary, actual capture from the affected host):
Issues Fixed or Closed
(none — several long-standing wash-out reports exist on the tracker (e.g. #1853, #3500, #4377), but on close inspection each has a distinct root cause that is not directly fixed by this patch. They are mentioned here only for context, not for auto-close.)
Roadmap Issues
(none)
Type of Change
Checklist
(The two unchecked boxes: the affected path is the DXGI capture / shader-selection chain inside
display_vram.cpp, which currently has no unit-test harness for branch coverage. Manual reproduction steps are documented above.)AI Usage