Fix HDR mode for virtual display - #281
Conversation
📝 WalkthroughWalkthroughThis PR refactors HDR colorspace handling in VideoRenderer by adding two private helper methods (frameUsesHdrColorSpace, applySwapChainColorSpace), a cached state member (m_SwapChainHdrColorSpace), updating Render to apply swap-chain colorspace via these helpers, and modifying SetHDR to reset caches and directly synchronize display and swap-chain colorspaces. ChangesHDR Swap-Chain Colorspace Handling
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Streaming/VideoRenderer.cpp`:
- Around line 707-730: VideoRenderer::SetHDR is mutating shared color-space
state and calling swap-chain APIs from the connection callback path, which can
race with Render() and applySwapChainColorSpace(). Update SetHDR so the actual
m_LastColorTrc/m_SwapChainHdrColorSpace changes and swap-chain SetColorSpace1
call are marshaled onto the render thread, or protected with the same
synchronization used by the render loop. Keep the logic anchored around
VideoRenderer::SetHDR, Render(), and applySwapChainColorSpace() so all DXGI
color-space updates happen on one thread.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6c5187ff-a805-4222-9b2c-29bcbbe5534f
📒 Files selected for processing (2)
Streaming/VideoRenderer.cppStreaming/VideoRenderer.h
|
|
||
| // Keep HDMI and swap chain color spaces in sync; force re-apply on next frame if this fails | ||
| m_LastColorTrc = AVCOL_TRC_UNSPECIFIED; | ||
| m_SwapChainHdrColorSpace = !enabled; | ||
|
|
||
| auto* swapChain = m_deviceResources->GetSwapChain(); | ||
| if (!swapChain) { | ||
| return; | ||
| } | ||
|
|
||
| DXGI_COLOR_SPACE_TYPE colorspace = enabled | ||
| ? DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020 | ||
| : DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709; | ||
|
|
||
| HRESULT hr = swapChain->SetColorSpace1(colorspace); | ||
| if (SUCCEEDED(hr)) { | ||
| m_SwapChainHdrColorSpace = enabled; | ||
| Utils::Logf("SetHDR(%s): swap chain colorspace set to %s\n", | ||
| enabled ? "true" : "false", | ||
| enabled ? "DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020" | ||
| : "DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709"); | ||
| } else { | ||
| Utils::Logf("SetHDR(%s): SetColorSpace1 failed: 0x%08X\n", enabled ? "true" : "false", hr); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Trace which thread invokes SetHDR to confirm cross-thread swap-chain access.
rg -nP -C4 '\bSetHDR\s*\(' --type=cppRepository: TheElixZammuto/moonlight-xbox
Length of output: 167
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- SetHDR / applySwapChainColorSpace symbols ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/build/**' --glob '!**/dist/**' \
'\b(SetHDR|applySwapChainColorSpace|m_SwapChainHdrColorSpace|m_LastColorTrc)\b' .
printf '\n%s\n' '--- likely thread/callback dispatch references ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/build/**' --glob '!**/dist/**' \
'\b(callback|thread|render thread|dispatch|invoke|post|queue|task|async)\b' State Streaming | head -n 300Repository: TheElixZammuto/moonlight-xbox
Length of output: 7322
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- file list ---'
git ls-files | sed -n '1,200p'Repository: TheElixZammuto/moonlight-xbox
Length of output: 5492
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Streaming/moonlight_xbox_dxMain.cpp (around SetHDR assignment) ---'
sed -n '120,150p' Streaming/moonlight_xbox_dxMain.cpp
printf '\n%s\n' '--- Streaming/VideoRenderer.h ---'
sed -n '1,130p' Streaming/VideoRenderer.h
printf '\n%s\n' '--- Streaming/VideoRenderer.cpp (around SetHDR and applySwapChainColorSpace) ---'
sed -n '150,240p' Streaming/VideoRenderer.cpp
printf '\n%s\n' '--- Streaming/VideoRenderer.cpp (around SetHDR) ---'
sed -n '680,740p' Streaming/VideoRenderer.cppRepository: TheElixZammuto/moonlight-xbox
Length of output: 10272
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Streaming/moonlight_xbox_dxMain.cpp (render task / thread comments) ---'
sed -n '200,260p' Streaming/moonlight_xbox_dxMain.cpp
printf '\n%s\n' '--- Streaming/moonlight_xbox_dxMain.cpp (stream start / task creation) ---'
sed -n '340,390p' Streaming/moonlight_xbox_dxMain.cppRepository: TheElixZammuto/moonlight-xbox
Length of output: 4116
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Streaming/moonlight_xbox_dxMain.cpp (120-150) ---'
sed -n '120,150p' Streaming/moonlight_xbox_dxMain.cpp
printf '\n%s\n' '--- Streaming/moonlight_xbox_dxMain.cpp (200-260) ---'
sed -n '200,260p' Streaming/moonlight_xbox_dxMain.cpp
printf '\n%s\n' '--- Streaming/moonlight_xbox_dxMain.cpp (340-390) ---'
sed -n '340,390p' Streaming/moonlight_xbox_dxMain.cpp
printf '\n%s\n' '--- State/MoonlightClient.cpp (372-386) ---'
sed -n '372,386p' State/MoonlightClient.cpp
printf '\n%s\n' '--- Streaming/VideoRenderer.cpp (170-210) ---'
sed -n '170,210p' Streaming/VideoRenderer.cpp
printf '\n%s\n' '--- Streaming/VideoRenderer.cpp (690-730) ---'
sed -n '690,730p' Streaming/VideoRenderer.cppRepository: TheElixZammuto/moonlight-xbox
Length of output: 8626
Marshal SetHDR onto the render thread
client->SetHDR is invoked from the connection callback path, but VideoRenderer::SetHDR now mutates m_LastColorTrc/m_SwapChainHdrColorSpace and calls SetColorSpace1 directly. That bypasses the render-loop synchronization and can race with Render()/applySwapChainColorSpace(). Move the swap-chain update onto the render thread or guard the shared state and DXGI access.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Streaming/VideoRenderer.cpp` around lines 707 - 730, VideoRenderer::SetHDR is
mutating shared color-space state and calling swap-chain APIs from the
connection callback path, which can race with Render() and
applySwapChainColorSpace(). Update SetHDR so the actual
m_LastColorTrc/m_SwapChainHdrColorSpace changes and swap-chain SetColorSpace1
call are marshaled onto the render thread, or protected with the same
synchronization used by the render loop. Keep the logic anchored around
VideoRenderer::SetHDR, Render(), and applySwapChainColorSpace() so all DXGI
color-space updates happen on one thread.
|
If I'm following this correctly, you are saying that ffmpeg's frame->color_trc contains wrong data? I have never seen this, this field always matches the host's colorspace. An PQ-encoded HDR frame will always have color_trc set to AVCOL_TRC_SMPTE2084. Also, I don't think there is anything wrong with checking CheckColorSpaceSupport() before calling SetColorSpace1(). It has always worked for me. Maybe you can get a log showing these things failing? |
I just have HDR never properly worked on my setup for Apollo (with virtual display) + XBOX Series X. It works as expected on Google Streamer. |
|
I'm getting this exact issue using Xbox Series X. Windows HDR calibration, both white screen tests are completely white regardless of moving the sliders in any direction, also having the same with when pressing the menu shortcut buttons on the controller to enter mouse mode ect... while that overlay is on the HDR looks completely fine? then once I come out of the overlay, HDR goes back to blooming whites, the Sun/Fire - other bright light sources might as well be white light blobs, barely see any details. My PC is also connected to a 3440x1440p monitor? not sure if it makes any different. Tried for hours with the calibration tools, both on Xbox and Windows, also messing around with my TV's settings, switch HGIG on ect, something is completely overriding any attempt to calibrate HDR it seems. I have now used both Apollo and Vibeshine, no difference. I will try it on my Nvidea shield to see if the client device makes any difference. Interesting how it's almost the exact same issues here. |
|
If you're using Apollo and have a non-virtual HDR display you can hook up temporarily, it would be great if you could test with a recent build of official Sunshine streaming from a real HDR display in HDR mode. It doesn't need to be good or calibrated, just enough to let you enable HDR on it. The fact that the tone-mapping changes when you bring up the quick menu makes me wonder if the problem could be something with the shader applied by Apollo to outgoing frames. This was significantly reworked in Sunshine within the last year. For reference, when I use the quick menu, the rest of the stream gets a very slightly dimmed effect. Here is a shot of the effect while playing an HDR video (ignore the fact that it's been converted to SDR). It's very subtle, the light gray box around the Youtube description goes from RGB #303030 to #272727.
|
|
Using official latest Sunshine (no Apollo, no virtual display) — I'm experiencing the exact same issue as described. Setup below. Symptoms: HDR is extremely bright/blown out in games and in Windows HDR calibration tool (both white boxes are pure white, sliders do nothing). I’ve tried full HDR calibration on both Windows and Xbox, different TV HDR modes (including HGIG), and disabled all NVIDIA overlays/auto-optimization. Nothing seems to affect the blown-out HDR during streaming. Still find it very odd that the quick menu feels like it actually fixes the issue lol |
|
Update. After changing a few more settings on Ghost of tsushima, it doesn't look too bad anymore. It's a little bright but nothing crazy, not sure if Sunshine fixed it or at least helped, potentially. It does look better now. Thanks! |
|
My setup is: Important part, I guess: I have a separate setup on Google Streamer 4K with Android Artemis and Moonlight clients, and HDR works perfectly. |
|
Please include your server logs, there might be clues in the desktop/capture format and colorspace values. Does anyone have this problem who is not using a virtual display? Is anyone using the new experimental virtual display that is part of Vibeshine? (I am using it successfully , but in a manual way with Sunshine.) |
|
Latest Sunshine build connected to my real 3440x1440 monitor. (Not using a virtual display here) As per previous comments, all the same results regardless of virtual display or not. On Xbox Series X + Moonlight: HDR is blown out. Both Windows HDR calibration screens (Maximum Luminance and Max Full Frame Luminance) are completely white — sliders do nothing. Bright areas (sky, sun, lights) turn into white blobs with no detail. SDR streams perfectly with no issues. Changing tone mapping or HDR settings while streaming has no visible effect. On NVIDIA Shield + Artemis: HDR works correctly. Calibration tests are visible, and I can properly adjust the sliders. If there's anything else I can do, please let me know. Thanks. |
|
I can see that the HDR profile you're using in Windows is set to 460 (the line "Sending HDR10 metadata"), but this wouldn't break the HDR Calibration app. You should be seeing the gray cross in the white box up until around 800. Windows turns off all other adjustments when running that app so the only thing that would mess with it is your TV's tone mapping setting. It should look similar to the Xbox checkerboard pattern... are you able to use the Xbox one? The only other thing I can think of that can create a blown-out look in Windows is the SDR Content Brightness slider, which is inside the HDR settings page. If it's set too high it will make the SDR content in Windows too bright (the right value is usually 15). But that's not what you're seeing anyway. |




It is a possible fix for the HDR issue reported here: #234
As I've never done any C++ programming (but have worked with a bunch of other languages over the last 15 years), I rely on Cursor for the proposed changes, with the following explanations of possible HDR issues.
I've published the project and tested via Dev Mode on my Xbox; HDR works fine.
Both reasons and fixes seem reasonable. I am not an expert, and if the changes don't make sense, so be it. I hope these proposed changes will help to fix HDR issues properly.
Reason 1
Swap chain color space never set to PQ (highest probability)
The swap chain defaults to sRGB/gamma 2.2 for R10G10B10A2. HDR10 requires explicitly setting DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020.
Problems:
Fix:
Reason 2
HDMI HDR enabled, but rendering path still SDR (timing / coordination)
SetDisplayHDR (HDMI) and SetColorSpace1 (swap chain) are triggered separately:
SetHDR waits for loading to finish; frames can render before HDMI switches or before swap chain color space is set.
There is also a known issue in the project TODO:
Fix:
Summary by CodeRabbit