Skip to content

M1.1.12 — Forge 3D: the kinematic character controller - #68

Open
guysenpai wants to merge 55 commits into
mainfrom
phase-1/forge/character-controller
Open

M1.1.12 — Forge 3D: the kinematic character controller#68
guysenpai wants to merge 55 commits into
mainfrom
phase-1/forge/character-controller

Conversation

@guysenpai

@guysenpai guysenpai commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

M1.1.12 — Forge 3D: the kinematic character controller

Fourteenth M1.1 sub-milestone, and the first that is neither a shape nor a solver pass. Seven gates,
each pushed and reviewed before the next opened. Base: main at a4354df (v0.11.11-mesh-shape).

Planned tag v0.11.12-character-controller. Not merged and not tagged here — both are Guy's.

What it is

A virtual character controller carrying no simulated body: it does not participate in the solve,
is not integrated, and is not tracked by the island partition. Settled on the reference, whose
CharacterVirtual has no rigid body.

But it does carry a broadphase presence — an inner body, a .kinematic body holding the
controller's own capsule, mandatory on the module and defaulting to on per character. The argument
is internal to the frozen surface: CharacterDescriptor has carried collision_layer since its
original version, and in Weld's query family the object layer is how an object declares itself
visible to other callers' queries (§1.11.5) — so either the character has a presence, or that field
has no observable effect. That inverts the reference's default, because the failure mode of
default-off is a character nobody can query, found late.

  • moveCharacter — sweep, slide along planes and creases, climb, stick to the floor on descent,
    depenetrate, push what it touches, and wake it
  • resizeCharacter — atomic, feet-anchored, keeps the presence BodyId, three outcomes
  • setCharacterPosition — teleport without resolving, verdict invalidated to .in_air
  • getCharacterInnerBody / destroyCharacter / the ground verdict
  • collideShapeBody — the seventh body-level adapter, the manifold fallback the sweep needs when
    its normal is unusable

Decisions worth reading before the diff

Position is the BASE of the capsule, never the centre of its shape, and the offset lives in
exactly one named place. The reference parameterises that anchor through mShapeOffset; Weld fixes
it. The base/centre distinction was the source of three separate defects in this milestone.

Ground determination is a bounded downward sweep, not manifolds at the current pose.
collideOrdered returns null on a separated pair and a resting character stands padding above its
floor, so a manifold-only reading answers .in_air for a standing character.

The slide is constrained by slope, and §1.12 did not say so: measured, a character climbed any
face up to 90°−ε by walking into it — 0.583 m of rise in one call against a 50° face under a 45°
limit, with the verdict correctly reporting .on_steep_ground throughout. The rule caps the
projected motion's up component at max(up_before, 0) and not at up_before; capping at
up_before drives into the plane on an inclined face and annuls the physically correct answer, a
body sliding down a 50° slope descending more slowly than in free fall.

Depenetration pushes out and never through. It reverts to the entry pose the moment it finds a
contact whose plane the base has crossed since entry. Without it, the exit side of an unresolvable
squeeze was the parity of max_depenetration_iterations: at 3 and 5 the base landed 0.800000 below
the ground plane, and nothing in the suite moved at any value — an odd count would have shipped in
silence.

PackedId.dead reserves the all-ones no-handle bit pattern. ground_body's default of 0 was a
live handle to slot 0 generation 0, so no bit pattern of that field meant absence — a coupling the C
ABI cannot express.

The discriminant for an error channel is whether an entry returns a value, not whether it writes.
setCharacterPosition was made fallible mid-milestone and reverted on that rule.

It ships one known, measured, pinned, UNFIXED functional defect

A character whose base is placed exactly tangent to a surface serves no horizontal motion —
permanently, and silently. At exact tangency the sweep reports a contact at distance zero,
paddedAdvance returns max(0, 0 − padding) = 0, and the slide returns a horizontal motion projected
on a horizontal plane unchanged; all four slide iterations are consumed and the remainder is dropped.

Recorded here rather than left for a reader to find. Three reasons it ships, and none is "it is small":

  • Not reachable from play — measured, every move leaves the character padding above its floor, so
    the state does not reproduce itself. Only authoring reaches it, position = (0,0,0) over a floor at
    y = 0 being the natural thing to write.
  • Pinned at seven heights in both directions — only exactly zero fails; 0.005 through 0.05 serve
    in full. The fix is testable in advance and the regression is guarded.
  • The deferral forces no refactoringsweepNearest is private to character.zig, no published
    surface sees it, its five call sites are in that one file.

