nvkms: fix duplicated half-image on 2Heads1OR displays (5K 120Hz VR headsets) - #1262
nvkms: fix duplicated half-image on 2Heads1OR displays (5K 120Hz VR headsets)#1262rvdende wants to merge 1 commit into
Conversation
Under 2Heads1OR the api head's frame is split horizontally across two hardware heads, and each head must scan out its own section of the surface. That section is selected by a viewport origin offset of viewPort.in.width * mergeHeadSection. Two of the three paths that set the viewport origin apply the offset: nvApiHeadSetViewportPointIn() when panning (nvkms-flip.c), and nvUpdateFlipEvoHwState() on flip (nvkms-hw-flip.c) -- but the latter only inside "if (pParams->viewPortIn.specified)", and nvidia-drm never specifies viewPortIn, so on the DRM/KMS path it does not run. nvSetViewPortsEvo() hardcoded the origin to 0 for every head. With nothing else setting it, both merge heads scanned out from x=0 under 2Heads1OR, so the leftmost section was repeated across the output and the right-hand half of the framebuffer was never displayed. Observed with an HTC Vive Pro 2 at 4896x2448@120 over DisplayPort: the pixel clock (1543470 kHz) exceeds a single head's maximum, so the mode is split across two heads (raster 4996x2574 -> 2498x2574, viewport in width 4896 -> 2448 each). The headset showed the left half of the framebuffer on both eyes. With this change head 0 gets origin x=0 and head 1 x=2448, and the image is correct. mergeHeadSection is 0 whenever 2Heads1OR is not in use, so this is a no-op for single-head configurations.
|
Thanks @rvdende. This fix makes sense to me for handling viewports with 2head1or. Will merge the change internally and mention your contrib in the internal change commit message. |
|
@Binary-Eater thanks appreciate it |
|
@Binary-Eater I've run into a stability issue Separate issue found while testing this under sustained loadWhile exercising this 2Heads1OR path on a Vive Pro 2 at 4896x2448@120 ( Symptom. Both merged heads' base window channels stop retiring methods — Decoding Why it kills the machine. /*
* If we have been waiting too long, print an error message. There
* isn't much we can do as currently structured, so just reset
* startTime.
*/The caller is typically holding the nvkms lock, so every subsequent modeset blocks behind it in D state and the desktop is unusable until a hard reboot. This is the same failure shape as #1192 (unbounded GPFIFO/pushbuffer waits in Fix for the hang. Branch: After two consecutive 5s timeouts the channel is marked dead and further methods are dropped rather than written into a ring nobody is draining. Verified on the reproduction above — both channels now retire and stop, and the desktop survives: The compositor and client both stayed alive and the session was recoverable by restarting the runtime instead of rebooting. Ten seconds rather than something longer is measured, not arbitrary: a first attempt at 30s was dead code, because the session becomes unusable enough to force a power-cycle after ~15s. This bounds the hang; it does not fix the underlying stall. Any guidance on why the merged-head window channels stop retiring under load would be welcome — the display engine appears to simply stop consuming, and I have not identified what it is blocked on. Environment: RTX 4090, 610.43.03 open modules, kernel 7.0.0-28-generic, Hyprland/Wayland, Monado direct-mode DRM lease. |
|
It seems in simple scenes the 5k 120hz works fine for a long time, but if you add additional load then the VR view freezes and a little while later like 60 seconds the entire desktop session freezes. So there is definitely some other code that needs fixes aswell. Im digging through and trying to piece things bit by bit but to be honest I have very little idea of where to go from here. |
The 2Heads1OR overfetch path looks half-implemented — is the valid-region register meant to be exposed?While testing this patch on a Vive Pro 2 at 4896x2448@120 (
/*
* Ampere, which adds "overfetch" to have tiled displays / 2-head-1-OR use cases without
* visual artefacts at head boundaries:
* (((SetViewportSizeIn.Width + 14) * SetMaxInputScaleFactor.Horizontal + 1023) >> 10) + 8
*/
return (((inWidth + 14) * maxHDownscaleFactor + 1023) >> 10) + 8;
But and the usage bounds are written at those same method offsets, but with older-class field definitions, so the bit goes out as 0: Grepping the whole tree, And the registers the TODOs point at aren't in the open headers:
Why this is asymmetric, and why 2Heads1OR is where it shows. The offset only exists in and no readers at all in Concretely, for this mode (each merged head has Also worth noting IMP is told nothing about this: The questions I actually have:
I'm not claiming this causes a specific failure; I haven't proven that, and the compositor hang I hit on this path (reported separately) has no GPU fault associated with it. This is narrower: the overfetch allowance is accounted for in the fetch sizing but the feature is never switched on, and the piece that would make switching it on safe isn't available in the open driver. Since this PR is what puts a head at a non-zero viewport origin in the first place, it seemed worth raising here. Environment: RTX 4090 (AD102, C67D core channel), 610.43.03 open modules, kernel 7.0.0-28-generic, Hyprland/Wayland, Monado direct-mode DRM lease. |
Problem
any display requiring 2Heads1OR shows the left half of the framebuffer duplicated across the output; reproduced on a Vive Pro 2 at 4896x2448@120
Fix:
It took quite a bit of digging and logging, but figured out how to get 120hz 5k working under linux. The problem was that the left eye video was copied to the right eye.
Here is some technical info from claude (which actually did the fix for me).
nvkms: honour mergeHeadSection in the modeset viewport origin
Under 2Heads1OR the api head's frame is split horizontally across two hardware heads, and each head must scan out its own section of the surface. That section is selected by a viewport origin offset of viewPort.in.width * mergeHeadSection.
Two of the three paths that set the viewport origin apply the offset: nvApiHeadSetViewportPointIn() when panning (nvkms-flip.c), and nvUpdateFlipEvoHwState() on flip (nvkms-hw-flip.c) -- but the latter only inside "if (pParams->viewPortIn.specified)", and nvidia-drm never specifies viewPortIn, so on the DRM/KMS path it does not run.
nvSetViewPortsEvo() hardcoded the origin to 0 for every head. With nothing else setting it, both merge heads scanned out from x=0 under 2Heads1OR, so the leftmost section was repeated across the output and the right-hand half of the framebuffer was never displayed.
Observed with an HTC Vive Pro 2 at 4896x2448@120 over DisplayPort: the pixel clock (1543470 kHz) exceeds a single head's maximum, so the mode is split across two heads (raster 4996x2574 -> 2498x2574, viewport in width 4896 -> 2448 each). The headset showed the left half of the framebuffer on both eyes. With this change head 0 gets origin x=0 and head 1 x=2448, and the image is correct.
mergeHeadSection is 0 whenever 2Heads1OR is not in use, so this is a no-op for single-head configurations.