Repository navigation
Conversation
The renderer imported the six textures of the first layer 0 swap set when it was built and copied two of them into the working image each frame. A frame from any other set (SteamVR switches between the compositor's set and a scene application's, e.g. SteamVR Home) tore down and rebuilt the renderer and the encoder, a stall each time. Now each texture is imported the first time its handle is submitted, the same way as before (OPAQUE_FD memory, SteamVR's format with MUTABLE_FORMAT, a UNORM view of an sRGB format, so no new requirement on any vendor), and drawn into the working image by a small compute shader, layer.comp. With the same size and the whole texture it samples each texel centre, so the result equals the old copy; a set of another size is scaled into the working image, which keeps the size of the first set. The encoder is built once per connect or rebuild. The renderer takes a list of layers; this commit passes layer 0 only. Imports of a set SteamVR destroys are released (DestroySwapTextureSet, DestroyAllSwapTextureSets) after dropping any pending job, under the encoder lock, with the generation moved so a job already taken is dropped rather than importing an fd about to be closed. The writers' implicit fences are now exported per texture and polled on the CPU (same cap and disarm as before) instead of also being imported as GPU semaphores: once the poll has seen them signalled the GPU wait adds nothing. The 4 ms guard sleep is unchanged. layer.comp is compiled to layer.comp.spv and embedded like the other Linux shaders (graphics.rs, bindings.h, alvr_server.cpp); those shared files only gain the byte array and its two symbols, nothing Windows runs changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SteamVR submits, per eye, the valid sub-rectangle of the texture (VRTextureBounds_t). SteamVR Home renders into part of a larger texture (bounds around 0.4-0.96, changing with load) and the whole texture was streamed, so the headset showed the image split, partly frozen or misplaced. layer.comp now stretches each eye's submitted rectangle over the eye (reversed endpoints keep a flip). Bounds that are not finite, out of [0, 1] or empty make the layer unusable; for layer 0 the frame is dropped with a warning. The fix first lived in fbinerd/alvr 037b7f2f, which cropped with vkCmdBlitImage and therefore needed a graphics-capable queue; drawing with layer.comp keeps upstream's queue choice. Co-Authored-By: Codex <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SteamVR submits its dashboard and other overlays as layers after layer 0 while a scene application runs, but only layer 0 was streamed (TODO: Merge layers): the SteamVR menu flickered or was missing over a game. Layers 1..n are now drawn over layer 0 in submission order by layer.comp in blend mode (straight alpha, on the sRGB-encoded values the stream already carries). A layer whose textures are not one of ours, or whose bounds are invalid, is skipped instead of dropping the frame. MAX_LAYERS becomes static constexpr: std::min(m_submitLayer, MAX_LAYERS) odr-uses it, and as a static const int without a definition it only linked in release builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s on Linux NVIDIA's dma-bufs carry no implicit fences, so the worker cannot see when a texture is finished and relied on the fixed 4 ms sleep. No fixed delay fits every load: per-eye analysis of the encoded stream (motion of each eye half against the pose SteamVR submitted) showed the right eye, which the compositor draws last, on the previous frame when read too early and on the next one when SteamVR's next write overlapped the read; in VRChat 3-6 ms were still wrong and 8 ms cost 14 fps. Steam Link's own driver waits instead on a timeline semaphore SteamVR keeps per shared texture. The driver can reach the same one: SteamVrSync.h finds it through the texture's resource state in SteamVR's shared memory (undocumented; the header lists each assumption, how it was found and what happens if it changes) and gets its fd through the public IVRIPCResourceManagerClient. Present reads the value of the write just submitted for every texture of the frame; the worker imports the semaphore once and waits for that value on the CPU, capped at two frames, then reads. A texture without a semaphore keeps the implicit-fence wait and the 4 ms guard (on Mesa those fences do carry the writer's work). Semaphores and mapped states are released with their texture set. Measured on NVIDIA: the semaphore was present for every texture; most are already finished when the worker gets to them, waits reach ~4 ms in SteamVR Home and ~6 ms when a heavy application's frame ends late. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… read PostPresent held SteamVR's submit thread until the next vsync tick (PaceAfterPresent), almost a whole frame. vrcompositor's WaitForAcquire then times out and it renders into the texture still being read; in SteamVR Home, with the menu, the image shook. While frames are streamed there is no such hold any more, except for frames that read a texture of vrcompositor's own sets (the set's process is vrcompositor): for those the value read at Present often does not yet cover the right eye's write, and without a hold that write lands during the read (the right eye showed the next frame). Those frames are held until the worker has read them, at most one frame interval (about 2 ms measured). Application textures are not held: holding them passes the application's late GPU frames on to the compositor (VRChat fell to 60 fps). Upstream's pacing stays while nothing is streamed (before a client connects, while the encoder is set up) and while SteamVR throttles an application, which is what honours the throttle hint. Every test ran that way; one run without any pacing from SteamVR's start hit an NVIDIA Xid 32 in vrcompositor before the client connected. Measured on NVIDIA with a Quest 2: SteamVR Home with the menu and VRChat (head turns, strafing, menus) without the right-eye shake, both eyes on their frame, 72 fps. An application SteamVR throttles is untested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When an application exits (SteamVR Home when a game starts, or the game itself), SteamVR destroys its texture sets and the compositor still presents one last frame naming them. That frame was logged as an error, "Textures not found in handle map", on every such transition. The driver now remembers the textures of the last few sets it was told to destroy; a frame naming one of them is skipped with a debug line. A texture that was never one of ours is still an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6149e92 to
332afeb
Compare
Vixea
left a comment
There was a problem hiding this comment.
I'm curious if you see the occasional glitched frame with this on NV still or if this might be an amd issue only, other than that no comment from this quick overview review
|
I haven't noticed glitched frames in my setup, but they may still be there. I'd need to analyse it calmly and over more time to be sure. There are things that likely need work for AMD, I don't have AMD hardware, so it's hard for me to work on that directly. What I could do is work on Intel, which shares the same Mesa/VAAPI path. That would fix the vendor-neutral parts (like the ownership transfer) and make it easier to finish the AMD side later with someone who has the hardware. |
|
Sounds good, it'll probably need a little deep dive have you checked vulkan validation with these patches? |
|
Tested on AMD — works great. Setup: RX 9070 XT, Mesa 26.2.4 (radeonsi, VAAPI encoder, AV1 10-bit, Constant 150 Mbps), Ryzen 7 9700X, Bazzite (Fedora 44, kernel 7.2.9, KDE Wayland), SteamVR beta, Quest 3 over Wi-Fi 6 (wired PC, −25 dBm / 2401 Mbps link on the headset). Max buffering frames: 1.2. Before this PR I had raised it to 2.0 to mask the stutter; it's no longer needed. Built ebec107 + this PR. I also kept the body of Before (ebec107 / nightly 2026.10.12): stutter in the right eye only (left eye smooth), in Titanfall 2 (OpenXR mod under Proton), Moss II and No Man's Sky. Server FPS oscillated between roughly 70 and 110, and the game's frame interval alternated 10/12 ms. After the PR, in a 46-minute Titanfall 2 session at 3288×3440 per eye (SteamVR supersampling 250%), ~156 Mbps:
Glitched frames (re: @Vixea's question): yes, on AMD I still see an occasional flash/glitched frame: a few per session, rare and short. Once there were a few seconds of flicker plus stutter that then went away by itself. This is with "image corruption fix" enabled. Some of the hitches may line up with ~1 s stalls of the game itself (periodic, likely autosave/level streaming), but I can't tell yet whether the flashes do too. I can test with "image corruption fix" off, with HEVC instead of AV1 if useful. Not tested: SteamVR Home (it got stuck on the loading screen and I disabled it). Before starting the game, vrcompositor also crashed twice at startup while Home was enabled, jumping to an invalid address right from Thanks a lot for this! |
Vixea
left a comment
There was a problem hiding this comment.
lgtm, after checking if overlay is pre-multiplied
SteamVR submits the layers above the scene (the dashboard, other overlays) with premultiplied alpha, so blending them with straight alpha applied the alpha twice and darkened their translucent parts (the laser, the icon bar, antialiased edges, fades). Checked by dumping the raw dashboard layer SteamVR gives the driver (one composited R8G8B8A8_SRGB texture): across five frames with the menu open no partly transparent pixel has a colour above its alpha, including the edges of white text, which straight alpha would leave near full white. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion and cleanup - fixes many bugs from the original "working code"
…ue family SteamVR's textures are created by another process. The Vulkan spec requires an image backed by external memory to be acquired from VK_QUEUE_FAMILY_EXTERNAL before use and released back afterwards. Every frame, the renderer now acquires each SteamVR texture before reading it and releases it after the reads, keeping its layout and content. NVIDIA's driver and Intel ANV produced correct pictures without the transfer (tested with and without it), but drivers that keep images compressed, AMD's DCC in particular, may need it to resolve the compression at the handover. It is not a proven fix for a visible artefact. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Tested on NVIDIA — works for me. Setup: RTX 4060 Laptop, driver 615.78.08, CachyOS, SteamVR, Quest 3 over I built the streamer from source following the wired-Quest guide The official build was not usable for me. With this build, gameplay is fine |
Summary
I use ALVR on Linux with an NVIDIA card and a Quest 2. Since the switch to direct mode I kept running into the same problems: the right eye shook whenever I moved my head (in SteamVR Home, in the empty SteamVR scene and in games like VRChat) while the left eye was fine, the SteamVR menu flickered or disappeared over games, SteamVR Home looked broken, and in VRChat the image doubled when strafing because the frame rate dropped to around 58 fps.
The root cause is that ALVR read SteamVR's images at the wrong moment, only read the first layer, and held SteamVR back for almost a whole frame. This PR makes ALVR wait for the exact moment each image is ready (the same signal Steam Link uses), draws every layer, and only holds SteamVR when it really needs to. On my machine all of the problems above are gone and VRChat runs at 72 fps.
Issues this may fix:
Testing request: I could only test on NVIDIA. I would really appreciate tests from the community on AMD and Intel GPUs, with WayVR or other overlays, and with heavy games where SteamVR throttles the frame rate. If something looks wrong, ALVR's log will say if a texture had no SteamVR semaphore, which helps to tell where the problem is.
Details
Six commits, each building on its own:
layer.comp. A change of layer 0's texture set (SteamVR compositor <-> SteamVR Home or a game) no longer rebuilds the renderer and encoder. Imports are released when SteamVR destroys a set.TODO: Merge layers).SteamVrSync.hfinds the same one (through SteamVR's resource state in shared memory, which is undocumented: the header lists every assumption, how it was found, and that any failure falls back to the previous behaviour) and gets its fd through the publicIVRIPCResourceManagerClient. Textures without a semaphore keep the implicit-fence wait and the 4 ms guard.WaitForAcquiretime out and render into the texture being read (SteamVR Home shook). Now only frames that read vrcompositor's own textures are held, until read (~2 ms); upstream pacing is kept while nothing is streamed and while SteamVR throttles an application.How the right-eye problem was measured: the encoded stream was recorded and, for each eye half, the frame-to-frame image motion was correlated with the pose SteamVR submitted for that frame. Before: left eye matched its own frame (0.97), right eye the next frame (0.996). After: both eyes on their own frame (0.997 / 0.998).
Testing
graphics.rs,bindings.handalvr_server.cpponly gain the shader bytes and two symbols, as for the other Linux shaders).Related: #3412 (right-eye stutter and Home on AMD), #3378 (item 3; item 4, the WayVR overlay behind the game, may be fixed by commit 3 but was not tested with WayVR). The colour fix is separate: #3423.