From 3b86b0c5b3b586caf807dc0b7a549da0a2707bcf Mon Sep 17 00:00:00 2001 From: m3ultra Date: Tue, 14 Jul 2026 23:10:11 +1000 Subject: [PATCH] =?UTF-8?q?Lane=20F=20R6:=20=E2=89=A4350=20interior=20draw?= =?UTF-8?q?=20gate=20+=20fix=20interior=20renderer.info=20accumulation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added an interior draw-budget sweep to flags_check.py (decision #2): enter all 9 shop types, assert <=350 draws/room, dig on so it also confirms C1's batching kept ?dig=1 riffling. The sweep caught a real bug in interior_mode.js (F-owned): the interior/dig render never called renderer.info.reset() while the shell runs autoReset=false (for the composer), so renderer.info.render.calls ACCUMULATED every frame — inflating any interior draw read (and the HUD were it shown). Fixed: reset per interior frame before both the dig and the walk render. True per-frame result after the fix: all 9 room types <=350 draws (worst dept 96) — C1 batching (35981ad) verified. flags_check GREEN (regression + interior budget + 4 flag smokes + all-on combo). Co-Authored-By: Claude Opus 4.8 --- F-progress.md | 2 +- tools/flags_check.py | 37 +++++++++++++++++++++++++++++++++++ web/js/world/interior_mode.js | 3 +++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/F-progress.md b/F-progress.md index a20c1f0..a9553a9 100644 --- a/F-progress.md +++ b/F-progress.md @@ -14,7 +14,7 @@ lanes land, adds C's ≤350-draw interior assertion when C1 lands, extends the d |---|---| | **F1 enforcement harness** | ✅ **DONE + wired.** `tools/flags_check.py` (self-contained server, headless, ~90s): **flags-off regression** (golden hash `0x3fa36874` + subsystems-inactive + ≤300/≤200k budget + ±40% snapshot + 0 errors) · **per-flag smokes** (winmap/dig/roster: cross ≥2 chunks + enter interior; dig opens+closes a riffle) · **all-on combo** (flag-composition law, decision #4). Wired into `qa.sh` as a **warn-level gate** (`--no-flags` skips fast; auto-skips without the Playwright venv). Current: **all smokes GREEN**, regression clean. | | **F2 plansrc** | ✅ **DONE + verified.** A landed the full seam (`52eb109`). Wired the shell bootstrap (`index.html`, F-owned): `?plansrc=osm` → `citygen.generatePlanFor(seed,'osm')` → real-data town **"Melbourne", 95 shops** (synthetic default = "Boolarra Heads" unchanged). Pinned **both goldens** in `scaffold_check.mjs`: synthetic `0x3fa36874`, **osm `0x34cfdec0`** (deterministic, 2 runs). Harness auto-detects plansrc + smokes it (enters OSM shop "Mui Karaoke Rooms") + includes it in the all-on combo. Also restored `binUnderAim`'s **nearest-bin fallback** (matches C's test page) so E riffles a nearby bin without perfect aim / before the bin GLB finishes loading. Synthetic golden frozen (selfcheck 1362/1362). | -| **≤350 interior draw assertion** | ⏳ add to harness once C1 (batching) lands (decision #2; book room's 1,245 is the bug). | +| **≤350 interior draw assertion** | ✅ **DONE.** C1 batching landed (`35981ad`). Added an interior draw-budget sweep to `flags_check.py` (enter all 9 shop types, assert ≤350 draws/room, dig on so it also confirms batching kept riffling). **Caught a real bug in my own `interior_mode.js`**: it rendered the interior without `renderer.info.reset()` while `autoReset=false`, so draws *accumulated* per frame (inflated 864/644/…). Fixed (reset per interior frame, both dig + walk render). True per-frame result: **all 9 room types ≤350 (worst dept 96)** — C's batching verified. | | **F3 tag `v2.0-alpha`** | ⏳ all landed flags smoke-green + regression green + qa strict green → tag (3 flags OK if A no-shows). | Base: v1.1 (`3a831fe`), HEAD `40fefd9`. `qa.sh --strict` GREEN. diff --git a/tools/flags_check.py b/tools/flags_check.py index c51b179..6f4bbd1 100644 --- a/tools/flags_check.py +++ b/tools/flags_check.py @@ -185,6 +185,42 @@ def smoke(p, label, query, is_combo=False): finally: b.close() +def interior_budget(p): + """Decision #2: interior draw budget is law — ≤350 draws worst room. Sweep every shop type + (dig on, so this also confirms batching kept ?dig=1 riffling). C enforces via its sweep; F pins it.""" + head('INTERIOR DRAW BUDGET (≤350 draws/room law — decision #2)') + b, pg, errs = new_page(p) + try: + boot(pg, 'dig=1') + pg.evaluate("() => window.DBG.setSegment(2)") # midday → shops open + types = ['record', 'opshop', 'toy', 'book', 'video', 'pawn', 'milkbar', 'dept', 'stall'] + rows, seen = [], set() + for t in types: + picked = pg.evaluate("""(t) => { + const P=window.PROCITY, D=window.DBG; + const s=(P.plan.shops||[]).find(x=>x.type===t && P.isOpen(x)); + if(!s) return null; D.enterShop(s.id); return {id:s.id, name:s.name}; + }""", t) + if not picked: + continue + pg.wait_for_timeout(500) # let async GLB fittings load → settled worst-case draws + info = pg.evaluate("() => { const i=window.DBG.info(); return {draws:i.drawCalls, tris:i.tris, mode:i.mode}; }") + rows.append({'type': t, 'shop': picked['name'], **info}) + pg.evaluate("() => window.DBG.exitShop && window.DBG.exitShop()") + pg.wait_for_timeout(150) + over = [r for r in rows if r['draws'] > 350] + for r in over: + FAIL(f"interior {r['type']} '{r['shop']}': {r['draws']} draws > 350") + if rows and not over: + worst = max(rows, key=lambda r: r['draws']) + OK(f"all {len(rows)} room types ≤350 draws (worst: {worst['type']} {worst['draws']}, tris {worst['tris']})") + elif not rows: + WARN('no interiors sampled (no open shops of the swept types)') + if errs: + FAIL(f"interior sweep: {len(errs)} console error(s); first: {errs[0][:140]}") + finally: + b.close() + def main(): srv = ensure_server() try: @@ -196,6 +232,7 @@ def main(): print("· plansrc=osm not landed (Lane A) — skipping that flag") flags_off_regression(p) + interior_budget(p) for fl in flags: smoke(p, fl.split('=')[0], fl) smoke(p, 'ALL-ON combo', '&'.join(flags), is_combo=True) diff --git a/web/js/world/interior_mode.js b/web/js/world/interior_mode.js index 2fbcc73..c8e3881 100644 --- a/web/js/world/interior_mode.js +++ b/web/js/world/interior_mode.js @@ -153,6 +153,7 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null if (dig && dig.active) { if (dig.update) dig.update(dt); renderer.autoClear = true; + renderer.info.reset(); // autoReset is off (shell owns it for the composer); reset per interior frame renderer.render(dig.scene, dig.camera); return false; } @@ -173,6 +174,8 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null keepers.update(dt); // Lane D: keeper idle + turn-to-greet (playerPos defaults to the camera) renderer.autoClear = true; // the composer may have left autoClear=false; force a clean interior frame + renderer.info.reset(); // autoReset is off (shell owns it for the composer); reset per interior frame so + // renderer.info.render.calls reports THIS frame's draws (HUD + the ≤350 room gate) renderer.render(scene, camera); const de = nearestExitDist(camera.position.x, camera.position.z);