device-lost: webgpu · apple · iOS

Closed
#1

device-lost — destroyed: no message

Renderer WebGPU · apple · 804×1348 @3x · r180 · handheld (textures ≤1024, drawn @2x)
Render incidents 6 (error, render-threw, device-lost)
Build 192fdd9 · Origin https://war-table-tactics-cg2.pages.dev · Map Pegasus Bridge · Phase Battle · Turn 1

Incidents so far:

  • +1.1s error: Script error.
  • +1.1s error: TypeError: undefined is not an object (evaluating 'window.ethereum.selectedAddress = undefined')
  • +1.5s error: Script error.
  • +63.1s render-threw: RangeError: Range consisting of offset and length are out of bounds
  • +63.1s error: RangeError: Range consisting of offset and length are out of bounds
  • +63.3s device-lost: destroyed: no message

Console, last lines:

  • warn: THREE.TSL: Return statement used in an inline 'Fn()'. Define a layout struct to allow return values.
  • warn: [stall] 128 ms in "frame:turfBake" — over the 85 ms DSP budget, the mixer missed 1 buffer(s)
  • error: [wtt] render failed: RangeError: Range consisting of offset and length are out of bounds
  • error: [wtt] frame-breaking object: Scene > Group > Group > Mesh | userData={"kind":"tile","coord":{"x":0,"y":0}} | MeshStandardNodeMaterial [] — or the first new object through a shader whose bindings went stale {"metadata":{"version":4.7,"type":"Object","generator":"Object3D.toJSON"},"geometries":[{"uuid":"59cdc191-722b-4c48-9bab-c9888eff8c96","type":"BufferGeometry","data":{"attributes":{"posit…
  • error: THREE.WebGPURenderer: WebGPU Device Lost:

Message: Unknown reason
Reason: destroyed

Diagnostics attached as wtt-render-report.json.

Staff #2

What happened on that iPhone

The report is from build 192fdd9 on 16 September, Brave on iOS 18.7, Pegasus Bridge, 63 seconds into turn 1. The sequence in the attached diagnostics:

  • A 128 ms stall in the turf bake, the grass-colour render that runs when the ground changes.
  • The frame threw RangeError: Range consisting of offset and length are out of bounds. The renderer's bisect blamed the tile mesh at 0,0.
  • 174 ms later the WebGPU device reported lost, reason destroyed.

The cause

The RangeError is a symptom of a device that was already dead, not the cause of the loss. I verified this on WebKit directly with a test page in Safari 27 on this Mac: after the device is lost, createBuffer with mappedAtCreation still returns a buffer, but getMappedRange() hands back a zero-byte ArrayBuffer. Copying vertex data into it throws exactly the reported message. Three r180 has one such upload site, WebGPUAttributeUtils.createAttribute, and it does not guard against an empty mapping. So the frame after the GPU process died happened to upload a new vertex buffer, threw, and the asynchronous device.lost promise only resolved afterwards. The bisect that followed rendered against a dead device, so "tile 0,0" means nothing.

That puts thread 38 in the same family as threads 28 to 34: WebKit's GPU process killed for memory. What was landing at that moment is the new detail. A turf bake a minute into a battle is triggered by a ground material slot's textures arriving. The memory diet merged today left the ground textures out:

  • materialSlots.ts decodes each source PNG at full 2048² with a plain createImageBitmap, three maps in parallel, then packs at a fixed 1024² with no regard for the render budget. On Safari those bitmaps and canvases live in the GPU process.
  • The five default ground materials are about 300 MB of PNGs on disk. Grass001 alone is 65 MB, its normal map 29 MB. A phone downloads and decodes all of that during the battle, which is why a slot landed at 63 seconds.
  • The bake target itself is small on this board, roughly 768×470 half-float, so it is not the culprit.

What was built

Two commits on branch worktree-ground-texture-diet, worktree at /Users/magnus/Work/wtt/wtt/.claude/worktrees/ground-texture-diet.

Shipped derivatives instead of sources (commit ddfb0bd8). Every ground material folder now carries two generated files, packed-albedoRough.png and packed-normalHeight.png at 1024², plus a packed.json that fingerprints the source files they came from. A new script, scripts/packGroundMaterials.ts, writes them with pngjs and a box resample. The registry script packs any stale folder before rewriting the index, and the index imports the pair so the bundler ships it. The pack rule lives in src/render/groundPack.ts, shared with the browser. The nine folders' derivatives weigh 37 MB against 435 MB of sources; Grass001 alone drops from 65 MB to under 6 MB. A desktop already packed to 1024 at run time, so the picture is unchanged. The materials test now fails on a missing or stale pair, so an edited source cannot ship behind an old derivative.

Ground textures on the render budget (commit 9d2cf8e0). The slot loader fetches the derivatives and decodes them straight to bitmaps, resized inside the decode to the budget's size and through the model loader's decode gate, so the ground and the miniatures share one concurrency budget. No canvas, no per-pixel loop, and no 2048² bitmap ever exists. A phone decodes to 512, never lower, because its 256 cap was written for a house catalog and the ground is the whole picture. The sculpt page's Reload textures still reads the sources, so an edit shows there immediately.

The WebKit signature (same commit). The render catch now recognises the zero-byte-mapping RangeError, asks the device with a 16-byte probe buffer, and when the device maps nothing records the throw as the loss in flight, skips the misleading bisect, and runs the reload ladder itself if the lost event never arrives within 1.5 seconds. A RangeError on a device that still maps is judged as an ordinary frame fault.

This thread is closed. Replies and edits are frozen — start a new thread if there is more to say.