docs(overhaul): contract S5 c3 building degrades

This commit is contained in:
Erik 2026-09-04 19:08:39 +02:00
parent c3ac20b2ad
commit 185590e016
2 changed files with 289 additions and 3 deletions

View file

@ -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 §§1416. 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; c3c5, #469 disposition, closeout, and G4 remain | G4 | Packet §§7 and 1216 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; c4c5, #469 disposition, closeout, and G4 remain | G4 | Packet §§7 and 1217 are binding; S5-c3 implementation is next; never merge main before G4. |
---

View file

@ -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.