Skip to content

Release 1.9.0: ready for games - #9

Merged
nolanbaxter merged 23 commits into
mainfrom
develop
Oct 30, 2026
Merged

nolanbaxter merged 23 commits into
mainfrom
develop

Conversation

@nolanbaxter

Copy link
Copy Markdown
Member

Release 1.9.0. Merging tags v1.9.0 and publishes winding-engine@1.9.0.

  • A lost GPU device is recovered from: a new device on the same canvas, everything loaded rebuilt in place. A crashed GPU process in Electron came back in 1.7 s.
  • engine.prepare builds a scene's pipelines, and draws the cascades' culled path, behind a loading screen.
  • engine.createMesh; node.setVisible; node.setTint and setGlow, in their batches; node.name, find and findAll; setWorldPosition, setWorldRotation, worldRotation and setParent keepWorld; engine.clock.timeScale.
  • Shadows: cascades cull caster by caster and cast the LOD level their maps can show (5.2 to 3.7 ms in an LOD field); a point light redraws only the cube faces something moved in (21.9 to 13.8 ms in a 36-torch dungeon).
  • Starting is faster: passes compile side by side and compute pipelines off the GPU main thread; 3.1 to 2.3 s cold, 1.2 to 1.0 s warm, launch to first frame in Electron.

Checked locally: 16 Node suites, 96 GPU checks.

🤖 Generated with Claude Code

nolanbaxter and others added 23 commits October 30, 2026 09:11
…play

Probes, decals, AO, OIT, TAA and FXAA are built the first time a frame
asks for them, and frames draw without them until then. prepare draws the
scene through the player's camera and one that sees all of it, waits for
what those frames started, and draws once more. A GPU step checks the
first frames after it start no pipeline (17 without it) and draw AO.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A driver reset, the GPU process crashing, or a laptop or Steam Deck waking
from sleep used to end the session. The engine now gets a new device on
the same canvas (Device.replace) and rebuilds what it loaded in place:

- the material registry, geometry arena and morph store keep their ids and
  offsets and remake their GPU side (restore); a new renderer adopts them;
- models are read again from their source and written back where they
  were (_load with into), their materials given the new textures by id;
- textures, fonts, LUTs, environments, splats and targets are rebuilt into
  the same objects; captured probes are captured again;
- loads under way finish and are rebuilt; one failed by the loss runs again.

Everything handed out is held weakly; a primitive now refers to its asset,
so an asset a scene draws is never collected out from under recovery (a
GPU step forces a collection to keep that honest). onDeviceLost gets
action 'recover' and a recovered promise; engine.gpu.loseForTest() loses
the device on purpose. In Electron a crashed GPU process came back in 1.7 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cascade skipped a batch only when the box around all its instances
missed it, so a crowd spread across a map drew every member into every
cascade: 13 of the horde's 18 ms. The cull shader gains a phase for each of
a light's cascades, testing every caster's box against that cascade's
matrix (the CPU's boxInOrtho, on the GPU). A cascade drawn whole draws a
batch that reaches past it from its culled list; one wholly inside it, or
a batch of one, draws directly. A second frame group points the shader at
the visible list for those draws.

Horde, 300 walkers, A/B in one run: shadows 12.9 -> 9.5 ms GPU, near
cascade 3.0 -> 0.6. A GPU step checks the culled frame matches the
unculled one, pixel for pixel, and fails when the cull drops casters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Meshes and their shadows, sprites, text, shapes, paths, tilemaps,
emitters, decals, splats and lights. Hiding is inherited down the
hierarchy and worked out once a frame, after a change, in one pass in depth
order. A hidden mesh carries a negative LOD radius in its bounds, so the
cull's lodSelected keeps it out of every view, shadows included (selected
as LOD casters are); merged draws read the same flag; a hidden light's
radius goes negative for the cluster pass. Raycasts and picks pass over
hidden nodes, and the scene's extent leaves their meshes out.

Nothing is rebuilt: hide and show cost 0.9 us a frame on the scene side in
a 1,000-object scene, against 30 us to remove and add (and a batch rebuild
on the renderer's). A GPU step checks a hidden group draws the frame of a
scene without it, and shown again the frame it had.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Positions, and optionally indices, normals, UVs and vertex colours, with a
colour, metalness, roughness, emissive and double-sidedness. Written as a
GLB (scene/gltf/write.js) and loaded, so it gets everything a file does:
flat normals when none are given, tangents, chunk boxes, material sharing,
pipelines built before it returns, and rebuilding after a lost device.
Defaults are a matte dielectric (metallic 0, roughness 1); a translucent
colour or vertex makes it blended.

Node tests read the written file back through the importer; a GPU step
draws a vertex-coloured, flat-shaded grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each shadow cascade picks the coarser of the camera's level and the one its
own resolution calls for, never finer than the mesh on screen. Nine hundred
spheres in three levels: shadows from 5.2 to 3.7 ms of GPU time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…GPU main thread

Renderer._init built each pass's pipelines after the last, and 16 compute
pipelines were made synchronously, each compiled on the GPU process's main
thread while everything else waited. PipelineCache.computeAsync, and every
pass at once. Electron on Iris Xe, first launch: create 1.7 -> 1.05 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first frame to cull a batch for a cascade paid 0.1 s of driver recording
once, mid-play. prepare now draws every batch through the cascades' cull, then
as they choose. First frame of play 125-180 -> 25 ms; no missed frames after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Steps keep their length and come less often; the scene's animations and
particles slow with them, the HUD keeps real time. clock.delta is the frame's
simulated seconds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A node keeps its name from the glTF file, and the nodes below one can be found
by it: spawn points, hand bones, parts to take off. createNode takes a name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… keepWorld

A node placed in the world whatever its parents, from where they are now, and
moved to a new parent without moving: ragdoll bones from physics bodies, parts
breaking off, weapons picked up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tints multiply and glows add down the hierarchy. Six 16-bit floats in the
draw data's normal-matrix padding, read as words; the vertex shader folds the
tint into the vertex colour and passes the glow flat. Only renderables whose
node changed are encoded again: 0.10 ms for 9,000 nodes, one walker flashing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BoxList.facesTouched: which faces of a cube a moved box reaches into, within
the light's sphere. The redrawn frame is the full redraw's to the pixel.
Dungeon (36 torches, 40 walkers): GPU 21.9 -> 13.8 ms. shadows._faceCull is
the A/B hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nolanbaxter
nolanbaxter merged commit 7bad151 into main Oct 30, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant