acdream/docs/research/2026-08-08-347-fix-contract.md
Erik fa0c053ebf
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
docs(physics): #347 closed WITHOUT a code change — retail's glide alternates exactly as ours does; AD-70 retired as a wrong inference
The round-2 cdb capture is decisive: during a live retail glide,
edge_slide fired ~1.5 times per find_transitional_position — the
arm/move alternation's exact signature (3 entries on the arming tick,
0 on the moving tick) — with cliff_slide in lockstep, step_down at
2.5x, step_up 0, and every stack sample on our identical call path.
cliff_slide's bytes match our port and ACE's (compare constant at
0x794610 verified 0.0), and the user could not distinguish the two
clients side by side. The "retail redirects within the tick" premise
misread round-1's set_sliding_normal cadence (per-event, not
per-tick, so its 1:1 ratio with edge never discriminated anything).

The alternation-tolerant assertion in Issue345SteepSlopeGlideTests is
therefore the CORRECT retail-shape pin from both sides; its comment
now cites the capture instead of calling the shape a residual. The
#269 note is honest the other way: the hope that a within-tick port
would explain that feel residual is withdrawn with the premise.

The temporary Scratch347 diagnostic is deleted. Capture evidence:
345-glide-stacks.cdb.log (repo root, untracked, cited from the
contract's RESOLUTION section).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:05:53 +02:00

182 lines
9.8 KiB
Markdown

# #347 fix contract — retail's within-tick slide continuation (full-rate glide)
**Date:** 2026-08-08. **Implementer: Fable directly (user direction), dual
Opus review, then the user's slope feel gate.** Predecessor: #345 landed at
`ab89ebdf` (validate_walkable return scoping); this contract closes AD-70.
## The defect (measured, not hypothesized)
Post-#345, the glide alternates in a strict two-tick cycle (scratch trace,
2026-08-08, 45-degree diagonal fixture):
- arming tick: ZERO XY yield, sliding normal becomes (0.707,-0.707,0),
transient gains Sliding;
- moving tick: +0.115/+0.115 (the along-crease component), sliding normal
CLEARS;
- repeat. 14 of 30 ticks stuck; lateral rate is HALF the input's lateral
component.
Retail delivers motion EVERY tick: cdb counters (`345-retail-glide.cdb.log`)
show edge_slide/cliff_slide 594 each over one ~15 s glide (~per 30 Hz tick),
set_sliding_normal 538, step_up 0.
## Retail's mechanism (pinned from pc this session)
`CTransition::edge_slide` @0x0050b3d0 (pc:273001+), steep-contact branch
(contact valid AND N.z < allowance @0050b3f7-0050b441):
1. `sphere_path.walkable = null; restore_check_pos()` back to the SAVED
(advanced, pre-step-down) candidate;
2. `*outState = cliff_slide(&contact_plane)` see below;
3. clears contact plane validity/water;
4. **returns 0 (FALSE = do not stop)** `transitional_insert`'s attempt
loop CONTINUES and retries at the position cliff_slide just produced.
`CTransition::cliff_slide` @0x0050a6d0 (pc:272397+):
1. crease = cross(steepN, **last_known_contact_plane.N**) NOT the current
contact; horizontal-projected (Z forced 0 via *0f arithmetic in BN's
rendering) and 90-degree-rotated: vector (-crossY, crossX, 0) = the
horizontal PERPENDICULAR to the crease;
2. `normalize_check_small` degenerate -> return 1 (OK_TS);
3. proj = dot(check-minus-curr displacement + LandDefs::get_block_offset
(curr cell vs check cell — the AD-69 seam family), perpVector);
4. sign-dependent arm: adds `perpVector * (+-proj)` to the CHECK POSITION
(add_offset_to_check_pos @0050a804/0050a857) — REMOVING the into-face
perpendicular component so the check pos keeps only the along-crease
part — and sets the collision normal (one arm negates the normal:
0050a813-0050a827);
5. **returns 3 (ADJUSTED)**.
Net: the SAME transitional_insert call retries at the slid position, the
insert validates clean, step-down lands on the flat side, OK — motion
delivered within the tick, every tick. The sliding normal is set as well,
so next tick's AdjustOffset pre-projection ALSO applies (that projection
producing an already-clean request is why retail still fires edge_slide
per tick: the request keeps pressing into the face).
## Our port today (`TransitionTypes.cs`)
- `TransitionalInsert` (:2306-2335) already continues the loop when
`EdgeSlideAfterStepDownFailed` returns false — the LOOP shape is ported.
- `EdgeSlideAfterStepDownFailed` branch2 (:2534-2544) matches retail's
steep-contact branch: restore, CliffSlide, clear, return false.
- `CliffSlide` (:2630-2672) computes the same perpVector and even calls
`sp.AddOffsetToCheckPos` with a sign-dependent arm and returns Adjusted.
**Yet the arming tick yields zero.** So the divergence is INSIDE this
chain, not its shape. Candidates, in test order:
1. **Sign-arm inversion (BN flag-test ambiguity).** Ours adds
`collideNormal*angle` when angle<=0 and `collideNormal*(-angle)` when
angle>0 — BOTH are non-positive multiples. Removing the perpendicular
component requires the CANCELLING sign in both arms: for angle>0
subtract (ours does), for angle<0 ADD the positive multiple (ours
subtracts more DOUBLING the into-face component instead of
cancelling). Retail's two arms at 0050a7d6/0050a849 vs 0050a7f6 are
sign-mushed in BN byte-decode 0050a7a0-0050a870 (fchs placement)
before concluding. If ours doubles the into-face component on one arm,
the slid retry re-collides HARDER -> retries exhaust -> zero yield ->
exactly the alternation (next tick's pre-projection is what moves).
2. **Retry-after-CliffSlide dies.** The Adjusted continue (:2323-2327)
re-enters InsertIntoCell; if the slid check pos still validates against
the steep triangle (e.g. because the offset was wrong per candidate 1,
or because the retry re-runs the PRIMARY phase from the pre-advance
position rather than the slid candidate), attempts exhaust. The
transit-fail probe's buffered [transit-fail-insert] lines (19 per stuck
tick in the scratch run) name each attempt's phase outcomes — READ THEM
FIRST; they may settle candidates 1 and 2 in one look.
3. **restore_check_pos ordering.** Retail restores BEFORE cliff_slide and
cliff_slide then adds its offset to the RESTORED (advanced) candidate.
Ours: branch2 restores then CliffSlide adds to... verify which position
RestoreCheckPos leaves in GlobalSphere (SaveCheckPos at :2265 saved the
ADVANCED candidate, so restore should equal retail). Confirm with the
probe's per-attempt positions.
## D1 — the minimal port
Fix ONLY what the instrumentation names (expected: the CliffSlide sign
arm(s), possibly one retry-entry position). Do NOT touch: the loop shape,
branch1/3/4 responses, PrecipiceSlide, the #331 sliding-normal
persistence/clearing, AdjustOffset (AD-66!), ValidateWalkable (#345 just
landed), DoStepDown internals.
## D2 — tests
1. Tighten `Issue345SteepSlopeGlideTests.Angled45Approach_GlidesAlongTheDiagonal`:
full-rate — stuckTicks small (crossing transient only, e.g. <= 3) and
lateral advance ~= the input's lateral component times the post-crossing
tick count (assert >= 85% of it, ordering-safe), replacing the
alternation-tolerant `Ticks/2 + 2` bound and its comment (and drop the
#347-residual comment).
2. The 30/60-degree ordering pin and perpendicular stop stay green
unchanged.
3. AD-65/AD-66 conformance, the #331 absorb pin, RetailEdgeResponseOrdering,
Issue265, TransitFailProbe, EdgeSlideBackProbePrecipiceSlideTests, and
the #271 staircase guard ALL stay green untouched.
4. Sabotage: revert the sign/continuation fix -> the tightened full-rate
assertion reds with the alternation numbers; restore.
## Acceptance
Clean-room complete suite; dual Opus review (conformance byte-check of
cliff_slide's fchs arms + blast radius over every CliffSlide caller);
retire AD-70 in the same commit; user slope feel gate (~2 min: glide speed
now matches retail side-by-side; downhill/uphill/hover unchanged).
## PREMISE REVISION (2026-08-08, same session — before any code change)
Three findings force the "retail = full-rate within-tick" premise back to
OPEN:
1. **cliff_slide's arms are conformant in all three sources.** ACE
(`Transition.cs:242-266`), our port, and the byte decode agree on the
crease vector, the sign arms, and the compare-vs-0.0 (double at
0x794610, verified zero). For the conformance fixture's geometry those
arms move the check position INTO the face (the arming tick's probe
trace shows primary dist -0.056 -> -0.200 -> -0.487 across the three
attempts) — the "dig" is what the code as written does, in retail's
bytes too.
2. **The round-1 counters refute a per-tick retry storm in retail.**
slidn:edge = 538:594 ~ 1:1.1. Our alternation shape produces THREE edge
entries per sliding-normal set (the dig-retries); retail produces ~ONE.
The glide window in the counter progression (edge 0 -> 594 across six
1280-vwalk blocks, then frozen) also shows edge growth stopping the
moment the hold ended.
3. **The user's side-by-side observation** ("I cant detect any speed
change from retail", #345 gate, 2026-08-08) is consistent with retail
ALTERNATING exactly as we do — arm one tick, move the next — and
inconsistent with retail moving at double our rate.
Competing hypotheses now:
- **H-A (retail alternates too):** retail's arming tick runs ONE edge
entry (no retry after the cliff-slide Adjusted) and yields nothing; the
next tick's pre-projection moves. Then our ONLY divergence is the two
extra futile dig-retries per arming tick (invisible — the tick's output
is discarded), #347 closes as measured-identical, and AD-70 is RETIRED
as a wrong inference, not fixed.
- **H-B (retail yields within the tick):** the single edge entry precedes
a successful same-tick commit. Requires the retried/continued insert to
land clean — mechanism unknown given the dig direction — and would keep
AD-70 open as written.
**The discriminator is one number in the round-2 capture:**
`CTransition::find_transitional_position` (~1/tick for the player) vs
`edge_slide` during the glide window. H-A predicts ftp:edge ~ 2:1
(edge every other tick); H-B predicts ~ 1:1 (edge every tick). The
round-2 script (`tools/cdb/345-glide-stacks.cdb`) now counts ftp, prints
the periodic progression, samples 6 stacks each for edge/cliff/step_down,
and auto-detaches at 300 edge hits via the fall-through-then-top-level-qd
recipe. NO code changes until this capture runs.
## RESOLUTION (2026-08-08, round-2 capture): H-A confirmed in its strong form — CLOSED, no code change
Round-2 (`345-glide-stacks.cdb.log`, auto-detached at 768 edge hits):
glide-window steady state per 1280-vwalk block: edge +133..158, ftp
+93..105, stepdown +351..376, cliff lockstep with edge, stepup 0. edge/ftp
~ 1.45 with ftp INCLUDING background movers — the player's true ratio is
~1.5, which is precisely the alternation's signature (3 arming-tick
entries, 0 moving-tick entries, averaged). All six edge_slide stack
samples: transitional_insert -> find_transitional_position ->
CPhysicsObj::transition -> UpdateObjectInternal — our exact path. Retail
performs the same dig-retries and the same alternate-tick yield. #347
closed; AD-70 retired; the D2 "tighten to full-rate" plan is CANCELLED —
the existing alternation-tolerant assertion is the correct retail pin.
The H-B mechanism does not exist in the binary's behavior.