Lane D round-7: validate ped-merge → GO for roster flip + full-density defaults + escape hatch

Lane E's R7 merge (447188a) made each ped 1 mesh/1 material (~1 draw/near-rig, fleet 116->24 at
the cap) — the gate my R6 memo named. Validated + handed F the GO.

Merge validated: bind+animate/no-T-pose, skinning/skeleton preserved, silhouettes+atlas materials
crisp (hi-vis/suit/kid), impostor bake clean, identity variety + determinism intact. GPU memory
dropped (geom 205->160, tex 141->90).

Flip numbers (full density, perChunk 16, in-shell): worst street view 241 draws <=300 (was 356
unmerged), 65,899 tris <=200k, 17 near-rigs; 30-chunk soak (726 builds/disposes) leak-free, 0 GPU
delta, heap stable. => GO.

D2: enableStream default perChunk 8->16 (full density, the merge affords it); near-cap stays 24.
D3: test page + harness default to stream now; ?roster=v1 escape hatch restores the fixed roster
(verified streamMode false + determinism). No-flag boot -> streamMode true + determinism.
F's web/index.html flip wiring (invert to default-on + ?roster=v1 escape) documented in
LANE_D_NOTES; in-shell no-flag-URL boot completes when F lands F1.

Only web/js/citizens/sim.js + web/citizens_test.html changed (+ NOTES/progress).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-15 00:51:51 +10:00
parent 0c12dacb6d
commit 43edabfe56
4 changed files with 83 additions and 6 deletions

View File

@ -1,5 +1,29 @@
# PROCITY-D — progress (Lane D · Citizens)
## Round 7 (the flip) — MERGE VALIDATED, GO given to F, defaults + escape hatch ready. qa GREEN.
Validated Lane E's ped-merge (the gate my R6 memo named) and handed F the **GO** for the roster flip.
Full go/no-go numbers + F's two-line wiring at the top of `LANE_D_NOTES.md`.
- **Merge validated (D1)**: all 19 peds now **1 mesh / 1 material** (was 8/2) → ~1 draw/near-rig
(fleet 116→24 at the cap); bind+animate/no-T-pose, silhouettes+atlas materials crisp (hi-vis/suit/
kid), impostor bake clean, identity variety + determinism preserved. GPU memory dropped too.
- **The flip numbers (full density, perChunk 16)**: worst street view **241 draws ≤300** (was 356
unmerged), **65,899 tris ≤200k**, 17 near-rigs; 30-chunk soak (726 builds/disposes) **leak-free, 0
GPU delta**, heap stable. → **GO.**
- **Full-density defaults (D2)**: `enableStream` default flipped 8→**16** (the merge affords it);
near-cap stays 24 (now 24 draws not 168).
- **Post-flip + escape hatch (D3)**: test page + shell now default to stream; **`?roster=v1` restores
the fixed roster** (verified: streamMode false, determinism ✓). No-flag boot → streamMode true,
determinism ✓. F's `web/index.html` flip wiring (invert to default-on + `?roster=v1` escape)
documented; the in-shell no-flag-URL boot completes when F lands F1 (roster proven running in-shell
at the shipping default via the in-shell soak above).
Only `web/js/citizens/sim.js` (perChunk default) + `web/citizens_test.html` (default-on + escape
hatch) changed. Committed atomically.
---
## Round 6 (harden streamed roster toward default-on) — DONE. qa GREEN. Flag still default-off.
Hardened `?roster=stream` and wrote the **default-on readiness memo** (top of `LANE_D_NOTES.md`) so

View File

@ -6,6 +6,58 @@ _rotOnly/head-bone normalize/upgradeStreetPeople). Measurements on the M3 Ultra
---
## ROUND 7 — MERGE VALIDATED · **GO for the roster flip** (→ Lane F: flip on this) · escape hatch ready
**Verdict: GO. Lane E's R7 ped-merge closed the one gap the R6 memo named. Full-density streamed
roster (perChunk 16) is now under budget in a real town. Flip it default-on.**
`qa.sh --strict` GREEN. All numbers in-shell (real town "Boolarra Heads", seed 20261990).
### Merge validation (E1 — `pipeline/merge_ped.py`, `447188a`) ✅
- **Every one of the 19 peds is now 1 mesh / 1 material** (was 8 meshes / 2 materials) → **~1 draw per
near-rig**; fleet **116→24 draws at the 24-cap** (E's own count: 92→19). Bonus: GPU memory dropped
too (geometries 205→160, textures 141→90 in-shell).
- Rigs **bind + animate** (walk/idle) — mixer 0.20.3 ms; **no T-pose**. Skinning/skeleton preserved
(65 joints), clips retarget unchanged.
- **Silhouettes + atlas materials intact** — hi-vis worker (vest/helmet/boots), suit, business, comical
kid all read crisp, no atlas seams/melting; **identity variety preserved** (`pickRig` unchanged).
- **Impostor bake clean** (19 subjects, 16×5 atlas); **determinism holds** (507 chunk-keyed identities
re-derive from seed).
### The flip numbers (full density, `perChunk 16` — no reduced-density concession)
| metric | pre-merge (R6) | **post-merge (R7)** | budget |
|---|---|---|---|
| worst continuous-walk street view **draws** | 356 ✗ | **241** ✅ | ≤300 |
| worst view **tris** | ~66k | **65,899** ✅ | ≤200k |
| worst-view near-rigs | (throttled to 8) | **17** (full) | cap 24 |
| draws / near-rig | ~7 | **~1** | — |
| 30-chunk soak (3 walks = 726 builds/disposes) | leak-free | **leak-free, 0 GPU delta** | baseline |
| heap over soak | 50→77→54 | 78→63 (GC, stable) | stable |
### Full-density shipping defaults (set this round)
`enableStream` default is now **`perChunk 16`, `radius 2`** (was 8). Near-cap stays 24 (now 24 draws,
not 168). `NIGHT_LIVELY_FLOOR 0.5` for the open-late block (R6). No other knobs change.
### → Lane F: the flip wiring in `web/index.html` (F owns the shell — decision #1, flip on my word)
Invert the R5/R6 flag: **stream is the default; `?roster=v1` is the escape hatch.**
```js
const rosterV1 = params.get('roster') === 'v1'; // escape hatch → the old fixed roster
const citizens = new CitizenSim({ renderer, scene, camera, citySeed: plan.citySeed,
graph: plan.streets, fleet,
chunkStream: rosterV1 ? null : { radius: 2 } }); // default-on
```
Hours-aware (optional, recommended): after construction, `citizens.setNightLivelyChunks(keys)` where
`keys` = the openLate shop's lot→chunk + 1-ring (`sim.chunkKeyAt(x,z)`; openLate = `hours[1] >= 22`).
Everything else (`setTimeOfDay`/`setExposure`/`update`/`setPaused`) is unchanged. The
hook-driven window (`onChunkBuilt`/`onChunkDisposed` → `sim.onChunkBuilt/onChunkDisposed`) is still the
preferred precise option; poll works with zero wiring.
**Verified in my harness (test page + runtime-enabled shell):** no-flag boot → `streamMode:true`,
perChunk 16, determinism ✓; `?roster=v1``streamMode:false`, fixed roster, determinism ✓. The
in-shell no-flag-URL boot completes once F lands the two-line flip above — the roster itself is proven
to run correctly in the shell at the shipping default (the soak + flip numbers above are in-shell).
---
## ROUND 6 — streamed-roster "default-on readiness" memo (→ Fable + Lane F, for the R7 flip call)
**Verdict: functionally ready (deterministic, leak-free, hours-aware, composes) — but full-density

View File

@ -83,7 +83,9 @@ import { KeeperManager } from './js/citizens/keepers.js';
const qs = new URLSearchParams(location.search);
const SEED = (parseInt(qs.get('seed'), 10) || 20261990) >>> 0;
const NOASSETS = qs.get('noassets') === '1';
const STREAM = qs.get('roster') === 'stream'; // v2: chunk-streamed roster (default off = v1)
// R7 flip: the chunk-streamed roster is now the DEFAULT. `?roster=v1` is the escape hatch back to the
// fixed v1 roster; `?roster=stream` is still accepted (explicit). Mirrors the shell wiring F lands in F1.
const STREAM = qs.get('roster') !== 'v1';
// ---------- renderer / scene ----------
const app = document.getElementById('app');

View File

@ -147,11 +147,10 @@ export class CitizenSim {
// downstream — LOD tiers, the 24 near-cap, mixer stagger, rig pool, impostor layer — is already
// per-citizen and chunk-agnostic and runs GLOBAL across live chunks, unchanged.
// perChunk default 8 keeps the worst continuous-walk street view ≤300 draws in a real town TODAY.
// Ceiling cause: each decimated ped is 8 sub-meshes but only 2 materials → ~7 draws/near-rig, so a
// dense street of near-rigs blows the 300 budget. The clean unlock (LANE_D_NOTES R6 memo) is merging
// each ped's sub-meshes by material (8→2) — then perChunk can go back to ~16 with headroom.
enableStream({ radius = 2, perChunk = 8 } = {}) {
// perChunk default 16 = full density. Affordable since Lane E's R7 merge made each ped ONE draw
// (was 8 sub-meshes / ~7 draws): the worst continuous-walk street view now sits well under the 300
// budget even with a dense street of near-rigs. This is the shipping default for the R7 roster flip.
enableStream({ radius = 2, perChunk = 16 } = {}) {
if (this.streamMode) return;
this.roster.forEach(c => this._releaseActor(c)); this.roster.length = 0; // tear down the v1 roster
if (!this.chunkEdges) this._buildChunkIndex();