A week in, Brackenmere ran nicely on my Mac. That was the trouble: I had no idea what it cost anyone else, and every new world, kit piece and Meshy model was adding to a bill nobody was reading.
Two days earlier the asset diet had shrunk what already existed. On 26 September a cloud session was working through a stack of layers for me (agent-ready docs, gameplay systems, a pile of new kit pieces), and partway through I asked for something different: "Is it worth adding in here a performance review so we're not creating items... incorrectly that we'll need to mass change later on." I wanted the whole game looked at, "run time / mb load in game", and I wanted "anything new we produce" to stay "performant as well".
Gorak at fighting distance on High, with his colour maps back at full size. More on why below.
Measure everything first
I tacked a format question on the end: "Also is the use of avifs an option over webp?" The answer was more useful than a yes. AVIF only makes the download smaller; once decoded, a texture takes the same graphics memory whatever it arrived as, and graphics memory was the real problem. The better tool for 3D textures was KTX2, which the graphics card reads while still compressed. "Ktx2 sounds great, yep add that in and queue up layer 4," I said, and it became the fourth layer.
It ran in that cloud session, a Claude owner on a machine with no graphics card, mostly while I was away.
It began by rewriting the measuring tool. One command now builds the real game, opens every world at every quality tier and records downloads, GPU texture memory, draw calls, shaders compiled after the world said it was ready, and the first frames you would see.
Three numbers stood out.
Under the hood The village needed about 1.5 GB of GPU texture memory: every model texture was a decoded image, so a 2048 pixel map costs 21 MB on the GPU however small its file. Every world compiled all of its shaders (19 to 69 of them) on the first frames after it opened. And every world downloaded every other world's code: 527 KB of gzipped JavaScript wherever you went.
The fixes
Textures that stay compressed on the GPU. Models and 3D surfaces moved to KTX2, a format the graphics card reads without unpacking. Painted pictures (signs, portraits, rugs) got an AVIF beside their WebP. Where a cheaper encoding showed close up, those surfaces kept their old maps.
The village square before the round (left) and after (right). The right one needs about a fifth of the GPU memory.
Shaders compiled while loading. This was the hidden one. On a real graphics card it is a hitch the moment you arrive. Now the runtime compiles every material in the scene, hidden ones included, and draws one frame behind the loading notice before revealing the world.
Where the stall went: behind the loading notice, not on your first steps.
Each world its own download. A world now names the kit categories it places from, and only those ship with it.
Nothing was free: downloads went up about a tenth, and loading took longer, because the work from your first frames now happens before them. I was happy with that trade.
Under the hood Across all eleven worlds, GPU texture memory fell from 4,187 MB to 1,094 MB; the village from 1,514 MB to 279 MB. Shaders compiled after ready went to zero. JavaScript to open a world fell from 527 KB to between 373 and 484 KB.
Guardrails, not good intentions
The fixes mattered less than what came with them. Two days before, I had ruled that performance numbers are reported, never asserted. The plan I said yes to with that KTX2 reply drew the line properly: keep my no-numbers rule for how things look, but let limits that depend only on the assets fail the tests, while numbers that depend on the machine are reported and never fail.
So every kit piece has a budget for its category or must say why not. Every shipped file sits inside a size class that the import script also enforces, so an oversized model is refused before it lands. Each world's JavaScript has a ceiling, and a committed baseline per world warns, without failing, when something grows by more than a fifth.
It became golden rule 12: performant by construction. The cheap path is the one the tools take.
Under the hood The first overrides were honest: Meshy's crates and treasure chest came in at 40,000 triangles, as heavy as a building for a pile of boxes. They stayed, with a reason on file to re-simplify them.
Four decisions and one condition
When I checked in that evening ("How's it going? What else is to go") the last fixes were landing. The audit left four calls for me: load the training square's monsters only when you walk towards them, drop character colour maps from 2048 to 1024 pixels, draw the hedge maze's full-detail hedges over a shorter distance, and stop bottles and candles casting into the hearth's shadows. Just after midnight my answer was: "Yes to all your questions if you think it's a good idea for performance and doesn't degrade from the experience."
The agents took that condition literally. Overnight they applied each decision on its own commit, measured it, and handed before and after shots to an independent art reviewer who did not know which was which.
It saw nothing from three of the decisions. From the textures it saw loss in first person on four characters, and called two more borderline. My condition said no, so all six went back to full size, and the saving shrank from 9.4 MB to 5.7. The maze had been tried at 18 metres first; the corners of the widest view went flat, so it settled at 22.
Gorak's embroidered hem at 1024 pixels (left) and 2048 (right). The reviewer caught it blind; it went back.
The next afternoon
I picked the rest up from my own laptop: "Let's install the KTX, then work through the decisions. Ask me upfront so I can leave you work." Among the big jobs I ticked the performance leftovers, and on castle torches I wrote: "Shadow torch light can be for more capable machines with a higher graphic setting." Torch shadows went in on High only, and once their cost was measured I kept them.
The budgets did their job too, which was to make me decide, not to block. When the town's save outgrew its limit: "Let's raise the bar to 150Kb, and accept the 96Kb so we stop running into these issues." When Kit Yard needed room for the castle walls: "Raise to 200Kb, accept." And when an agent dropped some of my new props to 512 pixel textures, I sent them back to 1024, for the reason the blind review had already shown: the loss is visible.
What I took from it
- Measure the whole game, then fix. The shader stall was invisible until something counted it.
- Put the rules in the tools.
- Fail on what is the same everywhere, report the rest.
- Let a blind reviewer guard the look. Optimisation always looks fine to the one doing it.