Mitigated by a documented precondition on CharacterDescriptor.position. Two candidate fixes and two
already-refuted forms are recorded in CLAUDE.md, owned by the next milestone that opens
character.zig. The refuted ones were closed by measurement, not argument: not counting the iteration
leaves a bit-identical state, hence an infinite loop; serving the remainder tunnels, measured through a
wall whose face stands at 0.8, since the sweep reports only the plane at distance zero.

Two rounds of external review after the internal gates closed

Seven findings, every one verified against source before being acted on, every one confirmed. The push
applied before a publication that can still fail. The broadphase proxy outliving the character, whose
own comment counted three released resources where there are four. ground_velocity fed the
penetration midpoint instead of the body's surface, on a path no test reached. step_height
unvalidated where every other stored physical parameter is. setShape maintaining two of its four
consequences. The union-published proxy box, wrong twice — permanently fat after a teleport, and
guarding a failure mode that does not exist since Broadphase.update already reserves before mutating.
And, in the second round, the deferral of the push having doubled the force ceiling: a ceiling one can
exceed by being touched twice is not a ceiling, closed by summing per body and capping the sum once.

Seven rounds of external review, and the last four were spent on the wrong level

The tangency defect — a character whose base sits exactly on its floor serving no motion — was closed
four times and reappeared four times, because each fix closed one ARRIVAL PATH and arrival at tangency is
a float-resolution phenomenon with as many paths as one likes. What is finite is the CONSEQUENCE, in one
place: a zero advance against a surface that does not oppose the travel direction obstructs nothing, yet
burns the slide budget and drops the remainder. That contact is now set aside for one retry that does not
spend the budget — 48 lines, no epsilon, bounded at one slot.

The round-2 estimate that this needed threading through five call sites was made without reading
SweepCollector, which already carried an exclusion. Same defect on both sides of the review: prescribing
from an imagined mechanism instead of a read one.

What it ships open, declared rather than discovered

An f32 collider-extent envelope, with its real cause and not "large collider": at a 1 km box
slideNormal returns null and the slide takes its documented stop rather than guess a direction exit.
Serving it would mean inventing a direction the narrowphase declines to supply. Measured boundary — a
scale, not a distance, since gjk.zig's coordScale is relative: 100 m half-extent serves every call,
300 m stalls on the third, 500 m and above from the second, and there is no stall at 50 km from the
origin. f64 is clean to 2 km. It needs a spec addition, not a code change: §1.11.4 bis characterises the
far field by distance alone and carries no extent axis.

Eight green assertions proved nothing, and the eighth exposed a blind spot

Seven were found by the probe table — disable a mechanism, require a test to fail. The eighth was found
by external review, and could not have been found by the table: it exercised no mechanism at all. The
counter-measure is a MUTATION probe — restore the previous implementation and require the test to fail —
which is how the scoped stand-off is now pinned, @max breaking a test where it previously broke none.

A domain table, and an exigence for the next descriptor

Every descriptor parameter at its legal bounds, measured and asserted. No gate of this milestone
enumerated those bounds and asked what the code does at each: gate F decided which values to REJECT and
never asked the other question, and both ends of padding fell into that hole. Cheap at the gate that
declares the fields, expensive afterwards.

Nine of this milestone's findings were in the measuring apparatus

Three tests that asserted nothing at gate E alone. A proxy-freshness test that passed with the proxy
update removed — a broadphase box is only a conservative filter, so a stale fat box the ray still
crosses yields the correct distance and the query is right for the wrong reason. A bit-exact
comparison passing the same literal at two precisions, hence comparing two different inputs. A
tie-break test whose insertion order made both rules agree. A closed form derived at 1.95 and
measured at 0.688. And three harness defects of one class: an exit code taken from a trailing
echo, a regex broken by an apostrophe, four probes reporting failure on compile errors.

Every one was found by disabling the mechanism, never by rereading the test. The standing
formulation: an assertion that exercises a path does not thereby test the mechanism that path uses.

Closure measurements

The eleven inherited envelope quantities, re-measured against main through a worktree with the same
probe compiled in both trees, both precisions: zero movement, digit for digit, on all eleven. All
seventeen inherited forge test files are byte-identical to the tag, solver_test.zig included.

The NGS energy-injection watch CLAUDE.md names this milestone to re-read is answered by
measurement: the retained speed is 5.0000005 at f32 — exactly one ULP above 5 — and exactly 5 at
f64. A solver that adds energy adds it at both precisions; one that rounds does not.

Bench reported, not gated, interleaved across eight reps (ReleaseFast): plane 212.0 ns, stairs
2235.5, wall 1764.5, mesh floor 7979.0, resize 203.0 ns — the worst still leaving 2089 calls per
16.67 ms frame.

Validation

Corner Result
test-forge-3d, Debug, f32 / f64 484/484 ×2
test-forge-3d, ReleaseSafe, f32 / f64 484/484 ×2
zig build test, Debug / ReleaseSafe 264/264 steps, 1656/1673 (17 skipped) ×2

zig fmt --check src/ bench/ tests/ clean; tree-wide zig build lint exit 0. Zero French in code or
brief prose, audited in Python (a byte-wise grep class silently matches nothing here).

Ten consignations, each with its owner, in the brief's closing notes — four of them added to
CLAUDE.md's open decisions.

🤖 Generated with Claude Code

The slide's projection is capped on `up` at its pre-projection component whenever the contact
plane fails the slope test (§1.12.6), on the plane branch AND on the crease branch. Without it a
character climbed any face up to 90°−ε by walking into it: 0.583 m of rise in one call against a
50° face under a 45° limit, with the verdict correctly reporting `.on_steep_ground` throughout.
The crease needed the same cap, measured at 0.0186 m per call through a rotated box offering two
normals, and the probe table shows the two branches are not redundant.

`resizeCharacter` is atomic and feet-anchored: build, test the target volume with the character's
own presence excluded, then commit. A refusal changes nothing. The presence's shape is swapped
before the proxy sync so the broadphase box reflects the new size, not only the new centre.

`pushBody` is unilateral and force-limited; `setCharacterPosition` teleports without resolving and
invalidates the reported verdict to `.in_air`. Its stale-handle answer is a typed error like every
other entry of the store: a destroy is idempotent, so ignoring a dead handle is its natural
answer, but a teleport that goes nowhere is a write the caller has to know did not happen.
Ten blocks. The slope constraint in four cases, one of which carries a correction: the rule's case 3
does not hold as stated for a steep but SLOPED face — a pure gravity step against a 50° normal
projects to an up component GREATER than the one it came from, so the cap fires and restores the
requested rate. The prediction that a descent is already below and hence untouched holds only for a
VERTICAL face. Computed before the test was written.

The unguarded squeeze mode is measured rather than guarded: a capsule needing 1.8 m under a ceiling
offering 1.0 m is PINNED at base exactly 0, bit-identical on the second call, and never driven
through the floor. That direction is safe only because `max_depenetration_iterations` is EVEN — at 3
and at 5 the base lands 0.8 m below the ground plane, and no test would have caught it — so the
parity is pinned with its measurement.

The step's FORWARD sweep now pins its padding stand-off at 1.93. The counterfactual was derived as a
lost stand-off and the measurement refuted that: dropping it gives 0.688, because the unpadded
advance leaves the capsule flush against the wall, the landing sweep then reports that wall at
distance zero instead of the plateau below, and the whole climb is refused. The cost is not 0.02 m of
stand-off but 1.24 m of a legitimate move never served. The LIFT's padding is unobservable and that
is confirmed by probe, not argued: it cancels against the landing drop in every geometry.
`syncPresence` updated the broadphase proxy AFTER the record had been written, on all three write
paths. `Broadphase.update` reserves a slot on its layer's moved log, so it allocates and can fail:
on the two pose paths the record and the body had moved while the proxy had not, so the stored box
no longer contained the body; in `resizeCharacter` the record and the presence body already pointed
at the new shape, which the `errdefer` then destroyed.

The entry now takes its TARGET instead of reading the record, so the one fallible call precedes
every mutation, and publishes the UNION of the old and new boxes so a failure leaves a proxy that
still contains the body. The commit that follows is infallible. Reserve-then-mutate, M1.1.1-HF1
D3/D4.

