Repository navigation
Stress lab audit: honest scores, and three engine speed-ups (laptop 3.6 to 85 fps, desktop play +21%) - #659
Merged
Merged
Conversation
…red, the gameplay preset has its own lanes The stress lab is a search tool. A lever that wins only there is benchmark tuning, so the loop now says so: - `record --decision keep` fails without `--holdout "<game>: <metric> <before> -> <after>"`, and a row with no decision is a `measure`, not a `keep`. - Uncapped fair runs record presented p99 from the frame starts. The report's `frameP99Ms` and `frameWorstP95Ms` are kept: the median of the windows hid one bad window. - Rows of the `?stress&play` preset (textured materials, mesh variety, street camera, object churn) get their own lanes on the ladder and stay out of the `real` trend.
…window An arm whose URL carries holdout=<name> is a ThreeNative game with no stress report. The window reads its own TN_FRAME_BUDGET lines, skips three warm-up windows and pools the rest as the lab pools its own, so a keep's holdout comes from the same ABBA window, browser and load gate as the lever's rows. A holdout row is no lane.
…each add or remove A spawn or a despawn at the scene's top two levels restarted the settle pass, and each pass walked the whole scene twice (the snapshot and the settle). A game that throws ten objects a second paid that walk for ever: on the stress lab's new `?play` preset it was 4.1 ms of a 9.8 ms frame (CPU profile), about 2,000 bridge crossings a frame, and the bridge census leaves that walk out by design, so no meter named it. A scan now walks the subtrees that arrived since the last finished scan, plus what an earlier scan could not merge yet: a hidden subtree, a mesh that moved while it was watched, a mesh with no partner. What merges is the same; the tests pin each of those cases, and the merged census on `?play` is equal on both builds (1,260 meshes in 32 draws, 960 in 31 after the bodies wake). Probe on the desktop, one run each, shared GPU (diagnosis, not a judged A/B): crossings 2,085 -> 389 a frame, `children` reads 3,030 -> 12, frame p95 30.3 -> 9.8 ms, p99 34.0 -> 11.4 ms.
…nts drawn frames - The fair window hashed the Wasm only, so both arms of a core-only change carried one hash and a paired window could not prove it ran two builds. The hash now covers the Wasm and every script the page loads. - A frame the Wasm engine declines to draw costs near zero and shows the previous image. A holdout report's fps now leaves those frames out, and every row records renderSkipPct (the lab report does the same).
…ired auto batch window and the desktop runs
The standard and physical fragment programs evaluated every point and spot light's GGX lobe on every fragment. An unshadowed lamp's lobe now sits in one branch on its distance attenuation, which is exactly 0 past the cutoff, so the branch changes no pixel. A lamp that reads a shadow map keeps the straight path. Stress lab, laptop Iris Xe, uncapped, 16 torches of 14 m range (paired probe, two runs per arm): scene pass 206-224 ms to 8.6 ms, 3.6 to 25.3 fps at the high tier, 6.3 to 34.8 fps at the adaptive low tier, 3.35 to 19.9 fps in play. UE 5.8.3: Engine/Shaders/Private/DeferredLightingCommon.ush:334-344.
PRD-871 plans the work after the 2026-11-10 audit: close the lamp range lever, bring the laptop adaptive lane to vanilla speed, and cut the virtual shadow planning cost on the desktop. The visual judge of the lamp range change read NEUTRAL. Rows #334-#337 are the desktop window range-score (base, head, one sun map, vanilla three.js).
The same branch as the standard and physical programs (078aae5): an unshadowed point or spot light evaluates its diffuse and Blinn-Phong terms only where its distance attenuation is above 0. The normal, view direction and Lambert term are evaluated once before the first branch. Test: native_engine_material_lamp_range_branch now covers buildLambert and buildPhong; it failed on the old loop and passes (27 material and TSL tests).
qualityTiers.shadow(tier).castShadows already said that the low tier casts no shadow, but only the kart kit applied it. The starter (and every game built on it, the stress lab included) drew the shadow pass at every tier. On an Iris Xe at the low tier that pass was the largest GPU cost: 11.8 of 27.4 ms. setupPost now sets the renderer shadow map from the tier at each tier change. A named `shadows` option keeps the game choice at every tier, and both TN_QUALITY_TIER and debug().shadows name the source (tier or override). PRD-871 Phase 2. Spec: starter-shadow-tier.spec.ts, red before, green after.
The pass recorder cannot instrument the owned engine, so gpuMain, gpuShadow and gpuOther stayed empty there. The facade now publishes each resolved sample as a split (scene pass; sun and lamp shadow maps; other passes, output and gather) under info.engine.gpuSplit, core gpuFrameSample carries it while it names the same frame, and the frame loop feeds the existing buckets from it. PRD-871 Phase 2. Spec: renderer.spec.ts "hands out the engine own pass split", red before, green after; frame-budget and GPU observation specs pass.
Laptop window lit-holdout-laptop (Iris Xe, uncapped, two runs per arm): the stress lab adaptive lane 7.1 to 36.0 fps, the tower-defense kit 109.3 to 114.2 fps. Desktop window lit-holdout: every lab head run voided by CI pool load; its kit pair holds. Ledger rows #338-#344. The learnings file gets the lever entry and the method traps.
The virtual shadow planner keyed each draw's shadow box on its world matrix too, so a rigid caster that moved re-read every vertex position to rebuild a box that only needed the new transform. The draw cache now keeps the vertex box in geometry space, keyed by the position store revisions alone. Stress lab, desktop, temporary sub-timers: 234 moving rigid draws a frame; vertex scans 234 -> 0, bounds rebuild 1.38 -> 1.12 ms. The rest of the rebuild is 240 rig draws (0.87 ms) and 11 instanced draws (0.20 ms). PRD-871 Phase 3. Red-green: virtual_shadow_bounds_cache (render_database_test) failed on the vertex scan count before, passes after; all 109 database cases and the shadow render cases pass on a native engine-only build.
The virtual shadow box of a skinned draw multiplied each bone world matrix by its inverse bind matrix every frame, only to read the stretch of the product. The inverse bind stretch now rides on the cached bone reach, so a frame reads the stretch of the bone world matrix alone. The product of the two stretches bounds the stretch of the product, and equals it for a rotation with uniform scale, so the box still holds every skinned vertex. Stress lab, desktop, temporary sub-timers, 240 rig draws a frame: rig box 0.87 -> 0.72 ms. PRD-871 Phase 3. Tests: skeleton, skinned palette, VSM, glTF hierarchy and fixture cases in the Wasm build (23 passed); render database, skinned crowd, renderer and shadow render cases on a native engine-only build (115 passed). The ledger also takes rows #345-#346: the laptop Phase 2 window (adaptive, both arms at low: base 35.8 fps, head 63.2 fps, shadow pass 11.3 -> 0 ms).
With the low tier now free of its shadow pass, the stress lab on an Iris Xe measures low at 12.7-13.7 ms: inside the 20% headroom line, so after five healthy windows the policy raised to medium (shadows on, 27 ms), fell back two windows later, and tried again after each 5 s cooldown. The shadows switched on and off every few seconds. Each raise that the next change undoes now doubles the wait before the next raise, up to 32 cooldowns; a raise that holds resets it. Steps down keep the plain cooldown. PRD-871 Phase 2. Spec: template-quality "should wait twice as long before retrying a raise the next window undid", red before (medium after one cooldown), green after; 55 quality and shadow-tier specs pass.
The Wasm facade publishes info.engine.gpuSplit since 4bd3989; the V8 player now does the same from its timer segments (scene; sun and lamp shadows; other, output and gather), so TN_FRAME_BUDGET gpuMain, gpuShadow and gpuOther fill on desktop too. player-gpu-stages.mjs asserts the split names the sampled frame and matches the stage times. Its ball now moves: the scene was static, so the cached sun shadow map drew no pass in steady frames and the existing Shadow > 0 check failed with and without this branch (checked on a player built without the PRD-871 Phase 3 sources). PRD-871 Phase 2. Proof: node tests/native-engine/player-gpu-stages.mjs build/engine-only/tn-native-engine-player-v8, RTX 2080, PASS.
The backoff of 97c33a8 still retried medium every few cooldowns on the Iris Xe stress lab, and each retry showed the shadows for two windows. The policy now keeps what a failed raise cost as a ratio of the cost before it (medium 28 ms over low 12 ms) and raises from that tier again only when the predicted cost fits the headroom line. A lighter scene still climbs; a scene that cannot hold medium stays at low and reports reason=predicted. PRD-871 Phase 2. Spec: template-quality "should not retry a raise that failed until the cost it measured would fit", red on 97c33a8 (medium after ten minutes at 12 ms), green after; 55 quality and shadow-tier specs pass.
The debug counter uniformUploadBytes added the uniform arena size each frame, while the FM2 upload sends only the blocks that changed. On the stress lab still pose with nothing moving it read 39 KB a frame beside a 516-byte real upload, so it pointed a profile at an upload that does not happen. It now adds the bytes each arena write sends. PRD-871 Phase 3. frame_slab_is_per_program: red before (still frame 6144), green after (still 0, moved 512 of 516 queued bytes); all 136 render database cases pass on the engine-only Dawn build.
The output pass wrote its uniform block (exposure, time, screen size) every frame, so a frame with nothing moving still made one writeBuffer call: the last upload of a still frame. The renderer now keeps the bytes the buffer holds and writes only a block that differs; a new buffer clears them. PRD-871 Phase 3. frame_slab_is_per_program: a still frame uploaded 4 bytes (red), now 0 (green). 136 render database cases, 98 renderer/output/ present tests and 14 post tests pass on the engine-only Dawn build.
The low preset left ssrEnabled unset, and the default (look layer 8 and up) turned SSR on: the phone tier, documented as "nothing screen-space", ran reflections. Their normal, metalness and roughness reads made the owned engine redraw the whole scene into a second G-buffer pass: 6.7 of 26.7 ms of the stress lab frame on an Iris Xe at low (per-pass GPU timers). Unreal draws none at that level: UE 5.8.3 Engine/Config/BaseScalability.ini:514-515, [ReflectionQuality@0] r.SSR.Quality=0. Medium and high keep their SSR. PRD-871 Phase 2 (AC-1). world-environment-defaults "draws no screen-space reflections on the low tier": red before (stages ssr, bloom, ...), green after; 484 related look, kit and render-chain specs pass.
6dd609e put the low-tier shadow switch inline and took src/render/postprocessing.ts to 212 lines; looks.spec caps a render file below 200. The switch is now tierShadows() in quality.ts (the same rule, override and report), and the post file is 199 lines. looks, starter-shadow-tier, template-quality and scaffold specs pass.
…8.3 thread and planning guards - Remove docs/PRDs/performance/PRD-828: a stale pre-work copy that #635 re-added. The finished PRD stays in docs/PRDs/done/ (DONE 2026-11-07). - PRD-573: v2.1, v2.2, v2.3, v3.1, v3.3, v5.1 and v5.2 name what landed (L5, FM2, FA1, PRD-828, PRD-613, PRD-752) and what is still open. No box ticked. - PRD-573, PRD-774, PRD-752: Unreal 5.8.3 reference for parallel pose evaluation, command build at add time, the late join, the frame lag limit, deferred release and the flush before a surface change. Each cited line was re-read in the clone on 2026-11-10. - PRD-765: P2 to P1, on PRD-828 follow-up 7 (about 1.2 ms of JS a frame). Checks: check-doc-links (3918 links), unreal-port-status --check up to date, check-doc-links and evidence-citations specs pass. evidence-budget spec fails in this worktree only (no node_modules here; 9 of 9 pass in the main checkout).
…60 fps (João); laptop frame by pass
… not a per-frame set (PRD-878) project kept the skeletons it had updated in an unordered_set that it cleared every prepare, so each rig cost a node allocation, a hash insert and a free, and a reflection pass probed the main pass's set. Each prepare now takes an id that is unique across databases. A skeleton records the id of the prepare that updated it. A reflection compares that stamp with the main pass's id, as it did with the main pass's set. Same-binary A/B on ?stress (3 runs of 120 s): project with the set minus the stamp +0.019/+0.154/+0.050 ms. Full native-engine label 686/686.
…draw's variant stamp holds (PRD-878) Every shadow row built a VertexVariant, with a copy of the material's node slots, and compared it with the cached one: about 550 rows a frame on ?stress, nearly all of them one per draw, so a per-build memo did not help. refresh() now stamps a draw when it re-resolves the material half. That covers the material's signature, the geometry revision and a converted input. The inputs the frame, the scene and the batch set per draw (fog and its node, environment, instancing, skinning, palette, morph, side, the material pointer) are matched exactly in a DepthRowKey. A row whose stamp and key match keeps its program. Red-green native_engine_shadow_row_follows_material_change: a shadow-only caster's material gets alphaTest 0.5 by a plain write. With the stamp not moved, the row keeps the unmasked program (red). With it, the row takes the masked program (green). Same-binary A/B on ?stress (3 runs of 120 s): shadow build without the stamp minus with it +0.114/+0.100/+0.113 ms. Full native-engine label 686/686.
…eometry revision (PRD-878) rigLaneKey ran skinnedLaneReason for every held rig every frame, and that made four string-keyed attribute lookups (position, normal, skinIndex, skinWeight) plus the morph lists. The geometry half is now held on the geometry for its revision, so the batch's guard is unchanged but reads a version. The ABI's morphAttributes setter moved the morph lists without a revision bump. It now bumps the revision, so every render cache held for the revision sees a morph set. Red-green native_engine_abi_rig_lane_follows_morph_set: a rig's lane is clear, then morphAttributes.position is set through tn_set. Without the bump the lane stays clear (red). With it, the lane reads "morph" (green). Same-binary A/B on ?stress (3 runs of 120 s): batch with the lookups minus the held half +0.008/+0.027/+0.014 ms. Full native-engine label 686/686.
… reads the verdict map (PRD-878) batch() looked up every draw's rig in the decision's verdict map, a tree of about 240 rigs, and only then dropped a virtual-shadow page caster, which it always drops. The 240 shadow-only rig draws on ?stress now leave first. Same-binary A/B on ?stress (3 runs of 120 s): batch with the old order minus the new +0.017/+0.018/+0.021 ms. Full native-engine label 686/686.
…tires the shadow row's cached depth program (PRD-878) The callback runs after refresh() stamped the draw, so that frame's shadow row kept the old depth program and alpha mask. The draw and the draw list's copies take a new variant stamp. Also covers deleting a rig's skin attributes.
…the remaining items stay open
Red-green: palette_lifetime fails on steady-frame allocation, then passes with versioned storage and direct dense slots (no claim map). Removal, addition, material and geometry replacement rebuild storage. CPU-only held-pose, compaction and skin-attribute cases pass. Expected ceiling ~0.10 ms; A/B pending.
Item 3: construct in place from the consumed first member; remove both the temporary copy and its push copy. static_assert requires noexcept DrawItem move construction. CPU batch_compacts_in_place, palette_lifetime and held_pose_skips_the_world_palette pass on the rebuilt target. Record-owned draw copies stay intact. Expected ceiling ~0.03 ms; A/B pending.
Red-green bone_liveness counts weak-owner reads across three steady frames (zero), then frees a shared/duplicate bone and verifies every dead slot is null. Bone destruction bumps a scene-thread generation; each skeleton scans its weak slots once after that generation moves. pose_version, skeleton_pose, clone_bounds and raycast_posed_skin pass. Expected ceiling 0.04-0.07 ms; A/B pending.
Red-green projected_cull_multiple_views covers two culls/two cameras on one scene, the root-owned bridge sharing a cull across cameras, two scenes, and repeated calls in one frame. Keep per-camera rig walks/rates and reuse same-frame results; hold turns still derive from the supplied frame. Existing projected_cull_band_animation, projected_cull_bands and projected_cull_list pass. Correctness guard for the ~0.10 ms JS rate-band move; A/B pending.
…that changed (PRD-879) `__tnStaleRows(blocks, plan)` patches the slab the engine holds: an unchanged cell block is a [source, count] run of the held rows, a rebuilt one is sent. `patchRows` is the upstream-three twin; the JS slab is no longer built on the engine path. Red-green: world-cells-main-cull.spec.ts "sends the engine's stale rows only the cell blocks that changed (PRD-879)" (red on the whole-slab pack, green after); abi_test world_stale_entries patches one block and refuses a run past the held rows or unread blocks (red on the old binding, green after).
…s eight corners twice (PRD-879) The drain's largest per-record leaf on the forest walk was #boundsAt (0.12 ms a frame of 0.27 in addPlacements, before1 cpu profile): Box3.applyMatrix4 over eight corners, run twice per placement with the same matrix. `placedBox` is the engine BOX op's math (centre moved, |basis| * half), and addPlacements reuses the first answer for its level boxes. The main cull's per-record bounds take the same helper. Red-green: world-cells-main-cull.spec.ts PRD-879 guard now counts Box3.applyMatrix4 in the walk (5556 before, 0 after); "measures a placed box as Box3.applyMatrix4 does, in closed form" checks the answer against three's for a rotated, mirrored placement.
… forest playtest box ticks on three exit-0 runs
…ms, crowd cost 4.16 to 3.70 ms (AC-1 not met)
…C-1 partly met, the drain bookkeeping stays in JS
…est row is the lab's own update
…); the open bars move to PRD-881 and PRD-882
jonit-dev
marked this pull request as ready for review
November 12, 2026 00:08
…interpolation.ts lint, one gitleaks false positive - stb headers downloaded from nothings/stb master no longer matched the locked sha256 (upstream moved); the lock already named revision 2c980bb5, so the URLs now use it (all three hashes verified at that revision) - packages/core/src/interpolation.ts: inline type import and Biome format (PRD-877 code) - .gitleaksignore: scripts/stress-loop.mjs line 324 is a metric label, not a key
…B; PRD-881 carries the reclaim PR #659 took the single-thread engine to 4,288,337 bytes and the threaded one to about 4,298,000 (+0.7%) for a 16% lower ?stress frame p50. build-artifacts and test failed on TN_WEB_ENGINE_OVER_BUDGET. The number lives in check-budgets.ts, the build action and the spec, which keeps them equal.
…s engine sources (81.99%)
jonit-dev
added a commit
that referenced
this pull request
Nov 12, 2026
Promote develop to main: the crowd and forest world run faster in the engine (#659)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is this PR about?
An audit of the stress test (scene, method, dashboard) and the first engine speed-ups that came out of it. The stress lab now measures the way a player plays, and two engine fixes make real frames faster.
Why does it matter?
The stress lab is how we decide if the engine got faster. Before this PR it could reward changes that only helped the benchmark. It also hid that a laptop GPU drew the scene at 3.6 fps.
Done
Method (no "benchmaxxing"):
play) has its own lanes. The ledger keeps the GPU frame by pass (shadow, scene, other).Engine:
Auto batch scans what arrived, not the whole scene, after each add or remove. Desktop RTX 2080, uncapped:
real86.8 to 92.7 fps,play39.7 to 49.4 fps (single runs).A lamp shades only the pixels inside its range. Every pixel paid for all 16 torches before. Laptop Iris Xe, uncapped, paired probe (two runs per arm): scene pass 206-224 ms to 8.6 ms.
real3.6 to 25.3 fps, adaptive low tier 6.3 to 34.8 fps,play3.35 to 19.9 fps. The design follows Unreal (UE 5.8.3: Engine/Shaders/Private/DeferredLightingCommon.ush:334-344).The
lowquality tier draws no shadow pass (startersrc/render/postprocessing.ts, named overrideshadows). Laptop,?stress&adaptive, both arms atlow: shadow pass 11.3 to 0 ms, 35.8 to 63.2 fps (ledger fix(engine-mcp): a shared symbol's detail returns every match in one call #345-fix(playtest): aggregate retained render samples per pass kind so assert.performance stops dying on payload #346).The
lowtier draws no screen-space reflections, as Unreal's lowest level (r.SSR.Quality=0). The default had turned SSR on, and its reads forced a second full-resolution G-buffer pass: 6.7 of 26.7 ms on the hot laptop. Laptop?stress&adaptive: head runs 84.6 and 67.5 fps (base 78.2 and 47.0), GPU p95 12.3 to 9.0 ms (rows docs(prd): PRD-452 records its Phase 3 evidence and DONE status #349-docs(guides): the public guides move here as Markdown, the site renders them #350). Before PRD-871 this lane drew 7.1 fps.A still frame uploads nothing. The upload counter counted the arena size, not the bytes sent; now it counts what crossed. The last unchanged write (4 bytes of output uniforms) is gone.
A failed tier raise is not retried until its measured cost would fit. Without it the laptop flipped
lowtomediumevery few seconds and the shadows blinked.TN_FRAME_BUDGETnames the GPU pass that costs on the owned engine:gpuMain,gpuShadow,gpuOther, from the Wasm facade and the V8 player.Shadow planning (desktop): a moving rigid caster keeps its vertex box, and a rig box skips one 4x4 multiply per bone. Bounds rebuild 1.38 to 1.06 ms a frame (sub-timers). The fair window was inconclusive under host load.
A
playframe stops walking the scene for LOD chains that were never registered. Each frame walked about 10k objects through engine proxies (15% of main-thread time in a CPU profile of?stress&play) and found nothing, because the scene registers no chain. Desktop, fair windowplay-lod-walk-c(uncapped ABBA, two runs per arm, load peak 7.0): 66.0 to 79.7 fps (base 63.9/68.0, head 78.1/81.3; rows fix: preserve Android consumer cohort arms (PRD-366) #364-docs: record isolated Linux UI cadence proof (PRD-399) #365), frame p50 11.71 to 11.04 ms, p95 26.46 to 18.35 ms. Holdout: the tower-defense kit holds at 286.9 to 306.5 fps (rows PRD-449: ThreeNative vs Godot scoreboard and TN fixes #366-PRD-446: Guard public API changes and prove N-1 upgrades #367, no effect — it registers no chain either).Checks run:
native_engine_material_*(21 tests) pass on the Wasm build. The newnative_engine_material_lamp_range_branchtest failed before the change and passes after it.744a9321c.starter-shadow-tier.spec.tsand the twotemplate-qualitypolicy tests: red before, green after (55 pass).renderer.spec.tsGPU split test red then green; 119 core frame-budget specs pass.virtual_shadow_bounds_cachered then green; 109 render database cases, shadow render cases, crowd and renderer tests pass. Wasm build: 23 skeleton, skinning and VSM tests pass.world-environment-defaults"no screen-space reflections on the low tier": red then green; 484 look, kit and render-chain specs pass. Visual judges NEUTRAL for shadows off (still, town) and SSR off (still, town, closeup).player-gpu-stages.mjson the V8 player (RTX 2080): PASS. Its oldShadow > 0check failed with and without this branch (static scene); the test ball now moves.shadow-node.spec"never walks the scene while no LOD chain was ever registered": red (3 walks) then green; 617 three-native and model-lod specs pass.?stressand?stress&play, uncapped ABBA: rows fix: complete PRD-112 packed golden path repair #360-docs: record isolated Linux UI cadence proof (PRD-399) #365.play-lod-walk-balso recorded its void runs: thetn-localCI pool took the host to load 45 mid-window, which is why each of its arms has one valid run.play-lod-walk-cis the clean window (two runs per arm, load 3.8-7.0).td-lod-holdout(rows PRD-449: ThreeNative vs Godot scoreboard and TN fixes #366-PRD-446: Guard public API changes and prove N-1 upgrades #367): the tower-defense kit, head staged as the same builtdistwith the guard patched into the shipped facade chunk, so the arms differ by 25 bytes and no engine interface moves.PRD-871 checklist
PRD:
docs/PRDs/performance/PRD-871-stress-lab-laptop-parity-and-desktop-shadow-cpu.mdPRD-872 (
docs/PRDs/performance/PRD-872-static-casters-and-rigid-rigs-in-the-desktop-frame.md) is filed in this PR as the successor for those two items, and both of its levers are now landed here and measured:da185d67e): the plan keeps one record per caster across frames (bounds, state, and the inputs they came from);CasterState::signatureis a version triple instead of astd::string, and the atlas marks presence with a stamp instead of astd::setinsert per caster.f95ea2eb6): the box is keyed on the fold of the bone locals,inv(mesh world) * root worldand the mesh scale.Measured, after three more exact cuts (bone positions transformed once per skeleton instead of per mesh, one
linearStretchper skeleton instead of 7,800, andupdateCastersskipped on a frame where no caster moved):plan-floor-c(ci(PRD-463): take the Android V8 build off the native critical path #372-PRD-463: realistic-scene ladder vs Godot #373, uncapped ABBA, load 2.0-2.6): base 105.0 fps / frame p50 8.96 ms, head 109.1 fps / 8.91 ms;planVirtualPages1.17 → 0.94 ms (0.35-0.44 with&noCrowd);So PRD-872's three acceptance numbers are still unmet: 8.91 against 8.4, 0.94 against 0.5, 3.9 against 2.5. The measurement says why, and it is recorded in PRD-872's Decisions:
planVirtualPagesis 0.35-0.44 ms with the crowd removed entirely, so 0.5 ms was never the planner's to reach with 240 animated rigs in the pass; and the crowd's 3.9 ms is animation (~1.0), world-space palette write and upload (~1.4), shadow plan (~0.5) and JS (~0.5). The next lever is the component-space palette — multiply the palette byinv(mesh world)and let the draw's own model matrix re-apply it, which three already does after skinning, so a walker that moves with a throttled pose stops rewriting 65 bones a frame. That is a skinning change with its own PRD, its own visual judge and both passes changed together, not another box here.Phase 1, the lamp range lever is closed:
Phase 2, the laptop adaptive lane reaches vanilla speed:
lowtier draws no shadow pass without game code (starter spec red-green; laptop shadow 11.3 to 0 ms; judge NEUTRAL).TN_FRAME_BUDGETcarries the GPU frame by pass on the owned engine (core spec; lab lines; V8 player test PASS).Phase 3, the desktop frame loses its shadow planning cost:
planVirtualPagesat 0.5 ms or less on an unchanged frame (1.02-1.64 ms today) — moved to PRD-872 Phase 1: the whole stage is 0.8-1.0 ms of a 10.0-10.65 ms frame, so it cannot carry AC-2 on its own.frame_slab_is_per_programred-green twice; still frame 0 bytes).playframe has a full stage split (stageSum13.07 ms against frame p50 12.55 ms, 104%) and loses its largest removable term: 66.0 to 79.7 fps, frame p95 26.46 to 18.35 ms (rows fix: preserve Android consumer cohort arms (PRD-366) #364-docs: record isolated Linux UI cadence proof (PRD-399) #365).Acceptance:
?stress&adaptive: 60 fps or more in every run (raised from 53 by João): 84.6 and 67.5 fps (docs(prd): PRD-452 records its Phase 3 evidence and DONE status #349-docs(guides): the public guides move here as Markdown, the site renders them #350).?stress: frame p50 8.4 ms or less (10.00 base / 10.65 head today, rows feat(prd-196): prove published Android cohort in the clean room #362-docs(prd-365): close public registry desktop proof #363) — handed to PRD-872, which files the scene dirty list and the rigidly-moving-rig work; this PRD closes when that one ticks it (zeroing the planner alone still reads about 9.0-9.6 ms).?stress&play: 60 fps or more — 79.7 fps (base 66.0) in windowplay-lod-walk-c, rows fix: preserve Android consumer cohort arms (PRD-366) #364-docs: record isolated Linux UI cadence proof (PRD-399) #365; it was 49.4.Desktop window
range-score(ledger #334-#337, uncapped): ThreeNative virtual shadows 95.0 to 96.7 fps, one sun map 137.1 fps, vanilla three.js 75.3 fps.Not verified
pnpm typecheck,pnpm lint,pnpm test.shadow-plan(fix(core): a throwing teardown can no longer kill a streaming world frame #347-fix(core): a cliffed terrain tile stays inside the LOD pop bound #348) has one valid base run; no end-to-end verdict yet. The walking crowd costs 4.5-6.5 ms of the 10-12 ms desktop frame; that is the next lever (PRD Handover notes).PRD-875, PRD-876, PRD-877 (absorbed from #663, 2026-11-11)
#663 is merged into this branch (
10b50e558) and closed: one PR for the stress-lab chain. Its rows, unchanged: PRD-875 Phase 1 (animationTickMicros, the Euler and event-split races, TSan 292/292), PRD-877 Phases 1-2 (interpolation meters, C++PoseHistory), PRD-876 Phase 1 (pipelineSyncCreates,programBuilds), and PRD-573 v1.9 (owned-engine ladder column).Since the merge:
Gains, fair window
877-pose-history(ledger feat(PRD-464): realistic-scene ladder — full scene at 1080p, TN 104 fps vs Godot 196 fps #374/PRD-458/473: streamed open worlds toward 60 fps (LOD, shadows, GPU scene); real-display check pending #375, RTX 2080, uncapped ABBA, head81901d260against develop64a6c32ab): frame p50 9.32 → 8.15 ms (both pairs lower: 8.09/8.20 against 9.15/9.49), frame p95 11.17 → 10.33 ms, 103 → 117 fps, GPU p95 6.09 → 5.08 ms. Debug read: physics interpolation 1.05 → 0.10 ms a frame, JS shared-transform writes 8,379 → 1,458 a frame. Fresh Opus visual judge: NEUTRAL, no regression.✅ PRD-877 done (
788567cda): the engine blends and restores physics poses in one call. Fixed on the way: C++blenddirtied every settled body every frame. The kartsmooth-driveplaytest passes on the engine path.✅ PRD-876 done (
51e2777d3): on the real GPU the Strata forest turn makes 0 sync pipeline creates and 0 program builds after the reveal; warm-up compiles 319/319 before it. Phases 2-3 closed by the sizing gate.✅ PRD-875 done (
200e1966d): the animation tick is 0.27 ms mean, so a pool saves at most ~0.20 ms (< 3% of the frame). Phases 2-3 closed by the sizing gate; WIP parked locally.✅ The 5 ungated native reds pass (
41135ff29, those cases rerun; the full label was not rerun after the fix). They were stale tests (MRTnormal, a bound class, r187 noise hashes), a memo check that never ran (top-level await in an IIFE bundle), and two Canvas2D cases now registered only with Skia.✅ PRD-872 AC-1 and PRD-871 AC-2 (frame p50 ≤ 8.4 ms) met on the same window.
✅ PRD-872 and PRD-871 done (
9c699f0c3, palette lane8c568dc0e): AC-1/AC-2 met (frame p50 8.09/8.20 ms ≤ 8.4). The remainingplanVirtualPagesand crowd levers measured under the 0.3 ms floor (relative-slot skip ≈0.05 ms, block-diff ≈0.07, bone walk ≤0.18), so they were closed by decision. Next real levers (bone-write stamps, GPU-joint crowd admission) are new PRDs.573a65a7b: world cells and terrain tiles run their frame in C++ (tested C++ twins exist, unbound; JS copies run every frame)Status 2026-11-11 (head
082c6807e) — ready for reviewDone, in
docs/PRDs/done/: PRD-871, 872, 875, 876, 877 (the folded-in #663 work), and PRD-878 and PRD-879, closed at their measured values by the owner ("option 1"). The open bars moved to two new PRDs: PRD-881 (the crowd's engine-owned stages to 2.5 ms,planVirtualPagesto 0.5 ms) and PRD-882 (the world drain and the desktop forest run).Measured (fair window
fwfinal, ledger rows 376-381, ABBA, mean of 2 runs, 0 void, RTX 2080):?stressframe p50 9.22 -> 7.75 ms (-16%), p95 11.50 -> 10.60, GPU upload 1,191 -> 638 KB a frame,planVirtualPages1.20-1.26 -> 0.79-0.89 ms. Head wins every ABBA pair. The crowd cost (?stressminus&noCrowd) fell 4.16 -> 3.70 ms; the bar of 2.5 ms was not met and is carried by PRD-881. Forest: playtest exit 0 three times,WorldCells.updatewalk about 6.7 -> 3.5 ms in the first lane. Fresh Opus visual judges: NEUTRAL on the walkers and the forest.Verified on
ee22d38c7(docs-only commits since): native-engine label 695/695 on freshly built test targets, registry dump equal, core world specs 1,034 passed, typecheck exit 0; doc checks and five doc specs pass on this head.Not run: the full
pnpm ci:localboard, and a desktop run of the forest (PRD-882). Not merged to develop.