A Year Inside the Steam Frame’s Graphics Stack
Valve has launched their much-awaited VR headset: the Steam Frame! When it was announced in November 2025, we wrote about what we were building toward. Now that it’s been released, this post gives an overview of the progress we’ve made since then. We’ll cover some of the challenges involved in building a stack that enables running everything from native ARM64 games to Windows x86-64 games, and how those challenges were overcome to give Frame users an immersive experience.
The Steam Frame is a standalone VR headset running a gaming-focused Linux distribution developed by Valve called SteamOS on an ARM System-on-Chip, the Qualcomm Snapdragon 8 Gen 3. It supports streaming games from a nearby PC over WiFi, while also being capable of running applications directly on the headset, which is where our work primarily lies.
The open source Turnip driver from the Mesa 3D project provides a Vulkan implementation for Qualcomm’s Adreno GPUs. It sits at the core of all graphics rendering on the Frame and is used instead of Qualcomm’s proprietary Vulkan driver. Igalia’s team, along with other brilliant upstream contributors, have worked on every component of Turnip (including its IR3 shader compiler) to narrow the performance gap between the two drivers.
Due to this combined effort, the gap has been bridged and in many cases Turnip is now faster than the proprietary driver, all while maintaining the high degree of correctness expected from Mesa 3D drivers.
While the Frame can natively run games that are compiled for the ARM architecture, it extends beyond Linux to be able to run Android games (via the newly introduced “Lepton” layer) and Windows games via Valve’s well known Proton compatibility layer. A unique challenge arose from the fact that the vast majority of existing Windows games are compiled for the x86 architecture, with no ARM version available. These games would need to be translated in real time to ARM to run on the Frame.
This translation is achieved by FEX-Emu, an open-source emulator that lets you run x86 applications on ARM CPUs. We’ve worked along with the FEX team to ensure that the overhead of the translated code is as minimal as it can be while ensuring the accuracy of it, all of which has now paid off with x86 games running almost as well as native ARM games.
Milliseconds Matter
On a monitor, a frame that arrives late creates a hitch in the animation; annoying, but usually not debilitating. You notice it and you carry on. But in a VR headset the image should be nailed to the room in the physical world. You turn your head, and the world is meant to stay exactly where it was: only your head is moving. If there’s a missed frame, the world moves with your head for an instant before snapping back into place. This may cause the VR user to experience VR discomfort.
Achieving the proper synchronization means the compositor (the software that sends images to the display hardware) has to take the frame the game just rendered, adjust it for where your head has moved since, and render it in the right place. It gets one slot per frame to do this. The Frame’s display runs at anywhere from 72 to 144 frames a second, so the entire time budget for a given frame is between seven and fourteen milliseconds.
For this reason, the graphics stack needs to be optimized for maximum performance with minimum latency. Being a standalone VR headset creates the additional challenge of delivering good battery life while doing all of this. At Igalia, all our work has been in pursuit of that goal.
Improved Turnip Performance
Our colleague Danylo Piliaiev contributed heavily on several fronts of the driver including improvements and crucial optimizations, as well as detection of critical bugs that blocked these optimizations.
One of the keys to optimizing 3D rendering is the depth buffer, which helps throw away objects that are hidden from view by other objects, but storing full-resolution depth buffers can consume significant memory bandwidth and processing resources. The Low-Resolution Z-buffer (LRZ), available on Qualcomm GPUs, can help reduce this overhead. The tricky part is that a driver has to set up rendering in ways that allow the LRZ optimization to remain valid. Danylo tackled this by adding a tracepoint that records why LRZ turned off in many cases, which led us to find and fix a few interesting bugs through the rest of the year.
Furthermore, LRZ had almost no conformance coverage anywhere in the industry, so Danylo built a test suite for it and pushed for the inclusion of those tests in VK-GL-CTS, Vulkan’s official Conformance Test Suite. These tests immediately found real driver bugs, both in Turnip and in the proprietary driver. Danylo wrote about challenges we overcame to make what Turnip is today.
In VR contexts, it’s incredibly useful (almost required) to be able to render different parts of the image with different levels of detail. Applications can use higher pixel densities in areas the user is looking at, and use lower resolutions in areas that sit in the peripheral vision. Vulkan has several mechanisms to render at lower resolution in some areas of the image, but Fragment Density Maps (FDMs) are specifically designed for VR contexts. While reviewing subsampled image work related to this, Danylo wrote fuzzing tests that detected more issues in the implementation of FDMs and other related extensions for Turnip.
Chasing Preemption Latency
Even with driver optimizations, the latency from other components in the Frame’s stack cannot be ignored. Our colleague Dhruv Mark Collins took up the challenge to keep the presentation latency of the compositor at a minimum, so that the compositor could draw in time for a VR frame, as described earlier.
The first piece of the puzzle was to have the compositor interrupt the GPU mid-frame when needed, which is called “preemption”. On a “tiler” like Adreno, which renders one region (a “tile”) of the screen at a time, this can only happen between tiles. Turnip’s approach to this problem grew out of last year’s ground-up overhaul of the autotuner, the code that decides how each render pass runs: when preemption is being delayed, we need to split the tiles into smaller pieces, stepping through progressively finer division levels until the interruption fits. Mark’s focus was on making that promise actually deliver.
When the tile-splitting was initially enabled on the Frame, some games still stuttered. Mark figured out that the tile divisor scaling was wrong: each level multiplied the number of pieces rather than adding to it, so by the second or third step the tiles had already reached the hardware’s support limits, and every splitting step beyond that did nothing. The solution was to fix the scaling so that each level was more gradual. Together with another fix, the changes improved these games substantially.
These fixes also helped other titles like “Sweet Surrender VR”, in which long render passes would run entirely in system memory. They now run on tile memory, and latency has noticeably improved.
The difference is larger than what the latency figures suggest. When playing a desktop game, it is rendered in a movie-theatre-like window placed inside a VR environment. When playing through a graphically intense scene that drags down the number of frames per second (FPS), the application would make the entire VR environment lag, rather than just the game, risking VR motion sickness. With these changes in place, even when the game runs at a lower FPS, it can do so without dragging the rest of the VR environment down with it. Finding the pre-emption latency bug led Mark to create a one-off tool for recording tile timings, which continues to yield dividends in our work.
Making Turnip Less Opaque
Similarly, our colleague Karmjit Mahil made considerable improvements in the visibility of driver runtime characteristics and submitted those upstream.
Perfetto is an open-source performance tracing and analysis framework. For a GPU developer, Perfetto is especially interesting for Vulkan drivers, as it can help bridge the gap between a Vulkan API operation and what ultimately happens in the Linux kernel and GPU.
Until this year, you largely had to infer what the GPU was doing with its tiles from the consequences. Thanks to Karmjit, now we have the groundwork for visualization: Turnip now emits its binning decisions into Perfetto captures. Those can be parsed by a Perfetto plugin which visualizes the bin grid, the per-eye offsets added by the headset, and what’s outside the scissor area shaded out.
Karmjit’s other habit is refusing to accept a workaround that works for the wrong reason. A game called “Creed: Rise to Glory” had artifacts on its characters’ skin. Initial investigation cast doubts on the LRZ implementation. Karmjit printed the depth values straight out of the game’s shader, which showed the game declaring one depth direction and writing another. The fix was one line of per-game configuration rather than turning off a major performance feature like LRZ. These types of workarounds for specific application bugs are somewhat frequent in Vulkan drivers and essential for improving the user experience with their hardware.
Changes to the IR3 Adreno Shader Compiler
The IR3 shader compiler is a world of its own, and our colleague Job Noorman is Igalia’s expert in that world. He works on the intricate task of turning shader instructions into actual hardware instructions. The Steam Frame required special attention from IR3 in several cases this year.
A lot of work went into supporting the necessary features required by modern games. For example, “Unreal Engine 5”, a popular game engine, requires support for 64-bit image atomics, which Turnip didn’t have. We collaborated within our team to add support for this with Danylo implementing the necessary driver bits and Job adding support in the IR3 compiler.
Besides adding features, a lot of work went into fixing issues found while running a massive amount of games on Turnip. One of the most complex parts of any compiler, including IR3, is the register allocator and multiple bugs were discovered and fixed. These fixes included source liveness detection when reusing registers during spilling and fixing incorrect register reuse. Adreno needs manual data synchronization between instructions which is a frequent source of bugs and the game “Resident Evil 3” caught another one, fixed by preserving sync flags. Another game, “Kingdoms of Amalur: Reckoning”, managed to find a long-standing issue that led to updating the way we handle booleans in IR3.
Another important fix for the Frame was emulating larger texel buffers in IR3, which was found after “Spider-Man Remastered”, “Ratchet & Clank: Rift Apart” and “Doom Eternal” all broke in the same way. Adreno’s descriptors cap texel buffers at 227 (a bit over 134.2 million) elements, but desktop games routinely ask for more than that. Danylo was able to trace the problems those games were having to that texel buffer cap, leading Job to implement the solution. Fixing it properly meant emulating the larger buffers: routing oversized texel buffers through global memory, lowering only the buffers that actually need it, and inserting bounds checks. This way, games get the addressing range they expect without every other buffer paying for it.
x87 in FEX
Old 32-bit x86 games make heavy use of x87, the floating-point unit Intel first shipped in 1980 (!). It works in 80-bit extended precision, which ARM has no equivalent for, so FEX implemented it with a floating-point software library.
Every call into one of the library’s routines meant setting the ARM floating-point control register to match x86 semantics and then restoring it afterwards. On Cortex CPU cores, writing to that register stalls the CPU pipeline, severely slowing the CPU at those moments. Converting an integer to an 80-bit float cost more in bookkeeping than in arithmetic.
Our colleague Paulo Matos, from Igalia’s Compilers Team, fixed FEX’s bookkeeping woes entirely. Simple conversions are now emitted directly as ARM instructions in the dispatcher, which were measured at fourteen times faster. It has been a long way involving plenty of patient work since the initial bootstrapping of the FEX x87 stack optimization.
In the six months that followed, Paulo applied the same principle to progressively harder instructions on FEX’s reduced-precision path, where x87 is carried in 64-bit doubles rather than the full 80 bits. Sine, cosine and tangent went first, then scaling and 2x-1, then arctangent and y·log₂(x), and finally both remainder instructions. By the summer essentially the whole emulation path had moved out of software emulation and into JIT-generated ARM code, yielding substantial performance improvements.
Improving CI and Preventing Regressions
On a device with a release date, the expensive failures are the things that used to work but no longer do, so Mesa now replays real game traces on real Adreno hardware before a merge request can land. Our colleague Emma Anholt wrote gpu-trace-perf and integrated snapshot testing using it into pre-merge CI for Mesa 3D. It includes Wine-based Windows replay support, improved image diff presentation, better log reporting, and parallelism, compared to the previous tool.
Getting this tool to run meant fixing most of what it stood on. Wine, DXVK, and the Linux and Windows builds of apitrace all needed work before any of it replayed reliably. D3D8 and D3D9 coverage on A660 GPUs landed first. D3D11 followed, through Windows RenderDoc and Mesa’s screenshot layer, which had race conditions of its own that had to be fixed before the results were stable enough to use. And the benefit is not Adreno’s alone: the same jobs were stabilised on Mesa’s software rasterizers and validated against the Intel and AMD drivers. She also built a new tool for improved frame trimming of apitrace traces, to reduce the overhead of replaying those traces in CI. That same tool can be used for performance testing (its original job) – the trace configuration file used in CI can also be used locally to A/B test performance using appropriate statistics.
We also maintain our own CI, geared specifically towards Steam Frame, where we run more than 600 game traces per configuration (there are four of them). We run this ourselves, as it can be too much for upstream CI due to load, bandwidth and noisy results. This also makes it possible to track performance improvements and regressions through all the changes we do to the Turnip.
All Upstream
None of the above happened in a vacuum. The Freedreno and Mesa communities reviewed, extended, and sometimes rescued this work, and plenty of what shipped on the Frame this year was written by people who are not us.
Because Valve builds on Mesa and FEX rather than on something closed, our work benefits the community at large. The clearest example came from the other direction: Someone running Turnip on an Adreno 730 phone reported that changing one register value nearly doubled their frame rate. This value is used by Turnip to disable caching in the hardware primitive pipeline and the Frame’s own GPU already had the right setting, so the fix actually changed nothing for the Frame, but it still went upstream to improve a whole generation of older Adreno hardware instead.
Across Igalia’s team, we’ve landed 168 changes to Mesa this year, and we’ve helped debug issues in other code as well. There is more we would like to write about. A hang that reproduced once in several thousand replays. A shader cache that turned out not to work between x86 and ARM. What a year of frametime analysis on a VR title actually looks like. Those would need posts of their own.
If any of this sounds like a problem you have or would like to work on, don’t hesitate to get in touch with us!