The resize's other fallible step is interceptable and a FailingAllocator test pins the full claim
there. The `bp.update` reservation could NOT be made to fail from a test — measured: a 5 m move on a
fresh character reports zero allocations seen, 40 teleports of 5 m report exactly one, and the
reversed ordering breaks nothing at exit 0. So the ordering rests on a structural argument, and the
use-after-free it forecloses is recorded as a latent hazard rather than claimed as demonstrated.
Three corrections after the gate F verdict.

The slope cap's bound was the pre-projection `up` component and had to be `max(before, 0)`. Capping
at `before` PENETRATES the plane on a sloped face: on a 50° normal a pure gravity step projects to an
up component of −0.5866, which exceeds the −1 it came from, so the cap bit and the output had a dot of
−0.2657 with the normal. And the projection was the physically right answer — a body sliding down a
50° slope descends more slowly than in free fall. Measured at contact, the old bound removed the
motion entirely: dy = 0.00000, the character frozen on the cliff. Same correction on the crease
branch, where no scene of five separated the two bounds, recorded as undiscriminated rather than
proven.

`depenetrate` carries an invariant instead of relying on its iteration count's parity: it never moves
the character to the far side of a contact plane it was on the good side of at the entry of the call.
The base and not the centre, because the centre of a 1.8 m capsule is still above a plane its feet
passed 0.80 m below. Failure direction, now sayable: an unresolvable squeeze keeps its entry pose with
a residual overlap and does not tunnel — verified identical at counts of 3, 4, 5 and 8.

`setCharacterPosition` returns to a no-op on a dead handle. The discriminant is whether the entry
RETURNS A VALUE, not writing versus destroying: the four entries that return something have no honest
answer for a dead handle, while this one and `destroyCharacter` return nothing. The setter-fallibility
convention goes to M1.1.15, where it covers the whole surface at once.
Two blocks, each the direction whose absence let a defect ship.

Gravity on a steep face: a 60° face so the trigonometry closes, the capsule's surface starting 0.12 m
clear, a pure 0.5 m downward request. The padding comes off the TRAVEL and not off the normal — the
first derivation here got that wrong, the same class of error already corrected at gate D — so contact
is at 0.24, the padded advance 0.22, the remaining 0.28 projects, and dy = −0.43 with
dx = −0.28·(√3/4). Both to six digits. Discriminating: capping at `before` gives −0.281645 and
−0.026694, and at exact contact on a 50° face it gives no motion at all.

A doorway narrower than the character: three widths against a 0.60 m capsule, the resolved offset
exactly `radius − width/2`, always inside the doorway, base unmoved and the request unserved. NOT a
second instance of the ceiling's tunnelling and that was measured — the walls are symmetric about the
entry pose so the alternation stays bounded, identical at counts of 3, 4 and 5 because it is per call
and not per iteration.

The squeeze test now expects the entry base of 0.02 rather than a flush 0, which is the invariant's
stated failure direction, and the parity assertion is demoted to a record that carries no correction.
Five rows, one per path the controller has: `moveCharacter` on a flat half-space, on a flight of
stairs, against a wall, on a tessellated triangle mesh, and `resizeCharacter`. Reported, not
gated — no baseline for this path has ever been measured, and registering a bound before
measuring it is the failure mode recorded at M1.1.8.

Runs are INTERLEAVED: every rep runs all five modes in sequence and the best rep per mode is
kept, so a thermal ramp lands on all five rather than on whichever was being measured while it
passed. Best-of-N per mode cannot resolve a gap under about 5 %.

The resize row carries accepted/refused counters, and they earned their place twice. On a bare
half-space the occupancy query traverses nothing and the row read 40.5 ns — a number whose name
promised what a resize costs and whose value was what it costs against an empty tree. Sharing the
stairs' scenery then made every resize REFUSED, because the first riser overlaps the capsule, so
the row timed a refusal under the name of a success. Both were caught by the counters, not by
re-reading the code. What ships is dedicated scenery: one box whose tight box starts 0.05 m past
the capsule's surface, so the 0.1 m fat margin makes it a candidate the narrowphase must actually
test and reject, plus three further out for tree depth.

