2026-10-01 · THE SERPENT LIES STILL

Where the church frame time was going

A frame of the church went from 4 to 5 fps to about 14 to 16 on an integrated GPU, and almost none of the gain came from where the first guesses pointed. Also a title screen with no text that tests could not see, a soft-lock in the fog walk, and test runs that were writing High quality into the player's settings.

The first day with the merged build. It compiled, the headless self-test passed 107 of 107 with nothing blocked, and then someone looked at it on the lowest-end machine the game is aimed at, an Intel UHD 630 integrated GPU.

Where it started

The very first render test, on day one, ran at about 4 fps. The UHD 630 has no 64-bit atomics, so the engine falls back from D3D12 SM6 to D3D11 SM5, which means no Nanite, no virtual shadow maps and software Lumen. The early gallery runs sat at 4 to 5 fps.

The merged church, at the Low quality tier, ran 7.8 to 16.7 fps depending on the shot, worst around 8 to 9.5 and best 14 to 16.5. The target is 30.

Profiling the editor binary gave a frame that did not add up:

game threadrender threadGPU frame
editor binaryabout 4 msabout 55 msabout 110 to 130 ms
packaged Shipping build1.1 ms3.9 msnot measured

About half the GPU frame was idle gaps between passes: the GPU was waiting for the render thread to submit work. The packaged build also used 0.66 GB of RAM where the editor binary in game mode used over 2.3 GB. Most of the editor binary's cost was editor overhead, so its CPU numbers overstate what the real game pays.

One title-screen cost showed up straight away. The title rendered the full 3D title scene behind an opaque painting, about 58 ms of GPU for nothing. It now switches world rendering off while the painting is up, and the gameplay game mode switches it back on.

Thirty toggles and no answer

The packaged game's church shots then stalled. One courtyard frame took 2.4 seconds of GPU time and the nave would not finish at all, with the CPU idle and the GPU's 3D engine about half busy. The stall in the nave survived about thirty toggles: shadows, fog, volumetric fog, light shafts, bloom, post, sky lighting, static and skeletal meshes, lights, Nanite, forced LODs, skin cache, morphs, render scale down to 20 percent. A stalled test process could not be killed, because a kernel thread was stuck in the GPU driver.

A later finding makes that bisect weaker than it looked: when several console commands were passed to one run, only the first took effect. Some of those experiments tested less than they claimed to.

What worked instead was to stop opening windows and build a headless audit stage that loads the church map with no rendering and counts what a frame would draw.

per view
static triangles drawn3.4 million, 3.26 million of them shadow-casting
movable local lights111 (17 shadowed, 24 volumetric)
skinned vertices per frame4,505,861

The same Low-tier shots, run with no window, finished in 30 seconds with no stall. The freeze only happens in a visible window, which points at the swap-chain present path rather than at an endless render. That also means every number below is a windowless number.

Three findings that moved the frame

Character LOD was pinned to LOD0. The skinned-vertex count was identical at Low and Medium, 4.5 million, and every skinning dispatch was LOD0. Each outfit piece follows the hidden mannequin body as its leader pose and attach parent, and skinned meshes copy the leader's predicted LOD. The body is hidden, so it never draws and its LOD never updates. The Low tier's skeletal LOD bias never reached any of the 61 outfit meshes, 56 of which carry lower LODs at about 25 and 13 percent of the full vertex count. Skinning alone cost 88 to 94 ms of GPU. Each outfit piece now picks its own LOD from its own screen size.

The profile named the Low tier's biggest passes. In the nave at Low:

passcostchange
volumetric fog conservative depth47 msoff
auto-exposure histogram11 msskipped; exposure is pinned, so the look is unchanged
translucency lighting volume9 msoff
TAA, plus its velocity pass14 to 22 ms, plus 24 to 28 msFXAA on the integrated-GPU profile

A per-frame call that re-uploaded everything. Buffer uploads cost 20 to 28 ms a frame. The effects system called MarkRenderStateDirty on its five instanced-mesh components every frame. Reading the engine source showed each instance edit already flags the update it needs, so the extra call was rebuilding the render proxies and re-uploading all instance data every frame. The call was removed. The day before, the effects system had already stopped clearing and re-adding thousands of instances per frame; that was necessary but it was not the expensive part.

One thing that did not help, but stayed in: the church generator now merges repeated identical static meshes into instanced batches, 694 actors into 46. In the editor binary the render thread did not change measurably, so draw count was not what held it. The merge is cheaper and correct, so it stayed.

The numbers

Nave frame time, Low, no window:

ms
before298
character LOD fix and Low-tier settings204
shadow, LOD-distance and upscale settings179
for comparison: 40 percent render resolution89

The 40 percent run shows the split: about half the frame is pixel cost and half is fixed cost.

Those first-frame figures were also inflated. The 180 to 270 ms readings were dominated by shader and upload warm-up. Measured 150 frames in, at steady state, the yard costs 63 ms and the nave 71 ms, roughly 14 to 16 fps, still with no window.

What the packaged game taught

  • The first packaged build could not start a game. The cooker only took the default map, so the church did not exist in the package and START GAME went back to the title. MapsToCook now lists both, and the package step ends with a smoke test that launches the exe, loads the church and fails the package if no church shot is rendered.
  • The first church load on the Intel D3D11 driver compiles every material. The game thread waited over 120 seconds on the render thread and the engine's render-fence watchdog killed the game as a hang. The timeout is now 900 seconds and the smoke test allows twenty minutes for that first load; later loads use the driver's shader cache.
  • Level master materials were missing the instanced-static-mesh usage flag. The packaged game swapped in the default material and recompiled shaders on every draw, at about 30 seconds a frame. Every level master sets the flag now.

A title screen with no text

The title screen showed its backdrop, rules and highlight bars and none of its words. The HUD drew text through the runtime Slate font cache, which produced no glyphs on the development machine. Headless tests cannot see glyphs, so all 107 passed over a screen with no text on it.

Text now draws with the engine's offline Roboto font, stretched to size with letter spacing divided by the stretch and the measure function matched. It was verified by capturing the game window: title, epigraph, the menu and the version tag all appear. Large text then looked soft, so it moved to the distance-field Roboto, which is sharp at any size.

Test runs were writing into the player's settings

The saved settings file said graphics quality High on a machine where High is unplayable. The cause was the headless self-test: a null-renderer run reports itself as a discrete GPU, so it wrote High as the first-launch default, and shot runs autosaved into the player's save slots.

Test and shot runs now use their own settings slot and save prefix. Null-renderer runs never persist a detected quality. A settings version bump re-detects quality once. The two slots written by measurement runs before the first play session were moved aside. The game also now starts at Low on every machine, and an existing saved High or Medium resets to Low once. The player can still raise it in Options.

A soft-lock in the fog walk

Dying during the three-second walk through the fog gate, from a poison or Rotfire tick, used to start the boss fight against a dead player, which sealed the gate for good. The walk now makes the walker invulnerable and restores that afterwards, aborts cleanly if the walker dies or disappears, and only starts the fight for a living player. Beginning a fight refuses a dead player, and respawning always clears a half-started one. The self-test has a case for it.

Controls and prompts

The controls follow the Elden Ring layout: Triangle or E interacts and is also the two-hand modifier, held with a weapon button to two-hand it; a plain tap no longer toggles. Jump is on Cross or F. The camera stick has a response curve with an exponent of 1.7 and a 0.25 second ramp to full speed, 220 degrees per second of yaw and 140 of pitch.

All gamepads now go through Microsoft's GameInput. The first attempt guessed a generic controller map that competed with the standard path and broke a DualSense's right stick, so it was removed. GameInput reports a DualSense as a generic HID device, so the prompt style set to Automatic means PlayStation unless the last pad reports an Xbox hardware id. The Controls page lists every binding in both columns and cycles Automatic, Xbox and PlayStation.

Smaller things from the same pass: the player was the grey mannequin for every class until the outfit builder was wired in, enemies asked for idle clips that did not exist, falling out of the world is now an ordinary death, and the Pyre refuses rest while an aware enemy is hunting you within 25 metres.

Watching the game play itself

An AI Watch mode plays the real game through the same inputs a player has: move axis, roll, light and heavy, lock-on, flask, interact. It reads enemy move timings to decide when to dodge, uses the navigation mesh to reach doors, the fog gate, loot and the exit, picks a random class each run, and plays in its own save slot so the player's saves are untouched. Skill levels are Expert, Average, Bad and Random, and speed 1x, 2x or 4x.

It writes a live log. A watchdog flags no progress for one second, tries to free itself for three, then restarts the run, and the same restart applies when control stays locked for 45 seconds, the bot idles for 75, or a run passes 30 minutes. It is on the title screen and in the pause menu. It has compiled but has not yet been watched end to end.

A separate diagnostics subsystem writes a session log of every warning and error, watchdog lines for hitches over 750 ms, low memory, a player at NaN or below the level, and a boss fight with no living player, and a crash file with the last 300 log lines. Shipping builds strip engine logging, so there it records the watchdog lines and crashes only.

A starting loadout, measured

The Knight now starts with the Frostborn greatsword. A check of the first loadout found that the backup longsword and shield, equipped but unused, pushed starting equip load to 91.75 percent and forced heavy rolls. They were removed from the starter loadout, leaving Frostborn plus armour at 28.3 of 41.2, 68.69 percent, which keeps the medium roll that was intended.

The first version of the character was rated 3 out of 10 in review and rebuilt: a narrower 17 cm helmet with an open visor, layered plate, a sword carried over the shoulder with clearance for the helm. A rendered check drives the real player, idle, walk, a light attack and the rear carry, and asserts the attack has started before it captures the contact pose. It also found two real defects: the finger-flexion sign was wrong on the mannequin's hands, and an exposed forearm sat between a sleeve that ends at 0.58 and a glove cuff that starts at 0.78, now overlapped by a leather underlay. A pale wrist lining is still visible, so the outfit is not finished.