Where Every Millisecond Counts: Optimizing Godot for Standalone XR

July 22, 2026 by
W4 Games

By David Snopek, Clay John, and Pablo Selener

This post accompanies our GodotCon Boston 2026 talk. The full deck, with all benchmark tables, diagrams, Perfetto traces, and the complete PR list, is available for download at the bottom.

For two years, W4 Games collaborated with Meta to improve Godot on Meta’s XR headsets, and is currently collaborating  with Google on Android XR support. This post covers some of the rendering optimizations we contributed, plus content optimizations you can apply to your own game today. These clients funded this work, but the majority of it landed upstream in Godot, and what couldn’t go upstream shipped as public GDExtensions. If you use Godot, you already have the benefits of these collaborations.

The problem

A standalone headset is essentially a smartphone strapped to your face, with two extra constraints: every frame renders twice, and you must maintain 72 fps (at minimum), because dropped frames in VR can make players physically ill. Since Godot 4, the recommended renderer for standalone XR has been the Compatibility renderer for performance reasons, but Meta and Google both want to deprecate OpenGL, which is what the Compatibility renderer uses. The goal: make Vulkan Mobile match OpenGL in performance on a Quest 3.

We agreed to use a modified version of GDQuest's TPS demo for benchmark: an automated process fixes the camera at three different markers within the scene and gathers some metrics. The starting numbers were rough: OpenGL hit 72/66/72 fps across the markers, Vulkan managed only 36/36/60. Over the course of the collaboration, we were nearly able to close the gap. Here are four of them:

Foveated rendering on Vulkan

Your eye only sees detail at the center of your gaze, so you can render the periphery at a lower resolution. Doing so is called “foveated rendering”. On OpenGL, Godot has supported foveated rendering for years; adding support on Vulkan requires enabling an extension and providing a fragment density map. Dario implemented support for the Vulkan  VK_EXT_fragment_density_map extension (PR 99551), and David added support for the OpenXR XR_FB_foveation_vulkan extension (PR 99768).

It worked great on the Quest 3 headset, but crashed on Pico, HTC, and even Quest 2 and Pro under some configurations. This situation could be acceptable to Meta, but not acceptable for Godot, which cannot ship code that crashes on some vendors’ hardware. Weeks of debugging pointed to a driver bug on Qualcomm Gen 1 chips, culminating in David combing through a 60,000 line Vulkan API dump before a workaround shipped in Godot 4.5. This is one of the many strengths of working in an open ecosystem. Even if a client’s project funds a feature, it can only be merged when it works for everyone.

In the Google project, we expanded on this work, with both improvements shipping in Godot 4.7:

  • Eye-tracked foveated rendering (PR 117868) moves the high resolution region to where your eye actually points, allowing far more aggressive density maps. It is what makes the Samsung Galaxy XR viable (same chip class as Quest 3, three times the pixels), and it also works on the Quest Pro. Work funded by Meta benefited Google, and vice versa.
  • Vulkan subsampled images (PR 116220) store the low resolution regions at actual reduced resolution instead of duplicating pixels, cutting memory bandwidth.

Combined effect on Quest 3: markers went from 54/52/66 fps to 72/64/72. On the Galaxy XR device at a 90 fps target: 82/73/90 to 90/80/90.

Multi-threaded rendering

Godot’s “Separate” thread model doesn’t actually render your game faster, it hands rendering to a second thread so processing for the next frame starts early (see deck for the Perfetto traces). In theory, this was a no-brainer optimization to enable. In practice, it was completely broken with OpenXR on Android when we started. We fixed it across PRs 109431, 109057, 109532, 109533, and 109753, including documentation so the bugs stay fixed.

The gain depends on your game, but it almost always helps with OpenXR plus Vulkan, likely because of how xrWaitFrame() paces frames. It is still experimental: try it in your game, and file bug reports, because that is what will make it stable. We recommend it for all OpenXR + Vulkan apps.