ReleaseFast, 2000 calls x 8 reps: plane 212.0 ns, stairs 2235.5, wall 1764.5, mesh floor 7979.0,
resize 203.0 — the worst still leaving 2089 calls per 16.67 ms frame. Leak check proven in both
directions: with `safety` forced true a deliberate 4 KiB leak prints `memory address ... leaked`,
and with the default the same leak prints nothing.
P1-1 the push survived a publication that can still fail. `pushBody` mutated inside the slide loop
while `syncPresenceTo` can fail after it, so an error returned with the record intact left bodies
already pushed and a retry pushed twice. The pushes are accumulated in `PendingPushes` — bounded by
`max_touched`, since a push targets only a body the move touched — and applied after the publication,
beside the wake loop that is post-publication for the same reason.

P1-2 the union fattened the proxy permanently and guarded nothing. `Bvh.update` returns without
refitting once its stored fat box contains the new tight one, so a teleport's leaf covered the whole
trajectory and no later call shrank it. And the motive falls too: `Broadphase.update` reserves its
moved-log slot before touching the node, so that entry is already atomic. `new_box` alone is
published and the paragraph justifying the union is deleted, not amended.

P1-3 the proxy outlived the character. `destroyCharacter` released three resources where there are
four, a count its own comment stated and the code contradicted. It gains a `*Broadphase` and calls
the infallible `Broadphase.remove`, so its return type is unchanged.

P2-1 `ground_velocity` was read at the penetration MIDPOINT instead of the body's surface, short by
half the penetration. Corrected by the same reconstruction `prepare` performs on the same field.

P2-2 `step_height` was never validated, the one stored physical parameter without a guard, though it
is consumed as a sweep distance. Now finite and non-negative; zero stays the disabler.

P2-3 `setShape` accepted any non-dynamic shape swap while maintaining two of its four consequences —
a mesh's cached world AABB and a half-space's absence from the trees. Restricted to convexes, which
makes both unreachable rather than handled.
Seven blocks. The accumulated push against a closed form, with the expectation PER-PRECISION and the
reason on record: the number of contacts against one body is two at f32 and one at f64, the second
existing only because an f32 residue leaves a sliver of `remaining` behind. Asserting 20 at both
precisions was pinning float noise, and it is what made the f64 matrix legs red on the first attempt.

A publication that fails leaves every body velocity untouched, injected with BOTH `fail_index` and
`resize_fail_index` — the second is what gate F was missing when it recorded this failure as
un-injectable, a list growing through `remap` before falling back to `alloc`. The measured limit is
stated at the assertion: the growth lands on the 34th append of a pose-write sequence, and five
constructions failed to land it on a call that also pushes, because a move that pushes barely advances
and so never refits its proxy.

A teleport leaves no permanently fat leaf, asserted by a candidate collector at the trajectory's
midpoint with a non-vacuity check at the new pose. Destroying a character removes its proxy, asserted
on REACHABILITY and not on a node count — a leaked leaf keeps its box and every query over that region
keeps visiting it. `ground_velocity` at the body's surface, on a platform spinning about +Z because
about +Y the point's height cancels out of the cross product and the fix would be unobservable, with
the character in frank interpenetration because the existing rotating-platform case goes through the
sweep. `step_height`'s domain, with nothing left behind by its three refusals.

And the seventh: a base of EXACTLY zero serves no horizontal motion at all, measured at six starting
heights so the trigger is known to be exact tangency and not "below padding". Pinned at its measured
behaviour, not fixed — a closure gate adds no behaviour line.
RD-4 and RD-5 for the two signature changes. The six findings with their origin — four in code that
six gates of internal review had passed, two omissions in the brief's own instructions — plus the
seventh found while building a probe.

`CLAUDE.md` is restored to its state at the base commit and the brief's status returns to ACTIVE:
both closure artifacts wait until the six fixes are pushed and verified. The corrected text for the
Tags row is recorded in the brief so it is not lost in the interval.
`PendingPushes.add` appended without coalescing and `apply` called `addImpulse` once per entry, each
capped at `max_push_force · dt`. Two contacts on one body in one call therefore delivered twice the
declared ceiling, three would have delivered three times, and nothing bounded the factor. The first
round wrote that consequence down — "the per-entry ceiling is unchanged" — which was exact and
insufficient: `max_push_force` is documented in newtons, and a ceiling one can exceed by being
touched twice is not a ceiling.

The impulses destined for one `BodyId` are now summed, the sum is capped once, and one impulse per
body is applied. The ceiling means what it says, the atomicity is untouched since nothing leaves
before the publication, and the answer stops depending on how many contacts the sweep happened to
make against one body — an iteration detail, not a physical quantity.

The scan in `add` is linear over at most `max_touched` entries, which is twelve. A hash container on a
path M1.1.14 must make reproducible would be the wrong trade even at scale.
The ceiling test's number is precision-INDEPENDENT where the slack-ceiling one's is not: under a
binding ceiling of `5 · 1` the box leaves at exactly 5 m/s at both precisions, because the contact
count no longer reaches the answer. That independence is the second half of what coalescing bought.

And the brief records why the seventh finding is not fixed here. Traced: `hit_dist` is exactly zero
every iteration, the resolved normal is the surface's +Y, the padded advance returns zero and the
slide returns a horizontal motion projected on a horizontal plane unchanged, so neither position nor
remaining moves — four identical iterations, then the remainder is dropped. The observable
formulation is right, but not counting the iteration alone is an infinite loop, and the only progress
available without a new mechanism is to serve the remainder — MEASURED to tunnel: with a wall whose
face stands at 0.8 the sweep still reports only the plane at distance zero, so serving would carry the
character through it. A safe fix needs a per-call exclusion threaded through `sweepNearest` and its
five call sites. Reported rather than widened, per the gate's own rule.
A base placed exactly tangent to a surface is a degenerate input: the controller's resting pose is
`padding` above its floor, and at exact tangency the horizontal sweep reports a contact at distance
zero the padded advance cannot clear, so the character serves no horizontal motion at all,
permanently, and nothing reports it. Measured at six starting heights: only exactly zero fails.

Documented and not fixed. The defect, its two candidate fixes and the two forms already refuted by
measurement are an open decision in `CLAUDE.md`, owned by the next milestone that opens
`character.zig`. This is the only half of the problem a closure gate can close honestly.
A capsule left exactly tangent to a surface served NO horizontal motion at all, permanently: the sweep
reports a contact at distance zero, `paddedAdvance` returns `max(0, 0 − padding) = 0`, and the slide
returns a horizontal motion projected on a horizontal plane unchanged, so all four slide iterations are
consumed with no progress and the remainder is dropped.

§1.12.6 makes the `padding` stand-off an obligation of the controller and nothing established it —
`paddedAdvance` cannot, having nothing to subtract from when the advance is zero. And `depenetrate` was
MANUFACTURING the failing state rather than merely failing to leave it: traced, a character starting
0.05 m inside the floor was resolved to a base of exactly `0.000000000` and froze, so any
interpenetration at all ended frozen, not only an authored tangency.

The fix is one parameter and one term: push out by `penetration + padding`. It cannot over-fire by
construction rather than by a threshold — a manifold exists only within the contact margin, so a capsule
already standing off is `.separated` and invisible to the query; traced, 0.005 and 0.02 produce no
contact at all. No exclusion through `sweepNearest` was needed: `gjk.zig` classifies exact tangency
`.shallow`, so the manifold path sees a tangent contact perfectly well.

Also withdraws the precondition documented on `CharacterDescriptor.position` last round, and says why: a
precondition the field's own `Vec3.zero` default violates is a bug with an apology attached. And deletes
a `plannedPush` paragraph still describing the pre-coalescing worst case.
The pin asserts BOTH directions over seven heights: zero now serves the whole metre, and the six clear
heights keep exactly what they had. The comment describing the defect as pinned-not-fixed is deleted
rather than amended — it described a state that no longer exists.

The seventh height earns its place by measurement rather than symmetry: at 0.2 the character enters NOT
grounded, the entry probe being bounded by `padding + predictive_contact_distance`, so floor-sticking is
skipped and it stays at 0.2 reporting `.in_air`. My first expectation for it was wrong, by a formula
that only covered grounded cases, and it is the case that proves the new branch does not touch a
character in contact with nothing.

Two other expectations moved and each was re-derived, not bumped. The interpenetration test's push is
now `overlap + padding` along the slope normal and floor-sticking then re-seats the character — which
the old expectation did not have to account for BECAUSE the old behaviour was the defect: a tangent base
made `stepDown`'s padded advance clamp to zero, so the old clean `0.0173205` was itself a consequence of
the freeze. The doorway's offset moves by exactly `padding`, and the assertion that matters — still
inside, at all three widths — was checked before the number was touched.
`penetration + padding` with `padding = 0` and a tangency of `−0.0` moves the capsule by nothing, so
the freeze closed last round was intact on that path — and zero is in the domain deliberately, since
it means "no physical margin". Removing it would mask the defect instead of closing it.

