Skip to content

Release 1.6.1: off means off, for every scene - #6

Merged
nolanbaxter merged 13 commits into
mainfrom
develop
Oct 9, 2026
Merged

nolanbaxter merged 13 commits into
mainfrom
develop

Conversation

@nolanbaxter

Copy link
Copy Markdown
Owner

Off means off, for every scene. A sweep of every feature for work done with nothing to do,
beyond the scenes the last release was measured on: occlusion culling with nothing to cull, the sky
under everything, OIT and depth of field with nothing to show, texture lookups a material does not
have, and an idle character that cost a scene its static fast paths. Each fix A/B'd, the same
pixels. Sponza GPU -8%, -21% with depth of field set; a town of untextured buildings -12% to -24%;
10,000 objects and an idle character, CPU -40%.

Changed

  • Off means off, for more than Sponza. A sweep of every feature for work done when it has
    nothing to do; each fix A/B'd on Sponza (with OIT, ambient occlusion or depth of field on) and a
    town of 900 buildings, the same pixels throughout (one channel moved by one level, from grading).
    • Occlusion culling's depth pyramid, second cull and second pass are skipped when every opaque
      mesh is merged, as they are culled by view alone. Sponza: GPU -8%; the town: -13% to -24%.
    • The sky is drawn after the opaque geometry, where nothing covers it, instead of under all of
      it first.
    • With oit on, a frame with nothing blended in view skips its two
      full-screen targets, its pass and its resolve; with it off, the separate blend pass is skipped too.
    • Depth of field whose largest blur is a pixel or less skips its three
      passes: the composite took the sharp colour everywhere. Sponza at f/22 focused at 10 m: GPU -21%.
    • The tonemap skips colour grading's white balance, contrast and saturation when they are neutral.
    • A material with none of the metallic-roughness, normal and occlusion maps skips their lookups
      and the normal map's basis, and one with no emissive map skips that lookup; what they would give
      is known. A town of untextured buildings: main pass -17%, GPU -12%. Textured Sponza: unchanged.
      An untextured surface's normal is now exactly its own: the 1x1 default decoded a hair off flat
      (4 channels in the town's frame moved by up to 2 levels).
    • A skinned or morphed mesh standing still costs what a static one does. A skin's bounds counted
      as changed every frame, posed or not, so one idle character had every frame re-union every
      box in the scene, rewrite every deforming object's draw data and rebuild the shadow casters'
      boxes. Now only a box that changed counts. 10,000 boxes and one idle Fox: CPU -40% a frame;
      the same pixels. Walking, as before.
    • Light clustering runs neither of its passes in a scene with no point or spot lights and no
      decals, once a frame has cleared the counts it leaves.
    • The pass that picks the shadow casters' levels of detail is skipped on frames that draw no
      shadow map.
    • TAA turned off frees its two full-size history images (15 MB at
      720p, 33 MB at 1080p); turned on again, it makes them anew.

npm install winding-engine

🤖 Generated with Claude Code

nolanbaxter and others added 13 commits October 8, 2026 11:55
- Occlusion culling: no HZB, late cull or late pass when every opaque
  batch is merged (merged draws are view-culled only).
- Sky: drawn after the opaque geometry with an equal depth test against
  reverse-Z's cleared 0, so it shades only what shows.
- OIT and the separate blend pass: skipped with nothing blended in view.
- DOF: skipped when the largest disc is a pixel or less (the composite's
  blend weight is the disc less one, so it was the sharp colour).
- Grading: a flag in domainMin.w skips gradeLinear when neutral.

Sponza GPU -8% (also with OIT or AO on), -21% with DOF at f/22; the town
-13 to -24%. Pixels identical but one channel by one level (grading).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bits above the UV-set mask say which optional maps are present. With none
of metallic-roughness, normal and occlusion, the shader skips all three
and the normal's basis on one uniform branch; with no emissive map, that
lookup. No pipeline variants. A branch per map cost textured Sponza ~2%;
one branch for the three does not.

Untextured town: main pass -17%, GPU -12%. Sponza unchanged, identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
applySkinBounds counted every skinned renderable as changed, so `skinned`
was never 0 with a character in the scene: the scene extent was re-unioned,
every deformer's draw data rewritten and the shadow batch boxes rebuilt each
frame. It now counts only boxes it changed, and the draw-data scan and
boundBatches take one `deformed` flag (a skin or morph changed a box this
frame) in place of "the scene has skins or morphs".

10,000 boxes + an idle Fox: CPU -40%; walking: level. Pixels identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Clustering skips both passes with no lights or decals, after one frame
  has zeroed the counts (a shader still building with its light loop
  reads them).
- The shadow casters' LOD cull runs only on frames that draw a shadow map
  (Shadows.drawsAny).
- TAA off frees its two histories; on again remakes them. The GPU test
  turns it off and on and checks both.

Same pixels on Sponza with and without point lights.

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

- Merged groups draw with drawIndexed: their count and place are fixed
  between rebuilds, and the indirect draw's validation was the ~7 us each.
  Sponza GPU -12 to -17%.
- The opaque prepass uses vsMergedDepth (position only, @invariant).
  Another 3-5%; pixels identical.
- scene.removedBoxes records a removed renderable's box; a revision change
  no longer forces changes.all, so only the shadow maps an added or
  removed object reaches are redrawn. Matches a full redraw exactly
  (tools/add-shadow-check.html).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A new renderable's world box starts as NaN, not its slot's last
  occupant's: that stale box was recorded as "before" and redrew shadow
  maps far from the new object (a far add redrew 4 cascades, now 2).
- Batch keys are numbers, not one template string per renderable; merged
  records are written field by field, not through array literals.

Town, one box added: 14.9 -> 14.5 ms (16.9 before a0b6545); remove
15.5 -> 13.1. Shadows match a full redraw.

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

- Alpha-masked merged groups get a prepass (fsDepthMask: the mask's
  discard) and shade with depthCompare 'equal', no depth write and
  USE_ALPHA_MASK 0. 1,200 leaf quads: main pass -6%; one pixel at a tie.
- A changed material forces a full shadow redraw only if it is masked or
  blended: opaque casters are depth-only.
- Splats decide their sort by the unjittered projection (renderer passes
  _taaSaved.projection); the GPU test now checks a still view under TAA
  sorts nothing (it fails without the fix).
- warmMipPipelines builds the mip/resample pipelines at renderer start.

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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test/fixtures/soak.js repeats what apps do (models, scenes, spawning,
features, lights, effects, environments, targets, probes, 2D levels,
whole engines) and counts GPU buffers/textures to destroy(), pipelines
and shader modules made, and (with --js-flags=--expose-gc, now passed by
gpu.ci.js) that everything let go of is collected. It fails on the old
IBL code; it passes now.

Closed:
- IBL and HZB compile shaders and layouts once per device (environments,
  probe captures and targets each left 2-3 pipelines in the cache).
- Probe slots reused; a scene's empty probe set destroyed.
- 2D views in a Map, destroyed 120 frames after last drawn; no view pins
  the first 2D scene.
- Device.destroy unconfigures the context and drops its listeners.
- asset.source is null (Sponza: 9.4 -> 0.4 MB of heap kept).
- Decal array let go after a quiet spell; sprite runs, spritesChanged,
  no-probes texture, engine._drawn.

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

- Font atlas starts again at 4096 (Font.atlasCap) instead of doubling to
  the device limit; text draws its glyphs back first (ensureBoxes, by
  font.generation) and packs read rectOf. Kerning memo bounded.
- Local shadow array shrinks after 120 frames under a quarter used
  (12 shadowed point lights: 128 layers -> 1).
- play({ join }) replaces a finished run of the same clip; warns past 64.
- View2D prunes padded tilesets no live map uses; particles clear their
  lists with no emitters; jobs hold the shared owner in a WeakRef.
- GPU tests: atlas restart, shadow layers given back; the soak's lights
  now really cast (castShadow, not castShadows) and gain a burst scenario.
- CHANGELOG 1.6.1 dated 2026-10-09.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nolanbaxter
nolanbaxter merged commit 9f0faf1 into main Oct 9, 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