Worth knowing: the community added tracing profiler support in Godot 4.6 (PR 104851), and we contributed testing, docs, and XR follow-ups on the Meta and Google budgets. Godot 4.7 ships official Perfetto-enabled Android export templates, no custom build needed.

Specialization constants

Godot’s template shaders carry features that are often unused (lights, decals, reflection probes), and unused shader code still has a performance const. Among other things, unused code inflates register usage, which lowers GPU occupancy and slows everything down. Specialization constants patch the shader bytecode before final pipeline compilation, so unused paths compile out entirely in the final pipeline binary. Multiple rounds of applying this optimization saved about 6 ms per frame.

This built on Godot’s ubershader system, which W4 contributed earlier to fix shader compilation stutter, knowing it would enable more later. Because of that foundation was already upstream, these wins were nearly free, and every Godot user gets them.

FP16

Most mobile GPUs natively support 16 bit floats, which halve bandwidth and can double throughput in heavy shaders. This sat on Godot’s wish list for years (Clay pitched it at GodotCon 2024), but nobody had the time to implement it until the Meta project funded it. The basic optimization is easy, the hard part was ensuring compatibility across Vulkan, D3D12, Metal, and devices without FP16 support. So we defined Godot-specific types (half, hvec4) that resolve to 16 or 32 bit depending on what is supported by the device (PR 107119). If your hardware supports FP16, you now benefit automatically.

Results

The following table includes all the work we did for the collaboration, and not just the above four optimizations.

 

OpenGL

Vulkan (before)

Vulkan (after)

Marker 1

72 fps

36 fps

72 fps

Marker 2

66 fps

36 fps

64 fps

Marker 3

72 fps

60 fps

72 fps

Frame rate doubled on the two worst markers and we are now vsync locked on two of the three. Marker 2 landed at 64 against OpenGL’s 66: not quite full parity, but close. Other improvements included: optimizing the render graph and sky rendering, shadow enhancements, and integration of re-spirv.

What you can do: content optimizations

Engine work only goes so far, and some of the biggest potential wins in this project came from content. Mobile GPUs are particularly punished by texture reads, overdraw, extra render passes, and vertex shader texture sampling; the common enemy is memory bandwidth. From the benchmark scene:

  • Bake terrain textures. The demo’s triplanar terrain shader did 33 texture samples per pixel, of which at most three survived. Baking the blend into UV-mapped textures saved 0.6 ms per frame. All the above performance numbers include this optimization because we found we couldn’t accurately profile performance without it.
  • Don’t sample textures in the vertex shader. In the example project, the grass and trees calculate their sway in the wind by reading from a noise texture that is passed into the shader. This is basically free on desktop, but it can be very expensive on mobile. Even worse, you pay the cost multiple times since the vertex shader is run again to calculate shadows. Instead of using a texture, you can bake the movement into the shader itself by using sin/cos offset by TIME. Doing that saves about 0.3 ms in the test project. 
  • Avoid dynamic skies on mobile and VR. As nice as it is to have a fully dynamic sky, it's best to avoid it on mobile and VR devices unless a dynamic sky is a very important part of your game’s look. Removing TIME from the sky shader saved 0.5 ms, and that number is only that low because this project also heavily optimized sky and reflection updates (PR 101973, PR 107902). At a 13.9 ms budget (72 fps), half a millisecond can be the difference between locked 72 fps everywhere and only in some places.

Also: always enable foveated rendering, and try multi-threaded rendering. Neither are enabled by default in the project settings.

The takeaway

This is how W4's professional services model reaches beyond our direct customers: clients fund the work, the results are submitted upstream in Godot or as public GDExtensions, and the whole community gets a better engine. Everything in this post is in Godot 4.7 or earlier.

If your team needs this kind of engineering muscle on Godot, from advisory to full development, we can help. Get in touch.


Download the full slide deck (.pptx):

Share this post