The target becomes `max(padding, standoff_floor_k · floatEps(Real) · coordScale)` with
`standoff_floor_k = 64`, strictly above `contact_margin_conv_k`'s 16: the floor exists to put the
capsule OUT of the contact margin, not on its edge. With `padding = 0` a resting character stands that
floor above its floor, which is correct — zero means no physical margin, not "stand inside the band
where the classification decides the verdict frame to frame".

That constant is the one place in this module where §1.11.2's tolerance discipline DOES govern, and it
says so at its declaration: `padding`, `max_slope` and `predictive_contact_distance` are named physical
parameters the discipline deliberately does not reach, and this module spends its length saying so.

The `depenetrate` comment is rewritten from an accepted limit into the contract it now is: `padding` is
what a sweep RESERVES, the pose invariant being only that the capsule is never left inside the contact
margin, and an authored pose closer than `padding` but clear of the margin is deliberately not
normalised.
Zero padding serves every call over three calls at six starting heights and against a 5 m collider.

And a separate limit, found while probing the floor: at a 1 km collider the move stalls from the second
call at f32, the character reading `.in_air`. My first version asserted this as a zero-padding residual
of the floor and BOTH halves of that framing were refuted by measurement — it happens at the default
padding as much as at zero, and with the floor removed the numbers are identical to the last digit. It
is a large-collider precision limit of the §1.11.4 bis class: the box face at zero is known only to
`ulp(1000)`, the pair's contact margin is 5.2e-3, and f64 serves every call. Pinned with a 5 m contrast
so it reads as a scale statement and not a box statement.

A second error of mine, also caught by measurement: the default-padding leg put a second character in
the first one's world, and self-exclusion being unilateral it collided with that presence and read
0.38 m on a call it expected to serve in full. A separate world, with the reason at the site.
`padding` had no upper bound. `max(0, d - padding)` zeroes every advance once padding is large, and the
same value enters the depenetration target, so a character created near a wall is displaced by it.
`padding >= radius` is refused at creation and at the resize: the relation is a domain of the pair, so
shrinking the radius violates it while neither field is individually out of range. The motive is
geometric — a stand-off shell thicker than the radius it surrounds is incoherent, the reference carrying
0.02 against 0.3.

And the domain table found a bound that froze: `padding = 0` with any downward component in the
displacement served 0.2 m once and 0.0001 after. Two diagnoses were prescribed from reasoning and
refuted by measurement — the depenetration was already floored, and flooring the descent closed the
horizontal cases only. Traced, the mechanism is neither: the advance runs along the DIAGONAL, so its own
downward component lands the capsule exactly tangent inside the slide loop, after which three iterations
advance nothing and the remainder is dropped.

So the fix is the class, not the instance: all five `paddedAdvance` call sites take `standoffTarget` —
the slide, the step's lift, forward and land, and the step-down probe. When the same defect appears
twice, sweep the class.
The padding relation refused at both doors, with both ends of the legal domain exercised so the guard
cannot be read as "padding must be small", and the resize re-check pinned on a shrink that violates the
pair while neither field is individually out of range.

And a DOMAIN TABLE: every descriptor parameter at its legal bounds, measured on one scene and asserted
rather than reported. No gate of this milestone enumerated those bounds and asked what the code does at
each — gate F decided which values to reject and never asked the other question, and both ends of
`padding` fell into that hole. After the class sweep no bound freezes and none produces silently wrong
geometry: the two rows that deviate from the reference are the declared stand-off and the declared slope
limit doing exactly what they say, and every row ends grounded.

Coverage stated rather than implied: of the four new mechanisms three fail a test when removed, and the
`stepDown` site does not — the slide site's floor already prevents the tangency it would recreate, so it
is covered by the contract and not by an assertion.
`@max(padding, floor)` let the floor override a requested padding: `64 * floatEps(f32)` crosses the
0.02 default at 2 632 m and reaches 3.8 cm at 5 km, so a caller asking for 2 cm silently got 3.8 and
two identical scenes translated apart stopped at different distances. Now the floor serves only a
caller who asked for no stand-off; a non-zero padding is honoured exactly and is invariant under
translation.

