Skip to content

VR: Add Mono rendering method - #442

Open
Noniv wants to merge 1 commit into
praydog:masterfrom
Noniv:mono-rendering
Open

Noniv wants to merge 1 commit into
praydog:masterfrom
Noniv:mono-rendering

Conversation

@Noniv

@Noniv Noniv commented Sep 21, 2026

Copy link
Copy Markdown

This adds "Mono" as a fourth option in the Rendering Method dropdown. It renders one view from the point between the eyes and sends that same image to both eyes every frame.

I wanted it for performance. Synchronized Sequential renders one eye per frame, but it only submits a finished stereo pair every second frame. When I tried it, the FPS shown in Virtual Desktop did not go up compared to Native Stereo and it felt choppy. Mono also renders one view per frame, but every frame goes to both eyes. What you give up is stereo depth: the world looks flat, like a very large screen wrapped around you. The UI still has depth because it goes to the compositor as its own quad layer.

In Clair Obscur: Expedition 33 (UE5, D3D12, OpenXR through Virtual Desktop's VDXR) I got roughly 70% more FPS than Native Stereo at the same settings.

The second reason is stereo bugs. Some games render certain effects for one eye only. In Clair Obscur: Expedition 33, fog is sometimes visible in one eye and not the other, and objects are occasionally lit in one eye only. The mismatch is uncomfortable to look at. Since Mono shows both eyes the same image, those differences go away.

How it works

Mono reuses the single view layout that the AFR methods already use, so is_using_afr() is still true for it and the render target and swapchain setup is unchanged. I added is_using_alternating_eyes() for the code that depends on left/right frame parity, and switched those call sites over so mono skips them.

Both eyes have to share one frustum, so mono forces Horizontal Symmetric projection, and Vertical Matched if the user left vertical on default. The existing per eye view bounds then crop the shared image at submit. The eye offset is the average of the left and right offsets.

On submit, the D3D11 and D3D12 components already had a path that copies one frame to both eye swapchains when the same frame is presented twice. Mono takes that path every frame, with the right eye copy reading the left half of the backbuffer.

Some work only ran on the left pass (pose update) or the right pass (roomscale movement, aim rotation, UObjectHook attachment ticking). With a single pass per frame, mono runs all of it on that pass.

Testing

Tested by me: D3D12 with OpenXR (VDXR) on Quest 3, one game, played with Quest controllers. Switching between Native Stereo and Mono at runtime works.

Not tested: D3D11, OpenVR/SteamVR, 2D screen mode, and headsets with canted displays (Index, Pimax). Those paths compile, and I changed them the same way as the D3D12/OpenXR ones, but nobody has run them. On canted headsets the symmetric projection is probably not enough, since the two eyes do not share an orientation there.

Adds a fourth rendering method that renders a single view from the
point between the eyes and submits that image to both eyes every frame.
It costs about as much as Synchronized Sequential, but both eyes get a
new image each frame, so there is no halved refresh rate or ghosting.
There is no stereo depth.

Mono reuses the single view layout that AFR already uses, so
is_using_afr() stays true for it. The new is_using_alternating_eyes()
covers the places that depend on left/right frame parity, which mono
skips. Horizontal Symmetric projection is forced (and Vertical Matched
unless the user picked Symmetric) so both eyes share one frustum and
the existing per eye view bounds crop the image at submit.

Work that used to run only on the left or right pass (pose update,
roomscale movement, aim rotation, UObjectHook attachments) now runs on
the single mono pass.

Tested on D3D12 with OpenXR (VDXR). The D3D11 and OpenVR paths compile
but are untested.
@joeyhodge

Copy link
Copy Markdown

nice work! I am looking to implement this as well as an additional separate option

joeyhodge added a commit to joeyhodge/UEVR that referenced this pull request Sep 26, 2026
Keep rendering IDs 0-4 intact and add Mono as ID 5. Retire GPU consumers before live mode transitions, gate fresh pose/projection/scene generations, retain per-image copy sources, and keep Native/Ghost/DIBR settings isolated. Pair the experimental frontend branch and add WARP copy, transition, geometry and profile tests.

Adapted from Noniv's Mono proposal in praydog#442; implementation and validation details are in docs/mono-rendering.md.
joeyhodge added a commit to joeyhodge/UEVR that referenced this pull request Sep 26, 2026
Keep rendering IDs 0-4 intact and add Mono as ID 5. Retire GPU consumers before live mode transitions, gate fresh pose/projection/scene generations, retain per-image copy sources, and keep Native/Ghost/DIBR settings isolated. Pair the experimental frontend branch and add WARP copy, transition, geometry and profile tests.

Adapted from Noniv's Mono proposal in praydog#442; implementation and validation details are in docs/mono-rendering.md.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants