Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
7fe6f3c
docs(brief): add M1.1.12 milestone brief
guysenpai Aug 4, 2026
eca0e4c
docs(brief): mark M1.1.12 specs read and open execution
guysenpai Aug 4, 2026
8f39761
docs(forge): sweep the stale two-term constraint-key prose
guysenpai Aug 4, 2026
bc1b5c2
feat(forge): freeze the character-controller api surface
guysenpai Aug 4, 2026
f677957
docs(brief): record M1.1.12 gate A execution
guysenpai Aug 4, 2026
71fb7a1
fix(forge): reserve PackedId.dead as the no-handle sentinel
guysenpai Aug 4, 2026
9330b5e
docs(brief): record the ground_body sentinel deviation
guysenpai Aug 4, 2026
7ecebb7
docs(forge): replace two justifications derived from outside the engine
guysenpai Aug 4, 2026
dd8ff4a
docs(brief): record the inner-body justification deviation
guysenpai Aug 4, 2026
29644f9
feat(forge): add collideShapeBody, the shape-against-body adapter
guysenpai Aug 4, 2026
44579a1
feat(forge): add the character controller store
guysenpai Aug 4, 2026
1d886be
test(forge): add the character controller acceptance suite
guysenpai Aug 4, 2026
54ae5d7
docs(brief): record M1.1.12 gate B execution
guysenpai Aug 4, 2026
4d03223
fix(forge): complete the character descriptor domain guards
guysenpai Aug 4, 2026
8f25d1e
feat(forge): determine the ground by a bounded downward sweep
guysenpai Aug 4, 2026
7df1174
test(forge): extend the character suite to ground determination
guysenpai Aug 4, 2026
5c3566a
docs(brief): record M1.1.12 gate C execution
guysenpai Aug 4, 2026
36aab2a
feat(math): re-export the exact triangle cross direction
guysenpai Aug 4, 2026
5ed3270
feat(forge): move a character by sweep, slide and depenetration
guysenpai Aug 4, 2026
0e869bb
test(forge): extend the character suite to the move
guysenpai Aug 4, 2026
8334c67
docs(brief): record M1.1.12 gate D execution
guysenpai Aug 4, 2026
db53683
feat(forge): climb steps and stick to the floor on descent
guysenpai Aug 4, 2026
024d5ae
test(forge): extend the character suite to step height
guysenpai Aug 4, 2026
ed604ff
docs(brief): record M1.1.12 gate E execution
guysenpai Aug 4, 2026
e1c60f2
feat(forge): slope-cap the slide, and resize, push and teleport
guysenpai Aug 4, 2026
970b5f5
test(forge): extend the character suite to resize, push and teleport
guysenpai Aug 4, 2026
cc6e23e
docs(brief): record M1.1.12 gate F execution
guysenpai Aug 4, 2026
8e3bb90
fix(forge): reserve before mutating on the presence write paths
guysenpai Aug 4, 2026
9d20f73
fix(forge): cap the slide at zero and never push through a plane
guysenpai Aug 5, 2026
10cc0fb
test(forge): pin the steep-face descent and the push-out direction
guysenpai Aug 5, 2026
b7286e1
docs(brief): record the M1.1.12 gate F corrections
guysenpai Aug 5, 2026
b11ed40
test(bench): add the character controller throughput bench
guysenpai Aug 5, 2026
7d332fe
docs(claude-md): update for M1.1.12
guysenpai Aug 5, 2026
2b93741
docs(brief): close M1.1.12
guysenpai Aug 5, 2026
4d288b5
fix(forge): close six review findings on the character controller
guysenpai Aug 5, 2026
d740011
test(forge): pin the six findings and a seventh found while probing
guysenpai Aug 5, 2026
5110a67
docs(brief): record the gate G round and hold the closure artifacts
guysenpai Aug 5, 2026
604fcec
fix(forge): cap the character's push per body, not per contact
guysenpai Aug 5, 2026
969e93d
test(forge): pin the push ceiling and report the seventh stopped
guysenpai Aug 5, 2026
e09898b
docs(forge): document the exact-tangency precondition on the descriptor
guysenpai Aug 5, 2026
81fc748
docs(claude-md): update for M1.1.12
guysenpai Aug 5, 2026
626b015
docs(brief): close M1.1.12
guysenpai Aug 5, 2026
a9c62c9
fix(forge): depenetrate to the padding stand-off, not to touching
guysenpai Aug 5, 2026
bbaa8a8
test(forge): flip the tangency pin and re-derive three expectations
guysenpai Aug 5, 2026
aed8d3a
docs(brief): record the third round and correct the artifacts
guysenpai Aug 5, 2026
3bee325
fix(forge): floor the depenetration stand-off so zero padding works
guysenpai Aug 5, 2026
746193d
test(forge): pin zero padding, and a 1 km limit that is not its residual
guysenpai Aug 5, 2026
f42fbec
fix(forge): bound padding above, and give every advance the stand-off
guysenpai Aug 5, 2026
3af40a5
test(forge): pin the padding bound and add the descriptor domain table
guysenpai Aug 5, 2026
9b828bf
fix(forge): scope the stand-off floor, and drop the padding guard
guysenpai Aug 5, 2026
bbafa6c
test(forge): pin the floor's scope and the stall's collider-size bound
guysenpai Aug 5, 2026
cbbe979
fix(forge): a non-opposing contact no longer consumes the slide budget
guysenpai Aug 6, 2026
3700677
test(forge): flip three stall pins, and fix a vacuous invariance test
guysenpai Aug 6, 2026
068280b
docs(claude-md): update for M1.1.12
guysenpai Aug 6, 2026
0da3419
docs(brief): close M1.1.12
guysenpai Aug 6, 2026
3c16c2a
fix(forge): exclude the sub-shape inside the cast, not around it
guysenpai Aug 7, 2026
543e3ae
test(forge): split the mesh counter-test along the measured line
guysenpai Aug 7, 2026
1c91748
fix(forge): key the set-aside on the normal's sub-shape, and hold a set
guysenpai Aug 7, 2026
480c1d0
fix(forge): filter non-opposing contacts during selection, not after
guysenpai Aug 7, 2026
2c73fbf
docs(claude-md): update for M1.1.12
guysenpai Aug 7, 2026
0e12411
docs(brief): close M1.1.12
guysenpai Aug 7, 2026
82ab8d4
test(forge): assert the portable invariant on the subnormal residual
guysenpai Aug 7, 2026
a03036e
docs(brief): cite the normative rule, and record the Windows flake
guysenpai Aug 7, 2026
6e5ab01
fix(forge): classify a triangle after its cast, not on its face normal
guysenpai Aug 8, 2026
0856d8b
docs(forge): correct the d == 0 reason, and record P2's real state
guysenpai Aug 8, 2026
34f0d81
fix(forge): one frame for the mesh filter, manifold at zero distance
guysenpai Aug 8, 2026
2aebca7
test(forge): yaw the mesh corpus, rest the tangency sweeps on an edge
guysenpai Aug 8, 2026
8a7b474
docs(claude): consign the squeeze's yaw dependence, bounded by measure
guysenpai Aug 8, 2026
f5730a3
docs(brief): the Windows FileNotFound is a poisoned cache, not a flake
guysenpai Aug 8, 2026
4fd57f1
docs(brief): a second post-eviction instance, and no recurrence
guysenpai Aug 8, 2026
f3955fe
fix(forge): carry the retained contact's normal through to the slide
guysenpai Aug 8, 2026
36c6866
test(forge): pin that no yaw freezes, and consign the traced residue
guysenpai Aug 8, 2026
90dfb4a
fix(forge): the character's cast path bypassed the internal-edge fix
guysenpai Aug 8, 2026
501c241
docs(claude): the residue has a closed cause and an unnamed remainder
guysenpai Aug 8, 2026
4438a4e
test(forge): pin the internal-edge cell to its value, not to a bound
guysenpai Aug 8, 2026
94ee517
fix(forge): a contact opposes only beyond transport noise
guysenpai Aug 8, 2026
a2e7493
fix(forge): the opposing verdict must not depend on the direction's norm
guysenpai Aug 8, 2026
a9751c5
fix(forge): revoke the opposing noise band, it opened tunnelling
guysenpai Aug 9, 2026
f0ba370
docs: consign the revoked band and the commit-hygiene cost
guysenpai Aug 9, 2026
0dc9137
fix(forge): the opposing filter shares the kernel's conditioning
guysenpai Aug 9, 2026
286c30e
fix(forge): condition the cast direction once, pin the contract table
guysenpai Aug 9, 2026
77e49c2
fix(forge): the cast adapter asserts its direction, callers condition
guysenpai Aug 9, 2026
ed3a8cf
fix(forge): one reduction answers emptiness, direction and distance
guysenpai Aug 9, 2026
01162c3
fix(forge): the two cast arms consume the same direction
guysenpai Aug 9, 2026
2413585
fix(forge): an unrepresentable norm is an answer, not an infinity
guysenpai Aug 9, 2026
e92b476
fix(forge): an out-of-domain displacement is refused, never saturated
guysenpai Aug 9, 2026
cccd0eb
fix(forge): the displacement domain is the public f32 surface's
guysenpai Aug 9, 2026
3d11530
docs(forge): the displacement domain defers to the specs that own it
guysenpai Aug 9, 2026
8cab4b0
docs(forge): the Errors list names InvalidDisplacement
guysenpai Aug 9, 2026
eaf82af
fix(forge): the f32 domain is decided where it can be decided
guysenpai Aug 9, 2026
00b7d8b
docs(forge): the displacement domain is a pointer, not a paraphrase
guysenpai Aug 9, 2026
30b900a
test(forge): the denormal case passes one input at both precisions
guysenpai Aug 9, 2026
91d4341
docs(brief): an oracle judging a rounding must outrank that rounding
guysenpai Aug 9, 2026
37145e5
docs(forge): the site says the bound the guard applies
guysenpai Aug 9, 2026
69c7e5b
docs(forge): the bound is on the norm as computed, not the true one
guysenpai Aug 9, 2026
0d3ef4b
docs(forge): widening moves the rounding threshold, never removes it
guysenpai Aug 9, 2026
893d5d8
test(forge): the accepted side of the frontier is pinned too
guysenpai Aug 9, 2026
9df4ad7
test(forge): width, order and reduction form are each pinned
guysenpai Aug 9, 2026
665f301
test(forge): no value and no state on the residual cells
guysenpai Aug 10, 2026
1c29157
docs(claude-md): refresh the M1.1.12 tag row before merge
guysenpai Aug 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions CLAUDE.md

