Brian Duffy

Writing

Technical

Getting good-looking work onto a Quest

The standalone frame budget, where it actually goes, and why the worst frame is the one to plan for.

Start with the arithmetic, because the rest follows from it.

At 72Hz you have 13.9 milliseconds per frame. At 90Hz, 11.1. On a Quest 3 at 120Hz, 8.3. In that window you render the scene twice, once per eye, and the compositor takes its share before you get any. If you want a planning number at 72Hz, ten or eleven milliseconds of application time is a reasonable place to start.

The size of the budget matters less than which statistic you’re optimising. On a desktop game you mostly care about the average; a frame that takes twice as long as its neighbours is a stutter. In VR that frame lags behind where the player’s head has already moved, and the mismatch between what the eyes report and what the inner ear reports is what makes people feel sick. So the frame to plan for is the worst one: the busiest room, the most enemies, the oldest headset you support.

Where the time goes

Usually not polygons. On mobile hardware the two costs that hurt are draw calls, which land on a phone CPU with a thermal ceiling, and fill rate, which is why transparency and overlapping particles get expensive at resolutions a flat game would think modest.

The third is memory, and it behaves differently from the other two. Run out and the process is killed. On Memoreum that showed up as memory-leak crashes, and it decided how the whole game was put together: every level is its own scene, loaded through an empty transition scene so the previous one is completely gone before the next arrives. On a small game you can treat memory as something to tidy up later. On a large one it becomes part of the architecture.

Four structural moves

Pool what you spawn, and fill the pools slowly. Blood, impacts and projectiles happen in bursts, exactly when the frame is already busy. On Memoreum they all come from pools loaded at the start of a level, filled one object per frame rather than in a single spike, so the cost lands during loading instead of during a fight. Dead enemies’ ragdolls are frozen after a delay for the same reason. It’s most useful for anything that appears often and in numbers; for a one-off effect, pooling just holds memory you didn’t need to.

Switch things off by distance, from one place. An AI agent that thinks every frame costs you whether or not anyone can see it. On Memoreum one coordinator ticks the whole enemy population once a second, handling awareness, hearing and switching off anything far enough away. For enemies that only matter when they’re near the player, the behaviour barely changes and the cost drops a long way. It suits games with lots of standard enemies better than ones built around a few complex characters.

Let the level design do your occlusion. A door that closes behind the player is a design feature and an occlusion boundary at the same time. Ours closed their occlusion portal as soon as the doorway was empty, so the headset never drew a room the player couldn’t see. It works best in games that are already built from rooms and corridors; open spaces need something else.

Tune per device, not for the worst one. A Quest 2 and a Quest 3 are different machines, and a single lowest-common-denominator setup gives the Quest 3 owner a Quest 2 game. We shipped separate render settings for each, and some decisions were per device too - on Quest 2, for example, enemy ragdolls always freeze after death.

Heat

A standalone headset throttles as it warms up, and it does it over the length of a real play session, not a quick test. The system will tell you when performance levels change if you listen for it, and it’s worth deciding in advance what you’ll give up when that happens. I’d also do some of your testing on a headset that has been running for a while, because that’s the state a player will be in.

Where I’d start

None of this is exotic, and most of it is about deciding early rather than optimising late. If I were starting another standalone VR project tomorrow, I’d settle how levels load and unload before building much content, put enemy updates under one coordinator from the first enemy, and set up per-device settings as soon as there was something worth looking at. Looking good on a Quest is mostly a matter of not spending the budget on things the player can’t see, so there’s more of it left for the things they can.