M1.1.12 — Forge 3D: the kinematic character controller - #68
Merged
Conversation
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.
The active-edge flags baked at M1.1.11.1 are NOT unconsumed: internalEdgeNormal consults them and is called on the contact path. What bypassed it was the character's cast path, which calls collideOrdered directly — so a capsule deep under a flat quad received the normal of the quad's INTERNAL DIAGONAL, measured horizontal and opposite on the two triangles sharing it, and verified by arithmetic against that edge's horizontal perpendicular. The slide read the pair as a crease, a third plane as a corner, and stopped. The edge is recovered from the contact POINT's distance to the three segments, which is what internalEdgeNormal already does. feature_id cannot serve: decoded on the faulty contact it carries a reference face on the SEGMENT (per-core-type ids, segment 6 and triangle 8) and incident winding VERTICES, class_edge never fires, and half the faulty contacts are single-point manifolds with two FACE halves and no edge information at all. So this is a reuse and not a mechanism. It closes ONE of the three residual cells and not the others: at f32, 26 of 28 converge and the two left are 0.506016 at 90 deg base 0.05 and 0.459151 at 315 deg base 0.05 — both at the lifted base where the closed one was the tangent base. At f64 nothing moved, the correction's noise gate being far tighter there. So the convergence condition is NOT met: no common expectation is pinned, the no-freeze property stays the pin, and the consignation stands.
Rewritten because the entry now describes something else: two thirds of the yaw dependence are closed and their parts are named. The severe half went with the retained contact's normal reaching the slide; one further cause was the internal diagonal of a flat quad returning its own normal in deep overlap, horizontal and anti-parallel between the two triangles, which the plane accumulator read as a sharp edge. The remedy was already in the module and merely bypassed. What remains is unnamed, and the entry keeps the one quantitative constraint we have on it: f64 does not move a digit where f32 gains a cell, and its noise guard is tighter by 2^29, so the cause sits above that guard at f32 and below it at f64. Both previously named candidates are recorded as traced and eliminated rather than merely doubted.
The magnitude bound does not discriminate it: that cell read 0.459 before the correction reached the character's cast path and ~1.700 after, and along > 0.1 accepts both, so removing the internalEdgeNormal call outright left the suite green. A bound is the right shape for the no-freeze property and the wrong one for a fix whose whole effect is WHICH value is served. Pinned to the measured stop at both precisions, 1.699976100 at f32 and 1.699999988 at f64. The 2.4e-5 gap between them IS the stand-off floor, 64 * floatEps(Real) * coordScale, which vanishes at f64 — so the split is by precision with a named cause and not by platform. Mutation probe against 36c6866: fails with actual 0.45921806 against expected 1.6999761.
A horizontal triangle's normal does not survive a quaternion yaw exactly: transported through 90 degrees it reads (7.4e-16, 1, 1.1e-15) at f64 and (3.97e-7, 1, 3.97e-7) at f32. Its dot with a horizontal sweep is then a small NEGATIVE residue where the geometry has an exact zero, so an exact >= 0 test admitted a FLOOR as an obstacle to horizontal travel. Advance zero, iteration spent, plane slot spent; the slide against that near-vertical normal injected a vertical residue into the motion, which admitted the CEILING the same way; and two near-anti-parallel planes formed a crease whose axis is their cross product, which is pure noise. At f64 that axis re-found the floor and the character froze at exactly zero every call, at f32 it happened to find a real contact. A coin toss arbitrated by rounding. opposing_noise_k = 16, as k * floatEps(Real) and nothing else: n . d is a product of two UNIT vectors, hence dimensionless, so no coordinate scale belongs in it — the difference from contactMargin, whose operand is a distance. The band reads NON-OPPOSING and discards: discarding a contact that opposed by a hair advances by a hair into a surface and the next call's depenetration recovers it, while reading it as opposing freezes, and a freeze recovers from nothing. Measured residues are 3.3 and 5 floatEps, so k = 16 is a 3x margin, and the angle it costs is asin(16 * floatEps) — 1.1e-4 degrees at f32. Governed by 1.11.2, unlike max_slope, padding and predictive_contact_distance. One predicate shared by all four arms so they cannot drift. All twenty-eight cells now converge at BOTH precisions: f32 in [1.699976300, 1.699986600], f64 in [1.699999905, 1.700000054]. So the common expectation replaces the magnitude bound, the Real == f32 guard is gone, the title is true, and the fifth open decision is deleted rather than rewritten. Mutation probe against 4438a4e: actual -0 against expected 1.7.
The kernels normalise the sweep direction themselves, so a caller is entitled to hand this path a direction of any length — but `opposes` compared `n . d` against an ABSOLUTE threshold, so the same geometry with the direction handed over twice as long changed sides. No geometry justifies that; it was introduced with the noise band one commit ago. The threshold is now scaled by the direction's length, which is exactly normalising it without paying a division, and it is guarded at TRUE zero by construction: at zero length the dot is zero too and `0 < 0` is false, so a null direction opposes nothing and no epsilon is invented. `n` is unit at all four call sites by the narrowphase's own contract, so only `d` needed the scaling. 489/489 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
The band closed all twenty-eight cells of the insoluble-squeeze grid at both precisions and still had to go: what it bought in an authoring-fault configuration it paid for on the DEFAULT path. Discarding a grazing contact lets the capsule advance INTO the surface by distance x |n.d| — measured 3.05e-5 over 32 m and unbounded in the distance. A traversable geometry is worse than a partial serve. And no band on that quantity can work, which is the durable result of the thread and is written at the site so nobody retries it. The two populations are a TRANSPORT RESIDUE tracking floatEps(Real) — it is the rounding of a quaternion rotation — and a REAL GRAZING INCIDENCE that is geometric and precision-free. Nine orders of separation at f64, a factor of 2.4 at f32, so the inseparability is structurally an f32 phenomenon. The length-dimensioned form escapes nothing: coordScale ~ distance makes it this test with both sides multiplied by the distance. The predicate returns to an exact zero. P2's norm-independence is KEPT but its mechanism is now structural rather than arranged: scaling d by a positive factor scales the dot by the same factor and cannot change its sign. The ||d|| term is not merely inert at zero — 0 * inf is NaN and x < NaN is false — so writing it would introduce a path where an infinite direction opposes nothing. What comes back is PINNED and not suffered: 0.506016400 at f32 for yaw 90 from a base of 0.05, and an exact zero at f64 for the same yaw from a tangent base, which nothing watched before. Those two assertions are the pin of the revocation itself and fail against a2e7493. The title stops claiming the precisions agree, because they do not.
The fifth open decision returns, carrying what the previous version could not: the named cause, the remedy that was tried and revoked, and the algebraic demonstration that no band on n.d separates a transport residue from a real grazing incidence at f32. And a tooling fact the revocation itself produced: 94ee517 carried a remedy and every expectation that remedy made possible in one commit, so undoing it could not be a revert — it was a hand reconstruction of three expectations, two re-measured cells and a re-authored consignation. Third instance of the class in this milestone. The cost is invisible going forward and paid going backward, which is when one is least willing to pay it. Separated from the revocation for exactly that reason.
The predicate conditioned the direction itself instead of taking what the kernel already computes, and got it wrong twice from that one root: an absolute threshold made the verdict depend on the direction's length, then a bare sign test UNDERFLOWED. For a denormal direction a product n_i * d_i with |n_i| < 0.5 flushes to exactly zero, the sign is destroyed, and a real wall reads non-opposing and is discarded — contradicting the contract shapecast_test.zig already pins, that every non-zero direction including a denormal is served. unitOf becomes pub and is re-exported by the narrowphase facade. It reduces by the largest absolute component before normalising, so it neither squares a denormal into zero nor overflows a huge one, and returns null at exact zero and nowhere else. There are no longer two answers to what direction the cast travels. The regression is at the BODY level, which is the gap that let both forms through: the kernel's own denormal test passed the whole time because it never goes near this predicate. Scope measured, not assumed. Triangle soup is excluded because the PLAIN cast returns null there too, so the two agree and the filter is not implicated — the direction is lost upstream in the swept traversal. Half-space is excluded because plane.zig asserts the direction is unit where shapecast.zig conditions it: two kernels, one parameter, two contracts, with the entry normalising once. Both are recorded rather than papered over. The tilt is measured, not derived: an axis-aligned wall proves nothing, since -1 * floatTrueMin is representable, and a first version of this test passed against the pre-fix commit. Swept over ten yaws the raw product survives at 20-50 and 70-85 degrees and is exactly zero at 60 and 65, where the contact normal's X component reads -0.4999997 and -0.4226. 65 is taken, and the test fails at BOTH precisions against f0ba370. 490/490 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
castShapeBody had a contract per SHAPE CLASS that nobody had written down, which is why the same defect arrived one cell at a time for four rounds. Measured over three classes and six properties at both precisions, four cells diverged and all four were the DIRECTION: plane.zig asserted the vector was unit and conditioned nothing, so a non-unit, denormal or zero direction was a domain violation on that arm and an ordinary answer on the other two; and the mesh arm additionally lost a denormal in its swept traversal. Conditioning once at the entry took all four together, the mesh traversal included. The conditioning skips a direction that is ALREADY unit, and that is not an optimisation. A first form normalised unconditionally and shifted every query answer by an ULP, caught by mesh_test's bit-exact brute-force oracle: the query entry already normalises, so an unconditional line here was a SECOND conditioning of one vector — the class this pass exists to collapse, re-introduced by the collapse. The gate is the module's existing unit predicate and no answer depends on which branch is taken. opposes now ASSERTS its precondition instead of re-establishing it, for the same reason. And unitOf reverts to private: it duplicates Vec.normalizeScaled line for line, so publishing it would have been a second answer as well. Two cells were measured NOT to diverge and are recorded as such rather than uniformised: hit.normal at an initial overlap differs in value because 1.11.11 fixes -direction as the FALLBACK and the half-space has no terminal simplex to offer instead, while all three keep the guarantee the contract actually makes, normal . direction <= 0; and the manifold is consulted on all three arms. The parameterised test is what was missing from the start: one scene per class, the same assertion for all three, so a future arm breaks it here rather than in a character scene three milestones later. It aborts against 0dc9137 on plane.zig:297. 491/491 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
The pass had uniformised the three shape classes by the PERMISSIVE, making the adapter condition its own direction. The map of every production caller says nobody needs the permission: query/root.zig conditions once for the whole query family and guards zero there, calling a zero direction a legal query with an empty answer; the ground probe and the step sweeps pass the exact constants up and up.neg(); the slide normalises its own remainder. So it uniformises by the STRICT instead. castShapeBody is an INTERNAL adapter of BodyManager, not a public entry: requiring a unit direction of it is legitimate where requiring it of query.shapeCast would not be, and the public contract does not move. And conditioning at the entry was not free of the defect it closed. The query entry already normalises, so an unconditional line was a SECOND conditioning of one vector; skipping it inside a tolerance left the two arms disagreeing INSIDE that tolerance, since plane.zig uses the raw norm in t = sep / -closing while shapecast.zig renormalises. Measured, a conditioned direction lands 0 to 1 ULP from unit, so that branch was taken on every query. An assert has no inside. The controller's remaining.scale(1 / distance) becomes normalizeScaled, the same shared form the query family uses. Stated plainly: nothing in the suite discriminates that line today — the denormal end is masked by the len_sq == 0 guard and the huge end aborts earlier on a separate, pre-existing defect where an infinite max_distance reaches shapecast.zig:193. It is an alignment guarded by the new assert, not a fix with a pin. Two table cells therefore move from served to precondition, and the property is pinned one tier up where it IS a contract: a second parameterised test asserts that query.shapeCast serves a non-unit and a denormal direction and answers null on zero, on all three classes. That table pins a long-standing contract and does not discriminate this change; the assert, running on every call in Debug and ReleaseSafe across the suite, is what pins the change. P3: three comments contradicted the code and are replaced rather than amended — the opposes doc still described the entry conditioning, and C4 claimed the manifold is consulted on all three arms where the half-space transports its stored plane normal and consults nothing. 491/491 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
The slide asked three separate questions of the same vector and got three different domains. lengthSq() == 0 for emptiness UNDERFLOWS, so a denormal remainder read as nothing and the displacement was silently dropped; sqrt(lengthSq()) for the distance OVERFLOWS, so an infinite max_distance reached the kernel and tripped its finiteness assert. The reduction by the largest absolute component that normalizeScaled already performs internally has neither failure, so Vec gains unitAndLength, which returns both quantities from that one reduction and is null at exact zero — the emptiness test the caller needs, without a threshold that could disagree with the direction of the same call. normalizeScaled delegates to it, same operations in the same order. A SECOND site of the same class in the same function: the step attempt derived its own sqrt(remaining.lengthSq()), and that is the one the huge case actually aborted on. Both now take the same reduction. Pinned at both ends of the range, and the test aborts against 77e49c2 on shapecast.zig:193, which is the infinite max_distance the second site produced. 492/492 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
An assert BOUNDS a direction, it does not canonicalise it. (1 + 4eps, 0, 0) passes every unit assert in the module, and inside that band the two arms of castShapeBody consumed different vectors: the convex kernel reconditioned its copy through unitOf while plane.zig used the raw norm in t = sep / -closing. Measured against ed3a8cf, one query answered 3.7 on one arm and 3.6999984 on the other, at BOTH precisions. Two forms could close it and the choice was MEASURED against the bit-exact oracles, which is the only criterion that has decided correctly on this column. Having the half-space condition like the convex breaks TWO plane tests, and they are a CONTRACT: normal.eql(d.neg()) at an initial overlap is 1.11.11's identity across the four kernels, and conditioning inside the plane kernel returns -normalizeScaled(d) instead of -d. Having the kernel not recondition breaks ONE test, and it is an ORACLE ARTEFACT. So: form one. castShapeUnit is ADDITIVE. castShape and castShapeBounded keep conditioning, keep their zero-direction guard and keep their signatures, so no existing caller moves; the shared body becomes a private impl taking a comptime flag. Only castShapeBody takes the new entry, its direction being a precondition it asserts — and that assert stays, no longer a band masking a divergence but the precondition of an entry that reconditions nothing behind it. The mesh_test line is an ORACLE CORRECTION and it is written at the site: the test measures that the accelerated path agrees with brute force, and two sides calling different kernel entries compare two things instead of one. Aligned, it measures the acceleration again. Codex's reproducer is pinned, bit-exactly and not approximately — the defect is one ULP and any comfortable tolerance would hide it — with a bound placed strictly between the two answers a divergent pair gives, where one ULP separates hit from miss. It fails against ed3a8cf at both precisions. 493/493 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
unitAndLength protected the intermediate square and not the final product: two components at 0.75 * floatMax have a norm near 1.06 * floatMax, which no float here holds, so largest * |reduced| left the range and the function answered inf. That inf became a sweep bound and tripped a kernel's finiteness assert one call later. Treated AT THE SITE that produces the value, as asked: length becomes optional, because a DIRECTION always exists where a LENGTH need not, and the direction is scale-free and unaffected. Not an assumption each caller re-derives. The controller TREATS it rather than leaving it consigned, so the consignation shrinks by that much: an unrepresentable norm is still a request, served to floatMax, which is the largest distance this arithmetic can express — the world's colliders bound the sweep long before it, and a caller asking for more is asking for more than a POSITION can represent either. Third site of the class swept two rounds ago, and one tier below the two others. The denormal assertion was VACUOUS: x >= 0 also passes for the old behaviour, where the displacement was dropped and x was exactly zero. It is bit-exactly the displacement at both precisions and now says so — an inequality where an exact equality was available admitted the defect it was written against. And the mutation probe did not catch that, which is the round's method finding: it had been run, and it FIRED — on the huge case, whose abort ends the body before any later case runs. A probe on a multi-case body proves that AT LEAST ONE case discriminates, never that each does. The three cases are now three tests, each with its own verdict, and each fails against the commit that precedes its own fix. Exposed and NOT chased, because it is a different limit: at 0.75 * floatMax from the origin a broadphase node's (min + max) * 0.5 overflows, so the ray origin it derives is infinite and a different assert fires. The test closes its scene with walls to keep the question on unitAndLength. 495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
The saturation was the module's only exception to its own written rule, and the rule sits three lines above the error set it belongs to: silently clamping makes a caller's mistake look like a modelling choice and leaves no diagnostic. Serving an unrepresentable norm would also mean segmenting the move, turning this entry into a multi-segment integrator for a request no POSITION can represent — 0.75 * floatMax is outside any expressible world, and the broadphase's own node arithmetic overflows there, which is why the test scene has to close its walls near the origin. So CharacterError gains InvalidDisplacement and moveCharacter uses the error channel gate A gave it. THE CALL PARAMETER NOW HAS A DOMAIN: finite components and a representable norm, checked at the entry like every descriptor guard, and written at the site. Every other guard in this module belongs to the descriptor and is checked once at creation; displacement is checked per call and was checked nowhere, which is what let an infinite bound reach the sweep. Both open ends of that domain stay open and are asserted: EXACT zero is a legal no-op, and a DENORMAL displacement is still served — the norm is unrepresentable only by overflow, never by underflow, the direction being scale-free. Downstream the length is representable by INVARIANT rather than by hope: the entry refused otherwise and remaining only shrinks, both slide forms being projections, so the two sites take it with a checked unwrap that states it. Recorded rather than assumed: I did not reproduce a traversed plane from the saturation in my own scene, which ends at (1.686, 0.02, 2.18), inside its walls. What I did find is that the assertions I had written there were ONE-SIDED — x < 2 and z < 3 — so they could not have detected a traversal in the other direction anyway. The refusal removes the path either way. And there was no consignation in the repo to withdraw: that class lived in the conversation and was never written into the brief or CLAUDE.md. 495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe. The test aborts against 2413585, where the same call saturated instead of refusing.
The domain tested representability in Real, which made it a function of a BUILD FLAG: the same public vector was refused at f32 and accepted under -Dphysics_f64, where its norm is an ordinary number. The interface takes a Vec3 in f32, so the discriminant is the quantity's ORIGIN and not the precision that handles it afterwards — the tolerance class this milestone made normative, applied to a DOMAIN instead of to a comparison. The bound is floatMax(f32), evaluated at the boundary, and the same vector is now refused at both. The test passes ONE input at both precisions instead of two, which is what the Real-scaled literal was hiding. And the HUGE test was the same defect: it read , so the two builds compared two things. It now uses 1e30 at both, and states plainly that the mechanism it exercises is live at f32 alone — 1e60 overflows there and is an ordinary number in f64 — so the guard is load-bearing at one precision and inert at the other, rather than dressing the f64 leg up as testing the same thing. moveCharacter's error documentation carries InvalidDisplacement with its reason. Mutation probe: against e92b476 the f64 leg accepts what f32 refuses, and the test fails there. 495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
Both control witnesses verified on the text before touching anything, not on the files being present. engine-tier-interfaces.md carries "Le canal porte AUSSI le domaine du DEPLACEMENT", the f32 boundary evaluated before widening, and the prohibition on saturating with its measure — a saturated bound leaves the TAIL of the displacement unswept, so a plane sitting there is traversed. engine-physics-forge.md 1.12.6 carries "le premier parametre d'APPEL du module a en avoir un" and why the domain table missed it: it tabulated fields, not parameters. The site text predated both and therefore paraphrased what is now normative elsewhere. Removed rather than amended: a second formulation of a normative rule is a second source, which is the rule of two sources applied to prose. What stays is what the code does and what only the site knows — the two open ends of the domain, and the two distinct refusals that share it. That measure also reconciles a disagreement I reported: I could not reproduce a traversed plane from the saturation in my own scene, and the reason is now named — the traversal comes from the unswept tail, and my scene's walls sat inside the saturated bound, so it could not exhibit it. 495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
The list on moveCharacter still announced StaleCharacter and the allocation failures alone, which the previous patch was meant to correct and did not. It points at 1.12.6 for the domain rather than restating it. P1 is NOT in this commit: the prescription and its stated outcome are not compatible, measured, and the fork is reported instead of picked.
The bound was floatMax(f32) and the norm was still computed at Real, so the domain went on depending on the build through the ROUNDING instead of through the bound. Codex's vector shows it: (floatMax(f32), 0.0002 * floatMax(f32), 0) was refused at f64 and accepted at f32. Evaluating in f32 does not fix it, and that is measured rather than argued: the reduced square 1 + 4e-8 falls below the half-ULP of 1.0 in f32, rounds to exactly 1.0, and the norm comes back as exactly floatMax(f32) — f32 cannot see its own overflow, so it would accept a vector whose true norm leaves the range. The prescription and its stated outcome were not compatible, which is why the fork was reported instead of picked. So the contract is stated in its own terms: THE DOMAIN IS THE f32 RANGE, AND IT IS EVALUATED IN AN ARITHMETIC THAT CAN DECIDE THAT RANGE WITHOUT ROUNDING. The width is the consequence and not the rule. Widening the components is EXACT, so the verdict is a pure function of the public input and identical under either -Dphysics_f64 setting. The declaration cites 1.11.4 bis, which measured the same phenomenon on the ray normals — the information is present in the f32 inputs and only a wider arithmetic extracts it — so a reader does not read the width as a precision picked at random. No reduction by the largest component is needed at this one site, and the reason is written there: the widening makes the square safe for every f32-origin input, and a square that overflows f64 can only come from a component already outside the f32 range. The test passes that exact vector at both precisions, formed in f32 and widened. Mutation probe against 8cab4b0: it FAILS at f32 and passes at f64, which is the asymmetry this removes. 495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
Both witnesses verified on the text: engine-physics-forge.md 1.12.6 carries "La regle dit quel domaine s'applique, pas quelle arithmetique l'evalue", with the reproducer as its measurement and the 1.11.4 bis reference; the moveCharacter entry carries "LE DOMAINE EST LA PLAGE f32, et il est EVALUE dans une arithmetique capable de trancher cette plage SANS ARRONDI". Reading 1.12.6 in full, it now also owns the two ends the domain leaves served — exact zero and the denormal — which the site still restated. So the declaration was paraphrase end to end and collapses to a pointer. Sixteen lines removed rather than amended. What stays is what only the site knows: that no reduction by the largest component is needed at this one place, and why. The guard comment also stops presenting f64 as a local choice — it is the arithmetic 1.12.6 requires. 495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
floatTrueMin(Real) is an f32 denormal in one build and an f64 denormal in the other, so the two legs compared two different vectors while the domain under test belongs to the public entry. floatTrueMin(f32) widened, and the underflow assertion conditional as in the HUGE case — the mechanism is live at f32 alone, and saying so beats a second literal that would make the f64 leg look like it tested the same thing. Third instance of the class in this file. P2-1 is not in this commit: measured exactly, its premise does not hold, and the measurement is reported first.
The one method lesson this round produced, and the only external finding of the milestone that did not hold. A first oracle answered with hypot in f64 and carried exactly the rounding it was judging, so it agreed with the guard and reported nothing — which reads as "no defect" for the wrong reason. Rewritten over exact rationals against the true f32 overflow boundary, the answer inverted: the vector is IN domain and accepting it is correct, and the exact-component predicate proposed in its place is worse in both directions. P3 was already delivered in 30b900a: floatTrueMin(f32) widened, underflow assertion conditional as in the HUGE case.
Both witnesses verified on the text: 1.12.6 carries "Une borne enoncable vaut mieux qu'une borne exacte quand l'ecart entre les deux n'a aucun habitant", with the true threshold 2^128 - 2^103 named and the band measured; the moveCharacter entry carries "norme AU PLUS floatMax(f32)". The site diverged on one real point and it was not a nuance: it said "a norm outside the f32 range", which is wider than the bound the guard applies and contradicts what the spec now establishes — the refused band is INSIDE the f32 range, every real below 2^128 - 2^103 rounding to a finite f32, and the bound is deliberately conservative. Replaced by the bound itself, and three further mentions of "the f32 range" and "representable" in the guard and the loop are re-expressed against the bound for the same reason. 495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
Witnesses verified on the text: 1.12.6 carries "norme CALCULEE au plus floatMax(f32)" with "et non la norme mathematique", and "Le domaine se declare donc sur ce qui est decidable"; the moveCharacter entry carries the same computed-versus-mathematical distinction. The "aucun habitant" phrasing has disappeared from both, counted. Two divergences at the site, and the second was not on the list. The summary line stated the bound as if it applied to the true norm — corrected to the computed one, with the arithmetic named. And the guard comment claimed that arithmetic "can decide that bound WITHOUT ROUNDING", which the new text explicitly refutes: no fixed-width arithmetic decides the mathematical form exactly, the squares of the widened components being exact while their sum is not. That sentence would have contradicted the spec it points at, so it is replaced rather than softened. No code change. 495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
Absence verified as much as presence, which is what this round is about: 1.12.6 carries "Elargir deplace le seuil de l'arrondi, il ne le supprime pas", and "sans arrondi" now has ZERO occurrences in either spec — counted, not assumed. The repo is swept for the same residue and has none either. The site diverged by carrying its own formulation of what the source now states better: "no fixed-width arithmetic decides the mathematical form exactly" was a second statement of a normative fact. Replaced by the source's framing at the pointer, and the guard comment keeps only what the site owns — that f64 is the arithmetic 1.12.6 fixes, not a choice made locally. No code change. 495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
Witnesses verified, presence and absence both. 1.12.6 carries "L'expression est NORMATIVE, parce qu'un domaine declare sur ce qui est calcule n'est defini que si le calcul l'est" and the transcribed predicate block; the moveCharacter entry carries "L'EXPRESSION est normative". And "sans arrondi" stays at zero in both specs and in src, checked rather than assumed — the residue check is now part of every spec alignment. The suite tested only the refused side, so a finer predicate — hypot, a wider arithmetic, a different summation order — would have refused (M, 1e-9*M, 0) and the suite would have called that an improvement. That vector is now asserted served. Its true norm is M * (1 + 5e-19), above floatMax(f32) and far below the real overflow threshold, so it is a deliberate consequence of the computed form. Its second component points DOWN, and the reason is written at the site: with it pointing up, the vertical part carries the pose to 3.4e29 and the run aborts in the BROADPHASE on the far-field limit this file already records. The sign leaves the norm, hence the verdict, untouched. The guard comment stops describing the expression now that 1.12.6 transcribes it, and says instead what only the site can: the expression IS the domain, so a line changed here changes what the engine accepts and changes 1.12.6 first. 495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
Your arithmetic verified independently before writing. (M, 7e-9*M, 7e-9*M) is accepted in x,y,z order and REFUSED if y and z are summed first: each square alone falls under the half-ULP of M squared and is absorbed, their sum does not. At 1e-9 both orders agree and at 9e-9 both refuse, so it sits in the band where the absorption flips and nowhere else. THE HYPOT CLAIM IS PROVEN RATHER THAN REMOVED, and the case that proves it is new. You were right that the shipped vector does not distinguish hypot — both accept it, measured. But a band exists where they part: the ordered sum loses the term once a squared falls under 2^-53, while hypot only refuses once the true norm exceeds M by half an ULP OF M, so for a in [8e-9, 1.05e-8] the sum refuses and hypot accepts. (M, 9e-9*M, 0) is asserted refused. Your conditional said to keep the claim if such a case exists, so it is kept and locked; if you prefer the two-property framing, that one case reverts alone. The comment now names the three properties and the one case that moves for each, rather than listing mutations it does not lock. The order case needed a -Z wall, and the reason is at the site: its third component is otherwise unblocked and carries the pose to z = -2.4e30, where the run aborts on the broadphase far-field limit — a different limit from the one under test, and the same isolation the refused side already uses. 495/495 green at f32 and -Dphysics_f64=true, Debug and ReleaseSafe.
Two assertions removed. One pinned 0.506016400 at 90 deg / base 0.05 and was refuted by ubuntu-24.04 / Debug reading 1.6999805; one pinned the f64 cell frozen at exactly zero and was never contradicted only because NO CI corner runs -Dphysics_f64 — verified absent from .github/ by grep, so its silence was not a confirmation. Both were measurements of one machine dressed as properties of a quantity the consignation itself calls decided by rounding. The surviving property lost its Real == f32 guard, was RUN unguarded, and the answer is that it fails at f64: yaw 90 / base 0 returns -0e0 here. So the scope is now written as a measured FACT rather than carried by a condition — the claim is made at f32, where four platform-and-mode combinations exercise it, and the f64 failure is recorded in CLAUDE.md instead of hidden. The two sides do not have the same standing and the comment says which. CLAUDE.md gains what the red taught: the residue varies by ARCHITECTURE and by COMPILATION MODE and not only by precision, which is one more constraint on its unnamed cause. And a sixth open decision, because the inventory turned it up: the f64 leg has never run anywhere but one machine, so every f64 claim of this milestone rests on a single sample — not charged to M1.1.12, owner named as the next milestone touching CI. 495/495 green at all four local corners, lint 0. The CI matrix is the only instrument for this class and it is read next.
The row dated from 2026-08-05 and predated twenty rounds of closure: it named neither castShapeBody's contract table nor the displacement domain, nor the four tunnelling defects, nor the methods the milestone leaves. Replaced whole, and the footer date follows. One formatting necessity, not a content change: the absolute-value bars in `distance × |n·d|` are escaped as `\|`, since a bare bar would end the table cell — the same escaping the earlier rows use for `\|omega\|` and `\|Delta pos\|`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
mainata4354df(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
CharacterVirtualhas no rigid body.But it does carry a broadphase presence — an inner body, a
.kinematicbody holding thecontroller's own capsule, mandatory on the module and defaulting to on per character. The argument
is internal to the frozen surface:
CharacterDescriptorhas carriedcollision_layersince itsoriginal 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 presenceBodyId, three outcomessetCharacterPosition— teleport without resolving, verdict invalidated to.in_airgetCharacterInnerBody/destroyCharacter/ the ground verdictcollideShapeBody— the seventh body-level adapter, the manifold fallback the sweep needs whenits 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 fixesit. 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.
collideOrderedreturns null on a separated pair and a resting character standspaddingabove itsfloor, so a manifold-only reading answers
.in_airfor 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_groundthroughout. The rule caps theprojected motion's up component at
max(up_before, 0)and not atup_before; capping atup_beforedrives into the plane on an inclined face and annuls the physically correct answer, abody 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 belowthe ground plane, and nothing in the suite moved at any value — an odd count would have shipped in
silence.
PackedId.deadreserves the all-ones no-handle bit pattern.ground_body's default of0was alive 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.
setCharacterPositionwas 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,
paddedAdvancereturnsmax(0, 0 − padding) = 0, and the slide returns a horizontal motion projectedon 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":
paddingabove its floor, sothe state does not reproduce itself. Only authoring reaches it,
position = (0,0,0)over a floor aty = 0being the natural thing to write.0.005through0.05servein full. The fix is testable in advance and the regression is guarded.
sweepNearestis private tocharacter.zig, no publishedsurface sees it, its five call sites are in that one file.
Mitigated by a documented precondition on
CharacterDescriptor.position. Two candidate fixes and twoalready-refuted forms are recorded in
CLAUDE.md, owned by the next milestone that openscharacter.zig. The refuted ones were closed by measurement, not argument: not counting the iterationleaves 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_velocityfed thepenetration midpoint instead of the body's surface, on a path no test reached.
step_heightunvalidated where every other stored physical parameter is.
setShapemaintaining two of its fourconsequences. The union-published proxy box, wrong twice — permanently fat after a teleport, and
guarding a failure mode that does not exist since
Broadphase.updatealready 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: prescribingfrom 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
slideNormalreturns 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,
@maxbreaking 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
paddingfell into that hole. Cheap at the gate thatdeclares 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
mainthrough a worktree with the sameprobe 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.zigincluded.The NGS energy-injection watch
CLAUDE.mdnames this milestone to re-read is answered bymeasurement: the retained speed is
5.0000005at f32 — exactly one ULP above 5 — and exactly5atf64. 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
test-forge-3d, Debug, f32 / f64test-forge-3d, ReleaseSafe, f32 / f64zig build test, Debug / ReleaseSafezig fmt --check src/ bench/ tests/clean; tree-widezig build lintexit 0. Zero French in code orbrief prose, audited in Python (a byte-wise
grepclass 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