Large diffs are not rendered by default.

387 changes: 387 additions & 0 deletions bench/forge_3d_character.zig

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions bench/results/forge_3d_character.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# forge_3d kinematic character controller bench

- Build mode: ReleaseFast
- 2000 calls per mode per rep, 8 INTERLEAVED reps, best rep reported
- Anti-DCE checksum: 31917.672

| mode | ns/call | calls/s | calls per 16.67 ms frame |
|---|---|---|---|
| moveCharacter / plane | 212.0 | 4716981 | 78616 |
| moveCharacter / stairs | 2235.5 | 447327 | 7455 |
| moveCharacter / wall | 1764.5 | 566733 | 9446 |
| moveCharacter / mesh floor | 7979.0 | 125329 | 2089 |
| resizeCharacter | 203.0 | 4926108 | 82102 |

**Reported, not gated.** No envelope is pre-registered: this is the first measurement of
this path, and registering a bound before measuring its baseline 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 or a scheduling burst lands on all five rather than on
whichever happened to be measured while it passed. Best-of-N per mode cannot resolve a gap
under about 5 %, which is the size of the gaps between these rows.

The five rows are five different code paths, not five scales of one: the plane row runs the
ground sweep alone, the stairs row arms the climb's three sweeps, the wall row arms the
slide, the mesh row puts the ground sweep and the contact fallback on a `.triangle_soup`
shape, and `resizeCharacter` is the only row that allocates.
2,903 changes: 2,903 additions & 0 deletions briefs/M1.1.12-character-controller.md

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,34 @@ pub fn build(b: *std.Build) void {
);
forge_mesh_bench_step.dependOn(&forge_mesh_bench_run.step);

// -------------------------------- M1.1.12 forge character controller bench --
//
// `moveCharacter` on a plane / stairs / a wall / a triangle mesh, plus
// `resizeCharacter`, INTERLEAVED across reps. Writes
// `bench/results/forge_3d_character.md`. REPORTED, not gated — no envelope is
// pre-registered (bench header).
const forge_char_bench_module = b.createModule(.{
.root_source_file = b.path("bench/forge_3d_character.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
forge_char_bench_module.addImport("forge_3d", forge_3d_module);
forge_char_bench_module.addImport("weld_forge", forge_api_module);
const forge_char_bench_exe = b.addExecutable(.{
.name = "forge-character-bench",
.root_module = forge_char_bench_module,
});
b.installArtifact(forge_char_bench_exe);
const forge_char_bench_run = b.addRunArtifact(forge_char_bench_exe);
forge_char_bench_run.step.dependOn(b.getInstallStep());
if (b.args) |args| forge_char_bench_run.addArgs(args);
const forge_char_bench_step = b.step(
"bench-forge-character",
"Run the M1.1.12 forge character controller bench (five paths interleaved, writes bench/results/forge_3d_character.md)",
);
forge_char_bench_step.dependOn(&forge_char_bench_run.step);

// -------------------------------------- M1.0.5 scene loader bench --------
//
// `loadFromBytes` on a ~10k-entity image synthesized in-bench via the
Expand Down
11 changes: 11 additions & 0 deletions src/foundation/math/math.zig
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ pub const CrossOutcome = vec.CrossOutcome;
/// Whether a triangle is EXACTLY flat, decided in integer arithmetic. THE classifier: callers that
/// must not admit a flat triangle ask this, never `triangleCross`.
pub const triangleIsFlat = @import("exact.zig").triangleIsFlat;
/// A vector PARALLEL to the exact area vector of three points, or `null` when that vector is
/// EXACTLY zero — the exact tier `triangleIsFlat` is written on, exposed for callers that need the
/// DIRECTION together with that exact null.
///
/// Re-exported at M1.1.12 for its second consumer: the character controller's edge slide, whose
/// direction is `n₁ × n₂` and whose `null` must mean "the two normals are exactly parallel" and
/// nothing weaker. The tiered `triangleCross` cannot serve there — its `.direction` comes from the
/// first float tier that forms a non-zero vector, so on exactly parallel normals it returns a
/// rounding residue that reads as a valid crease. That is the very confusion §1.11.17 records as
/// having nearly shipped.
pub const triangleCrossDirection = @import("exact.zig").triangleCrossDirection;
/// The power-of-two exponent that reduces three points below unit magnitude.
pub const pow2ReductionExponent = vec.pow2ReductionExponent;

Expand Down
35 changes: 34 additions & 1 deletion src/foundation/math/vec.zig
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,43 @@ pub fn Vec(comptime N: usize, comptime T: type) type {
/// `surfaceArea`, `rayInterval`, `inflate` and `overlapsHalfSpace` — pure vector
/// arithmetic, no threshold, no physical semantics.
pub fn normalizeScaled(self: Self) ?Self {
const both = self.unitAndLength() orelse return null;
return both.unit;
}

/// The unit direction AND the length, from ONE reduction by the largest absolute component.
///
/// **Three questions, one reduction, and that is the whole point.** A caller that asks
/// `lengthSq() == 0` to test emptiness, `@sqrt(lengthSq())` for the length and this for the
/// direction has asked three times and gets three different domains: the square UNDERFLOWS for
/// a denormal vector, so a real displacement reads as empty and is dropped, and it OVERFLOWS
/// for a large one, so the length comes back infinite and poisons whatever consumes it. The
/// reduction has neither failure, and returning both quantities from it is what stops a caller
/// from reconstructing one of them the unsafe way.
///
/// `null` at EXACTLY zero — the largest absolute component is zero exactly when all three are —
/// which is the emptiness test the caller needs, exact and without a threshold.
///
/// **`length` is itself optional, because a DIRECTION always exists where a LENGTH need not.**
/// The reduction protects the intermediate square, but the final product `largest · ‖reduced‖`
/// can still leave the range: two components at `0.75 · floatMax` have a norm of about
/// `1.06 · floatMax`, which is not representable at either precision. Answering `inf` there
/// would be answering a number that is not the length, and it propagated — measured, it became
/// an infinite sweep bound and tripped a kernel's finiteness assert one call later.
///
/// So the unrepresentable case is an ANSWER this function gives, not an assumption each caller
/// re-derives. The direction is unaffected: it is scale-free by construction, which is exactly
/// why the two are returned separately rather than as one vector.
pub fn unitAndLength(self: Self) ?struct { unit: Self, length: ?T } {
const largest = @reduce(.Max, @abs(self.data));
if (largest == 0) return null;
const reduced: Self = .{ .data = self.data / @as(Simd, @splat(largest)) };
return reduced.scale(1 / reduced.length());
const reduced_length = reduced.length();
const full = largest * reduced_length;
return .{
.unit = reduced.scale(1 / reduced_length),
.length = if (std.math.isFinite(full)) full else null,
};
}

/// Largest absolute component. Zero exactly when every component is zero.
Expand Down
18 changes: 17 additions & 1 deletion src/modules/forge/api/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ pub const EntityId = types.EntityId;
pub const BodyId = types.BodyId;
/// Opaque collision-shape handle (`u32`, same packing as `BodyId`).
pub const ShapeId = types.ShapeId;
/// The `index:24 | generation:8` packing shared by `BodyId`/`ShapeId`.
/// Opaque character-controller handle (`u32`, same packing as `BodyId`).
pub const CharacterId = types.CharacterId;
/// The `index:24 | generation:8` packing shared by `BodyId`, `ShapeId` and
/// `CharacterId`, and the `PackedId.dead` all-ones reservation those three use for
/// "no handle".
pub const PackedId = types.PackedId;
/// Simulation class of a body (static / kinematic / dynamic).
pub const BodyType = types.BodyType;
Expand All @@ -44,6 +48,18 @@ pub const BodyDescriptor = types.BodyDescriptor;
/// Physics pose (position + rotation, no scale).
pub const Transform = types.Transform;

// --- Character controller (`engine-physics-forge.md` §1.12) ---

/// Everything needed to create one character controller. A controller is VIRTUAL —
/// it takes part in no solver pass, and its pose is written by `moveCharacter` alone.
pub const CharacterDescriptor = types.CharacterDescriptor;
/// The TERNARY ground verdict. Zig mirror of the Etch enum owned by
/// `engine-movement.md` §2 — same order, same values.
pub const GroundState = types.GroundState;
/// Result of one `moveCharacter`: the resolved BASE position plus the five ground
/// quantities, of which `ground_state` is the discriminator.
pub const CharacterMoveResult = types.CharacterMoveResult;

// --- Queries (the complete frozen family, `engine-tier-interfaces.md` §1) ---

/// Number of object layers a query mask can address; `addBody` rejects a body
Expand Down
Loading
Loading