diff --git a/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md b/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md index 0dcd7447..0b9a08de 100644 --- a/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md +++ b/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md @@ -682,7 +682,8 @@ Update immediately when a slice changes state. Chat is not the ledger. | S4-c3b | **LANDED 2026-09-04** — implementation `26e97ba41`, provenance contract `5110bf676`, packet-only correction `3f2f00c9f`; packet §22–§25. Dead classic-group `LocalSortCenters`/`CachedBatch.LocalSortCenter` storage and the alpha camera-parameter/digest chain are deleted. Live per-cell/particle CYpt keys, opaque `SortDistance`, building/private/portal distances, two FIFO lists, all state/barriers, and AP/AD rows remain. | **G3 LEAD SELF-GATE PROVISIONAL PASS; owner acceptance/owner-only rows pending. G4 UNPASSED.** | Retail/deletion lens PASS. Production lens found no code defect and one artifact-provenance omission; packet-only fix round 1 passed its narrow re-review. Fresh campaign: Release 0W/0E; App 132/132; Core 29/29; allocation 2/2 at 0 B; shader 32/32. G3: route 1 13/13, route 2 4/4, route 3 retry 3/3, all exit 0/graceful. The first route-3 attempt stopped before Nanto on registered #462. Exact PNG paths: packet §25. | | S5-c1 | **LANDED 2026-09-04 after the owner-authorized documentation/evidence exception.** Mandatory stop `efb075619` remains in history; final campaign tip `1718832e2`, reviewed scratch tip `b2d56f9e3`. | G4 UNPASSED | Narrow exception re-review PASS. Clean evidence: hermetic 16,760/16,760; inclusive InstalledDat 385 pass / the same documented 10 fail / 1 skip; both exact `d6592d3ac`, `WorktreeDirty=false`, manifests fully verified. Fresh campaign Release 0W/0E, Core 111/111, visibility 38/38, allocation/AP-116 4/4. Lead graphical portal-haze gate PROVISIONAL PASS, exit 0/graceful, 10 PNGs under `logs/selfgate-20260904-171137-s5c1-particle-visibility`; owner inspection pending. | | S5-c2 | **LANDED + REVIEW-CLOSED 2026-09-04; lead graphical gate PROVISIONAL PASS.** Campaign stack `a4de2efc4` -> `048027e71` -> `ef819eedf`; reviewed scratch tip `521f5edda54`; packet §§14–16. The opt-in IA-24 path borrows the exact prior-completed landscape set/flag, selects world casters through S2 CELLARRAY or building `EffectCellId`, selects authored terrain slots, and projects exact active instance runs without rebuilding retained topology. Pack-off remains unchanged. | G4 UNPASSED | Retail/deviation PASS; production's documentation-only fix round 1 narrowly re-reviewed PASS. Fresh campaign Release 0W/0E and affected 137/137. Official evidence: hermetic 16,768/16,768; broader InstalledDat 468 pass / 10 fail / 1 skip; canonical 385 pass / the same 10 fail / 1 skip; manifests and nonpassing identities exact. Valid lead gate `logs/selfgate-20260904-183925-s5c2-candidate-shadow-ab`: seven PNGs, exit 0, graceful logout, no fatal match, no client left. Exact parent `5c106bcdf` A/B at `s5-c2-ab-parent/logs/selfgate-20260904-183815-s5c2-parent-shadow-ab` proves the gate-visible dark lighting switch predates c2; #469/IA-24 carry it for resolution or explicit owner acceptance before G4. Owner PNG inspection pending. | -| S5 | IN FLIGHT — c1 and c2 landed/review-closed/provisionally self-gated; c3–c5, #469 disposition, closeout, and G4 remain | G4 | Packet §§7 and 12–16 are binding; S5-c3 is next; never merge main before G4. | +| S5-c3 | **CONTRACTED 2026-09-04; implementation pending.** Packet §17 is binding. | G4 UNPASSED | Exact building part-0 degrade selection, selected-Gfx complete-body gate, selected shell submission, shared retail FPS/automatic-degrade owner, and the three live Config preferences. Sonnet implements in a detached scratch worktree; lead verification and both sequential reviews precede landing. | +| S5 | IN FLIGHT — c1 and c2 landed/review-closed/provisionally self-gated; c3 contracted; c4–c5, #469 disposition, closeout, and G4 remain | G4 | Packet §§7 and 12–17 are binding; S5-c3 implementation is next; never merge main before G4. | --- diff --git a/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md b/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md index 642aac63..b13d6e38 100644 --- a/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md +++ b/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md @@ -1,7 +1,7 @@ # Campaign OVERHAUL v2 — S5 consumers, material, and closeout packet -**Status:** S5-c1 and S5-c2 LANDED + lead graphical gates PROVISIONAL PASS -2026-09-04; S5-c3 is next. +**Status:** S5-c1 and S5-c2 LANDED + lead graphical gates PROVISIONAL PASS; +S5-c3 CONTRACTED 2026-09-04, implementation pending. **Branch:** `claude/campaign-w-retail-frame-walk`. **Gate:** G4 remains unpassed. Nothing merges to `main` before G4. @@ -947,3 +947,288 @@ silently accepted as a solid-renderer result: issue #469 records it and IA-24 now names the gate-visible discontinuity. It must be resolved or explicitly owner-accepted before G4. S5-c3 is next; G4 remains unpassed and nothing may merge to `main`. + +## 17. S5-c3 contract — exact building degrade selection and complete-body gate + +This chunk is deliberately bounded to the building part-0 path named by S5. +It replaces the capture-specific fixed `+0.99` selection, preserves the +selected GfxObj identity separately from its optional drawing BSP, draws that +selected shell, and makes the three already-authored Config preferences live. +It does not add generic entity LOD, change S5-c1 particle visibility, change +S5-c2 shadow selection, or implement the fixed-function material work reserved +for c4. + +### 17.1 Lead-verified retail and paired-binary facts + +The paired executable is `C:\Users\erikn\Downloads\acclient.exe`, SHA-256 +`006FFEADC5D679C871497112A5BD1F87714D0E273E2166BAE5052DDE369297B1`. +Its CodeView record remains GUID `{9E847E2F-777C-4BD9-886C-22256BB87F32}`, +age 1, matching `refs/acclient.pdb`. Fresh read-only Ghidra output was checked +against the named pseudo-C. Exact function byte ranges are: + +| Function | VA / file offset / length | SHA-256 | +|---|---|---| +| `SceneTool::UpdateFPSCounter` | `0x0043E510` / `0x0003E510` / 176 | `4B49B883F2F03284F08BAF70716F2AFFAA6591EAD4142D8D5A68879F79CA9DA2` | +| `CPhysicsPart::Draw` | `0x0050D7A0` / `0x0010D7A0` / 192 | `965E1E6208DF662EE3466AA455CFF57BBDEA8AA3751E4DE2F5FD30C3842661FA` | +| `CPhysicsPart::UpdateViewerDistance` | `0x0050E030` / `0x0010E030` / 384 | `30F0EEA8829728497E1D00E0D9737DF9EB112CC6643A24ED6150F1350D29D49D` | +| `GfxObjDegradeInfo::get_degrade` | `0x0051E4B0` / `0x0011E4B0` / 400 | `E0FE4A2BC79F108985B84DDBA44460B8B80809AE81088779D02C2FEF6CCDD8B7` | +| `Render::SetDegradeLevelInternal` | `0x0054C3C0` / `0x0014C3C0` / 240 | `9A59895EC901ABA75584EA5B059222C00CC9CF2B7AF54F5FB7C2A2CF444D83E4` | +| `Render::CalcDegLevel` | `0x0054CAF0` / `0x0014CAF0` / 736 | `607DC8B8DBAAB9498C163241CE535E14751378A7EC06DFFD724BB68910C7BB08` | +| `RenderDeviceD3D::DrawBuilding` | `0x0059F2A0` / `0x0019F2A0` / 177 | `0FEC14C65D32DACEA0344A9F347937A2588CE3D14694AC658607BDFE5ACDE125` | + +`RenderDeviceD3D::DrawBuilding @0x0059F2A0` always publishes the building's +outdoor portal list and calls `UpdateViewerDistance(parts[0])`. Its complete +body is gated only by +`parts[0]->gfxobj[parts[0]->deg_level] != nullptr`. Inside that gate retail +sets building/detail state, calls `FlushAlphaList(0f)`, calls +`CPhysicsPart::Draw(part0, 1)` for the portal-only walk, sets the building +flag, calls `CPhysicsPart::Draw(part0, 0)` for the selected shell, clears the +flag, and clears detail state. A non-null selected GfxObj whose `drawing_bsp` +is null therefore still crosses the alpha barrier and draws its shell; only +the portal-only sub-walk naturally emits nothing. The current acdream +`if (bsp is null) return` conflates those facts and is wrong. + +`CPhysicsPart::Draw @0x0050D7A0` normalizes its local level to zero when the +part has no degrade descriptor or its stored level is outside the loaded Gfx +array, then indexes `gfxobj[level]`. A null selected GfxObj returns; a non-null +one reaches `DrawMesh`. `UpdateViewerDistance @0x0050E030` measures from the +viewer to the base GfxObj's sort center after component-wise part scale, stores +CYpt and heading, selects level zero/mode one for a missing ladder or the +player, otherwise calls `get_degrade(CYpt / gfxobj_scale.z)`, and calculates a +draw frame only when the selected GfxObj is non-null. + +`GfxObjDegradeInfo::get_degrade @0x0051E4B0` uses +`max(0, abs(distance) - Render::s_rDegradeDistance)`. With automatic degrades +enabled it uses `Render::deg_mul`; otherwise it uses +`Render::s_rUserSuppliedDegradeBias`. For a nonnegative multiplier the strict +threshold is `ideal - (ideal - max) * multiplier`; for a negative multiplier +it is `ideal + (ideal - min) * multiplier`. The first strict `effective < +threshold` match wins; equality advances; no match selects the last level. +`degrades_disabled` forces level zero and that level's mode; a forced level is +clamped to the last slot. This chunk has no production force-level or global +disable control, but its pure selector must keep those two explicit inputs so +the retail branches are pinned and no future caller must fork the algorithm. + +The binary statics are `s_rDegradeDistance=50`, `max_framerate=20`, +`min_framerate=8`, `ideal_framerate=10`, `auto_update_deg_mul=1`, manual bias +`0`, and initial `deg_mul=0`. The Config UI's authored defaults are separately +automatic=false, bias=0, distance=50; production follows the persisted UI +preference, not a capture's transient multiplier. + +`SceneTool::UpdateFPSCounter @0x0043E510` sums the prior 20 frame-duration +slots, publishes FPS as zero when the sum is not strictly greater than +`0.000199999995f`, otherwise publishes `20/sum`, then shifts the history and +inserts the just-finished duration. Preserve this retail one-sample ordering, +the 20-slot warmup contents, and single-precision behavior. + +`Render::CalcDegLevel @0x0054CAF0` first shifts its 30-slot candidate history. +With automatic degrades off it stores the unchanged current automatic +multiplier and returns. With automatic degrades on it evaluates retail's exact +five-weight piecewise formula from current FPS and the 8/10/20 rates, adds the +current multiplier, clamps to `[-1,+1]`, and calls +`SetDegradeLevelInternal(candidate)` only after all 30 prior history slots are +within the strict retail `abs(slot-candidate) < 0.01` band; it finally stores +the resulting current multiplier. Port this body as one testable pure routine +from the named function and paired bytes. A proportional controller, moving +average substitute, time-based debounce, modern clamp, or reordered history +is not equivalent. + +`Render::SetDegradeLevelInternal @0x0054C3C0` also derives retail object, +particle, static-light, and dynamic-light budgets. S5-c3 changes only the +building consumer of `deg_mul`: TS-15, AP-116, and AP-85 already own the +surviving non-building LOD, particle-range, and light-pool differences and +must be amended in the same implementation commit to name that uncoupled +adaptive-budget fact. Do not perturb those already-landed policies here. + +Finally, `CBuildingObj::makeBuilding @0x006B53A0` calls +`InitPartArrayObject(model, 1)`. That path accepts either a direct GfxObj or a +Setup, but `DrawBuilding` still uses only `parts[0]`. A Setup-backed building +therefore resolves the existing `SetupMesh.Flatten`/Resting placement part +zero and its transform; it never flattens every Setup part into the building +shell draw. The installed-DAT census below contains no Setup building, so this +branch is a synthetic structural pin, not an installed-population claim. + +### 17.2 Installed-DAT population pin + +The lead scanned the installed land/cell/portal DATs through the legal +`DatCollection` reader before this contract. Record and reproduce these exact +facts in an InstalledDat test or an existing InstalledDat census surface: + +- 1,639 landblocks contain 6,979 building instances across 398 distinct + models; +- all 6,979 installed building models are direct GfxObjs; zero are Setup or + another type; +- all 6,979 resolve part-zero GfxObj data; none are missing; +- 6,760 instances have degrade ladders, containing 27,859 level slots across + 350 distinct ladder models; +- every one of those 6,760 ladders has exactly one zero-id slot and it is the + final slot; no nonzero slot is missing and all 21,099 nonzero slots have a + drawing BSP; +- level-count histogram: 2→196, 3→216, 4→4,964, 5→1,341, 6→43; +- there are no duplicate building-anchor groups. + +Thus the complete-body null-selected-Gfx path is real and widespread, while +the non-null-Gfx/null-BSP path requires a synthetic pin even though its retail +behavior is unambiguous. Do not “repair” the authored final zero slots, clamp +to the last nonzero slot, or use BSP presence as a body-existence proxy. + +### 17.3 One typed selection, one shared degrade owner + +Replace BSP-only selection with one value that carries at least the selected +GfxObj id, nullable drawing BSP, degrade level, and degrade mode. The base +direct-Gfx path carries its own GfxObj id even with no ladder. Ladder entries +carry the authored GfxObj id and mode as well as min/ideal/max and the optional +BSP. A selected GfxObj id of zero is the exact complete-body failure. A +nonzero id with null BSP is an admitted complete body with no portal walk. +`HasGeometry`, the fixed `DefaultDegradeMultiplier=0.99`, and the erroneous +default distance 100 are deleted, not retained as fallback truths. + +Create one renderer-lifetime degrade owner, passed/borrowed through normal +composition: no static mutable global, service locator, duplicate UI mirror, +or second ticker. It owns the 20 frame-time slots, 30 candidate slots, current +automatic multiplier, exact rolling FPS, and the current persisted settings +view. It advances exactly once per accepted graphical render callback using +that callback's real delta; world replacement/portal travel does not recreate +or double-tick it. Settings changes are observed on the next frame: + +- automatic=false → building selection and the FPS panel's DEG field use the + persisted manual `GraphicsPerformance` bias; +- automatic=true → they use the exact current automatic multiplier; +- both modes use the persisted `DegradeDistance`; +- the FPS panel's FPS field borrows this same retail rolling-FPS value, not a + second cadence, while its authored show/hide option remains unchanged. + +The controller must define and pin initial/warmup behavior, zero and tiny +deltas, NaN/infinity input, settings toggle transitions, and exact float +ordering. It may fail closed or sanitize only where the retail x87 comparison +would do so identically; any modern safety divergence must receive a register +row in this same commit. No allocation is permitted after warmup. + +### 17.4 Exact building body, portal, and selected-shell execution + +At `DrawBuilding` entry retain the unconditional BLD transcript event and +portal-list publication. Then select part zero once and use that same immutable +selection for every downstream action: + +1. selected GfxObj id zero: return after entry publication; no alpha barrier, + portal walk, look-in draw, or shell; +2. selected id nonzero: emit the existing alpha barrier first; +3. if the selection's BSP is non-null, run the existing pass-1/pass-2 portal + walk through each active view; if null, emit no portal work but continue; +4. submit the shell after the portal walk using exactly the selected GfxObj id. + +Thread the typed selection through `OnBuildingShellTurn` and the production +world/populator/dispatcher seam. Do not mutate the retained +`RenderProjectionRecord`, rebuild retained topology, clone a record per frame, +or silently keep the base shell's `MeshRefs`. Classification must preserve +the retained building's scene/lifecycle identity, root transform, material/ +surface overrides, detail state, effect/anchor membership, and picking facts, +while replacing the part-zero GfxObj with the selected one. Direct-Gfx models +use identity part transform. Setup models use exactly the precomputed part-zero +placement/default-scale transform, and portal projection, sort-center distance, +and shell submission must agree on the same composed part-zero frame. + +The building path submits exactly one selected part-zero mesh. If its render +mesh is not resident, request/load that selected id through the existing +asynchronous mesh seam and draw no shell for that frame; never fall back to +the base GfxObj, block on DAT access, or query DAT from the renderer. Retry on +the following frame through existing residency behavior. Zero retained shell +records during a streaming boundary remains an empty shell turn; multiple +matching retained shell records for one committed building is a fail-loud +ownership error. No new dictionary, cell scan, or linear model search may run +per building per frame. + +### 17.5 Config and deviation truth + +`Render_AutomaticDegrades`, `Render_GraphicsPerformance`, and +`Render_DegradeDistance` become live rows and must no longer receive +`storeOnly:true` or cite AP-198. Update the exact Config dim-set test and all +comments/counts that pin it. AP-198 narrows from its current residual set by +those three rows; do not disturb the already-live landscape-radius and +building-detail controls. Keep AD-78's count and description mechanically +consistent with the actual controller test. + +Narrow TS-15 explicitly to non-building parts. Amend AP-85 to say its fixed +one-list light cap is not driven by `SetDegradeLevelInternal`'s multiplier, +and AP-116 to say the explicit Retail/Extended particle-range choice is not +the adaptive multiplier's object/particle distance budget. Those existing +rows cover every intentionally uncoupled `SetDegradeLevelInternal` consumer; +add no duplicate row. If implementation discovers another surviving +deviation, add or correct its row in the same commit. The active-row total +must change only if a row is actually added or retired; narrowing does not +change it. + +### 17.6 Allowed scope and explicit nonchanges + +Allowed production scope is the existing building walk/factory/registry, +frame-driver world-data/populator/dispatcher classification seam, the one +renderer/frame-root composition seam needed for the shared controller, the +FPS retained-UI binding, `DisplaySettings` comments, and the Config row +dimming flags. One focused App-layer controller file may be added. Tests may +change only for those surfaces, the exact retail math, installed census, +composition identity, residency retry, lifecycle, and allocation. The only +authorized documentation file in the implementation commit is +`docs/architecture/retail-divergence-register.md`. + +Do not change portal admission, BSP traversal, alpha FIFO routing, material +blend/state, particle update/range, point-light selection, terrain/shadow +selection, streaming radii, generic entity/scenery/creature LOD, DAT reader, +RHI/shaders, login/network/runtime gameplay, self-gate scripts, artifacts, or +this packet/plan. Do not add a base-shell fallback. No graphical client is +launched by the implementer. + +### 17.7 Required automated proof, mutations, and return + +Focused tests must prove at minimum: + +1. direct base Gfx and ladder selections carry exact id/BSP/level/mode; +2. positive and negative multiplier arms, strict threshold equality, last + slot, disable, forced-level clamp, scaled sort-center/CYpt, zero/negative/ + NaN/infinity inputs match a literal independent retail oracle; +3. the prior-20 FPS ordering and the complete 30-slot `CalcDegLevel` formula, + stability gate, clamp, manual arm, warmup, and settings transitions match + fixed expected sequences—not implementation self-comparison; +4. zero selected Gfx emits BLD only; nonzero/null-BSP emits BLD → alpha + barrier → selected shell with no portal events; nonzero/BSP preserves BLD + → barrier → portal passes/look-ins → selected shell; +5. a far selected LOD changes the submitted GfxObj id while all retained + ownership, transform, surface/detail, effect-cell, picking, and ordering + facts remain exact; no base fallback occurs while the selected mesh is + unavailable, and the same selection appears after residency succeeds; +6. a synthetic Setup building draws only transformed part zero and uses the + identical composed transform for distance, portal clip, and shell; +7. Config's three rows are live-colored, persist exact values, and the one + composed controller instance is borrowed by both renderer and FPS panel; +8. publish/retire/revisit/reset and first/login/portal/null-root frames neither + duplicate nor recreate the controller and leave existing building + lifecycle behavior intact; +9. the installed census equals §17.2 exactly; and +10. the warmed selection/controller/classification path allocates 0 managed + bytes and does not advance retained topology or materialize records. + +Sabotage and restore at least these exact mutations, recording each first +failing test/assertion in the implementation commit body: use BSP-null as the +complete-body gate; clamp the final zero slot to the prior nonzero slot; use +the base GfxObj for shell submission; change strict `<` to `<=`; feed the +manual bias while automatic is enabled; update FPS after inserting the current +sample; allow one stable history slot instead of all 30; fall back to base when +the selected mesh is unavailable; and recreate the shared owner for the FPS +binding. Include an allocation sabotage that constructs or clones per +building and prove the 0-B pin catches it. + +Implementer return: `git diff --check`; Release solution build 0W/0E; focused +building/walk/driver/dispatcher/settings/UI/composition tests; exact math and +0-B lanes; official hermetic lane; inclusive InstalledDat lane with the +campaign's accepted exclusions and explicit documented global identities; +one clean commit; exact file/count/mutation report; no client run. + +Sequential review 1 — retail/deviation fidelity: re-check every §17.1 byte/ +named fact, the §17.2 census, selection math, FPS/auto controller, complete- +body gate/order, Setup part zero, selected shell, Config/register truth, and +all mutations. Sequential review 2 — architecture/production/gate honesty: +one shared owner/tick, one typed selection, no retained mutation/base fallback/ +renderer DAT read/topology rebuild, async residency, transform/material/ +lifecycle identity, allocation, allowed scope, build/lane provenance, and at +least three reproduced sabotage claims. Reviews run sequentially. A failed +lens gets one bounded fix contract; a chunk needing a third fix round stops +and is written up.