And the `padding < radius` guard is REMOVED at both doors. Its freeze motive was already conceded —
a large padding stopping the character short IS the declared stand-off. The remaining motive, that a
push larger than the shape might carry the capsule through thin geometry, was measured across
fourteen configurations: padding at 2x and 3.3x the radius, against a 0.1 m wall, at seven entry
depths straddling its mid-plane. The capsule exits on the side it entered from every time — the
push-out invariant reverts to the entry pose whatever the push size. The guard rejected valid
descriptors on no basis.

The `depenetrate` comment no longer asserts a narrowed contract the spec does not carry. An
implementation comment cannot redefine a normative document, so it records the real state instead:
the code is ahead of the spec until the narrowing lands upstream.
A non-zero padding is honoured exactly over five decades of distance — 0 to 50 km against a
half-space floor, the whole metre every call at both precisions.

And the stall regime is a COLLIDER SIZE, not a distance, which is what the measurement corrected: the
prediction put it beyond 10 km, and there is no stall at 50 km because `gjk.zig`'s coordScale is
relative, `|dpos| + coreExtent(a) + coreExtent(b)`, so it does not grow with distance from the origin.
Swept at f32: 100 m half-extent serves every call, 300 m stalls on the third, 500 m and above from the
second; at f64, 2 km still serves. Pinned at 100 and 500 with the f64 leg, which makes it a precision
regime and subsumes the 1 km row as one point of a curve.

The padding guard's test is removed with the guard, and the domain table gains a `padding = 2 * radius`
row in its place — legal, stopping short exactly as `r - eps` does.
@guysenpai
guysenpai force-pushed the phase-1/forge/character-controller branch from 4abd9f1 to bbafa6c Compare August 5, 2026 17:15
The last four rounds each closed one way of arriving at exact tangency, and another path appeared each
time, because arrival is a float-resolution phenomenon with as many paths as one likes. What is finite
is what tangency then does, and it does it in one place: a zero advance against a surface that does not
oppose the travel direction obstructs nothing, yet the slide leaves the motion unchanged, the next
iteration finds the same contact, and the budget burns with the remainder dropped.

That body is now set aside for ONE retry that does not spend the budget, and the sweep reports the next
real obstacle. Traced first: with the resting floor set aside and self-exclusion kept, the sweep returns
null and the whole remaining metre is free.

`SweepCollector` gains `exclude_also`, `sweepNearest` gains the parameter, and the four non-slide call
sites pass null so their contract is unchanged in meaning. Bounded by construction — one slot, one free
retry per call — and with no epsilon: the advance test is exact zero and the opposition test is the sign
of a dot product.

The round-2 estimate that this needed threading through five call sites was made without reading
`SweepCollector`, which already carried an exclusion. 48 lines.
The squeeze and the doorway both pinned a stall that is now gone: a ceiling's downward normal does not
oppose a horizontal motion, and two antiparallel wall normals do not oppose a motion along the corridor.
Both keep the assertion they exist for — never through the floor, never ejected — and those were checked
before the numbers were touched.

The 1 km collider was expected to flip too and does NOT, and the trace says why: `slideNormal` returns
null at that scale, so the loop takes its documented "stop rather than guess a direction" exit and never
reaches the non-opposing test. Serving it would mean inventing a direction the narrowphase declines to
supply. The test keeps its stall expectation and gains the real cause. `padding = floatMin` is added at
both precisions on the 5 m scene.

And the distance-invariance test was VACUOUS — restoring `@max(padding, floor)` broke nothing, because
starting at `y = padding` with a horizontal motion and asserting `x` alone never exercises
`standoffTarget`. It now walks into a wall and asserts the final clearance: at 5 km the floor is 3.81e-2,
so `@max` stops 3.8 cm short where the caller asked for 2. Eighth green assertion in this milestone that
proved nothing, and the first found by external review rather than by the probe table.
Ten consignations with their owners, the domain table posed as an exigence for the next milestone that
declares a descriptor, and the mutation probe written as a method rather than an anecdote: the probe
table disables mechanisms and is therefore structurally blind to a test that exercises none, so the
counter-measure is to restore the previous implementation and require the test to fail.

Item 1 of the closure order is BLOCKED and verified rather than presumed: the deposited
`engine-physics-forge.md` carries the milestone-opening mtime and its §1.12.6 still reads "le `padding`
tient la capsule à distance des surfaces", the exact sentence the narrowing replaces. `handle_dead`
appears in neither file. The site comment recording the divergence stays.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant