Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
432b721
docs(brief): add m1.1.9 raycast queries brief
guysenpai Jul 25, 2026
1345288
docs(brief): open m1.1.9 living section
guysenpai Jul 25, 2026
004cf52
feat(math): add aabb ray interval slab test
guysenpai Jul 25, 2026
2db8af9
docs(math): state rayInterval lane and domain contract
guysenpai Jul 25, 2026
a858be3
fix(math): reject a nan reciprocal in the rayinterval domain
guysenpai Jul 25, 2026
d2bc671
docs(brief): log the ipc crash-recovery timing finding
guysenpai Jul 25, 2026
812508e
test(ipc): assert crash-recovery behaviour, not latency
guysenpai Jul 25, 2026
3bdc224
feat(forge): add branch-and-bound ray traversal to broadphase
guysenpai Jul 25, 2026
d9ade52
feat(forge): add analytic ray-core kernels to narrowphase
guysenpai Jul 26, 2026
bfac8cf
fix(forge): reject a rounded box before the membership test
guysenpai Jul 26, 2026
94ef715
feat(forge): add real-bound raycast query entries
guysenpai Jul 26, 2026
1791bdb
fix(forge): normalise the widened body rotation at creation
guysenpai Jul 26, 2026
a217f64
feat(forge): freeze the public query family and layer domain
guysenpai Jul 26, 2026
d363763
test(forge): add the raycast query acceptance suite
guysenpai Jul 26, 2026
07e3e0d
fix(forge): condition the ray kernels and terminate any-queries
guysenpai Jul 26, 2026
d2047e6
chore(forge): add the raycast bench and close m1.1.9
guysenpai Jul 26, 2026
fb07cd9
test(forge): pin the oblique far-field bound and shuffled bench
guysenpai Jul 26, 2026
23f1a1b
test(forge): bound the far-field normal and measure order locality
guysenpai Jul 26, 2026
6b8b65a
fix(forge): normalise the ray normal and close the bench leaks
guysenpai Jul 26, 2026
fc8c684
docs(forge): apply the claude-md patch for m1.1.9
guysenpai Jul 26, 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
17 changes: 11 additions & 6 deletions CLAUDE.md

Large diffs are not rendered by default.

427 changes: 427 additions & 0 deletions bench/forge_3d_raycast.zig

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions bench/results/forge_3d_raycast.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# forge_3d raycast throughput bench

- Build mode: ReleaseFast
- Scene: 10000 STATIC bodies (spheres / boxes / capsules on a 3 m grid)
- Rays: 10000 per rep, 10 reps, best rep reported
- Anti-DCE checksum: 10635397.257

| mode | ns/ray | rays/s | rays per 16.67 ms frame | hit rate |
|---|---|---|---|---|
| closest | 746.3 | 1339944 | 22332 | 0.88 |
| any | 546.0 | 1831502 | 30525 | 0.88 |
| all (buffer 32) | 1532.4 | 652571 | 10876 | 0.88 |
| closest (5 m bound) | 386.4 | 2587992 | 43133 | 0.19 |
| closest (swept order) | 185.7 | 5385030 | 89750 | 1.00 |
| closest (swept, permuted) | 253.7 | 3941663 | 65694 | 1.00 |

**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. The structural guarantee is
the logarithmic visited-node test in the acceptance suite. The C1.1 target of
10 000 rays/frame at 60 Hz is verified at its own declared point,
`bench/physics_forge_3d_integration.zig` on the demo scene; the frame column
here is an order-of-magnitude indication on a synthetic grid.
1,110 changes: 1,110 additions & 0 deletions briefs/M1.1.9-queries-raycast.md

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,33 @@ pub fn build(b: *std.Build) void {
);
forge_np_bench_step.dependOn(&forge_np_bench_run.step);

// ------------------------------------- M1.1.9 forge raycast bench --------
//
// Closest / any / all raycast throughput over a 10 000-body STATIC scene,
// plus a short-bound variant. Writes `bench/results/forge_3d_raycast.md`.
// REPORTED, not gated — no envelope is pre-registered (bench header).
const forge_ray_bench_module = b.createModule(.{
.root_source_file = b.path("bench/forge_3d_raycast.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
forge_ray_bench_module.addImport("forge_3d", forge_3d_module);
forge_ray_bench_module.addImport("weld_forge", forge_api_module);
const forge_ray_bench_exe = b.addExecutable(.{
.name = "forge-raycast-bench",
.root_module = forge_ray_bench_module,
});
b.installArtifact(forge_ray_bench_exe);
const forge_ray_bench_run = b.addRunArtifact(forge_ray_bench_exe);
forge_ray_bench_run.step.dependOn(b.getInstallStep());
if (b.args) |args| forge_ray_bench_run.addArgs(args);
const forge_ray_bench_step = b.step(
"bench-forge-raycast",
"Run the M1.1.9 forge raycast throughput bench (closest/any/all over 10k static bodies, writes bench/results/forge_3d_raycast.md)",
);
forge_ray_bench_step.dependOn(&forge_ray_bench_run.step);

// -------------------------------------- M1.0.5 scene loader bench --------
//
// `loadFromBytes` on a ~10k-entity image synthesized in-bench via the
Expand Down
246 changes: 245 additions & 1 deletion src/foundation/math/aabb.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
//!
//! Stored as its minimum and maximum corners over 3-vectors. Face contact
//! counts as overlap / containment (inclusive `<=`), the convention broadphase
//! wants.
//! wants — and `rayInterval`, the slab test the ray traversal descends on,
//! follows the same inclusive convention.

const std = @import("std");
const vec = @import("vec.zig");
Expand Down Expand Up @@ -68,6 +69,108 @@ pub fn Aabb(comptime T: type) type {
const d = self.max.sub(self.min).toArray();
return 2 * (d[0] * d[1] + d[1] * d[2] + d[2] * d[0]);
}

/// Parametric hit interval of a ray over a box, returned by
/// `rayInterval`.
pub const RayInterval = struct {
/// Ray parameter at which the box is entered. Negative when the
/// origin is already inside.
enter: T,
/// Ray parameter at which the box is left.
exit: T,
};

/// Slab test of the ray `origin + t·direction` against this box,
/// returning the parametric interval it spans, or `null` when the ray
/// misses (an empty interval).
///
/// `inv_dir` is the componentwise reciprocal of the direction and
/// `dir_is_zero` marks the direction components that are **exactly** zero.
/// The masked lanes still take part in the product below and their result is
/// discarded, so any DEFINED value is legal there — `undefined` is not. The
/// natural caller value is what `1 / 0` yields, an infinity.
///
/// Preconditions: `origin` and both box corners are finite, and every lane of
/// `inv_dir` not marked in `dir_is_zero` is non-zero (equivalently: the
/// direction is finite). They are what makes the NaN repair below exact —
/// `0 · inf` is then the only NaN reachable, and the exact quotient of an
/// exactly-zero numerator is zero. `inf · 0`, whose repair to zero would be
/// wrong, is excluded by these preconditions.
///
/// Nothing here is clamped: `enter` may be negative when the origin lies
/// inside the box, and the caller intersects the interval with its own
/// `[0, max_distance]` window. Face contact is a hit — `enter == exit`
/// counts — matching the inclusive convention of `overlaps` and
/// `contains`.
pub fn rayInterval(
self: Self,
origin: Vec3T,
inv_dir: Vec3T,
dir_is_zero: @Vector(3, bool),
) ?RayInterval {
const Simd = @Vector(3, T);
const o = origin.data;
const lo = self.min.data;
const hi = self.max.data;

// Domain assertion at the entry, the shape of `sleep.assertDomain`
// and `assertPositionDomain`: a finite origin, and a reciprocal that
// is neither zero nor NaN on every lane the mask does not cover
// (equivalently, a finite direction — `1 / NaN` is NaN and `1 / inf`
// is zero, so both non-finite directions are caught). This is what
// excludes `inf · 0` and so makes the NaN repair below exact. An
// INFINITE reciprocal is legal and must stay so: it is the subnormal
// direction component, the very case the repair exists for. The box
// corners stay covered by the doc comment alone — they are
// engine-produced, and an assert per visited node would cost for
// nothing.
std.debug.assert(@reduce(.And, @abs(o) < @as(Simd, @splat(std.math.inf(T)))));
std.debug.assert(@reduce(.And, dir_is_zero |
((inv_dir.data != @as(Simd, @splat(0))) & (inv_dir.data == inv_dir.data))));

// A direction component that is EXACTLY zero makes the ray parallel
// to that pair of slab planes. The axis leaves the product and is
// replaced by the face-inclusive containment test of the origin in
// that slab: parallel and inside constrains nothing, parallel and
// outside is a miss on its own.
//
// The guard is at TRUE ZERO and carries no epsilon. Its purpose is
// to keep `0 · inf` out of the product, and the exact-zero branch
// achieves that without introducing a geometric constant — the
// reference's absolute `1.0e-20f` parallel guard is deliberately
// not reproduced (`engine-physics-forge.md` §1.11.2).
const outside_slab = (o < lo) | (o > hi);
if (@reduce(.Or, dir_is_zero & outside_slab)) return null;

const t_lo = (lo - o) * inv_dir.data;
const t_hi = (hi - o) * inv_dir.data;

// `0 · inf` is the one NaN this product can produce, and the mask
// above does not cover every way in: a direction component small
// enough that its reciprocal overflows to infinity is NOT exactly
// zero, and an origin sitting exactly on that axis' face plane makes
// the numerator exactly zero. Repairing that NaN to zero is not a
// tolerance — the numerator IS zero, so the exact quotient is zero.
// Left unrepaired, `@min`/`@max` return the other operand and the
// lane silently loses the box.
const zeros: Simd = @splat(0);
const a = @select(T, t_lo != t_lo, zeros, t_lo);
const b = @select(T, t_hi != t_hi, zeros, t_hi);

// The parallel axes are neutralised by an unbounded interval, so the
// reductions below see only the axes that actually constrain.
const unbounded_lo: Simd = @splat(-std.math.inf(T));
const unbounded_hi: Simd = @splat(std.math.inf(T));
const near = @select(T, dir_is_zero, unbounded_lo, @min(a, b));
const far = @select(T, dir_is_zero, unbounded_hi, @max(a, b));

const enter = @reduce(.Max, near);
const exit = @reduce(.Min, far);
// Strict `>`: `enter == exit` is a single-parameter graze, a hit
// under the same inclusive convention as `overlaps`/`contains`.
if (enter > exit) return null;
return .{ .enter = enter, .exit = exit };
}
};
}

Expand Down Expand Up @@ -120,6 +223,147 @@ test "generic Aabb f64 instantiation compiles" {
try testing.expect(!box.contains(V.fromArray(.{ 3, 0, 0 })));
}

/// Ray parameters as `rayInterval` wants them: the reciprocal direction and the
/// exactly-zero mask, derived from a direction the way a caller does.
fn rayParams(comptime T: type, direction: vec.Vec(3, T)) struct { inv: vec.Vec(3, T), zero: @Vector(3, bool) } {
const one_v: @Vector(3, T) = @splat(1);
return .{
.inv = .{ .data = one_v / direction.data },
.zero = direction.data == @as(@Vector(3, T), @splat(0)),
};
}

test "rayInterval: entry and exit on a centred box" {
// Box [-1,1]×[-2,2]×[-3,3], ray from (-4,-4,-4) along (1,1,1):
// x slab [3, 5], y slab [2, 6], z slab [1, 7]
// → enter = max(3,2,1) = 3, exit = min(5,6,7) = 5.
const box = Aabbf.fromMinMax(Vec3.fromArray(.{ -1, -2, -3 }), Vec3.fromArray(.{ 1, 2, 3 }));
const origin = Vec3.splat(-4);
const p = rayParams(f32, Vec3.one);

const hit = box.rayInterval(origin, p.inv, p.zero) orelse return error.TestExpectedHit;
try testing.expectEqual(@as(f32, 3), hit.enter);
try testing.expectEqual(@as(f32, 5), hit.exit);

// A ray pointing away from the box spans a negative interval, which is
// still a valid (non-empty) interval — clamping to `[0, max_distance]` is
// the caller's job, not this one's.
const away = rayParams(f32, Vec3.one.neg());
const behind = box.rayInterval(origin, away.inv, away.zero) orelse return error.TestExpectedHit;
try testing.expectEqual(@as(f32, -5), behind.enter);
try testing.expectEqual(@as(f32, -3), behind.exit);

// A ray that misses: parallel offsets aside, this one passes beside the box.
const miss_origin = Vec3.fromArray(.{ -4, 10, -4 });
try testing.expect(box.rayInterval(miss_origin, p.inv, p.zero) == null);
}

test "rayInterval: origin inside yields a negative entry" {
// Unit box centred on the origin, ray from the centre along (1,2,3):
// x slab [-1, 1], y slab [-0.5, 0.5], z slab [-1/3, 1/3].
const box = Aabbf.fromMinMax(Vec3.splat(-1), Vec3.one);
const p = rayParams(f32, Vec3.fromArray(.{ 1, 2, 3 }));

const hit = box.rayInterval(Vec3.zero, p.inv, p.zero) orelse return error.TestExpectedHit;
try testing.expect(hit.enter < 0);
try testing.expect(hit.exit > 0);
try testing.expectApproxEqAbs(@as(f32, -1.0 / 3.0), hit.enter, 1e-6);
try testing.expectApproxEqAbs(@as(f32, 1.0 / 3.0), hit.exit, 1e-6);
}

test "rayInterval: a zero direction component falls back to containment" {
// Unit box [0,1]³, ray along +X only: the Y and Z axes are exactly parallel
// to their slabs and leave the product, replaced by containment of the
// origin in those slabs. No epsilon is involved on either side.
const box = Aabbf.fromMinMax(Vec3.zero, Vec3.one);
const p = rayParams(f32, Vec3.unit_x);
try testing.expect(p.zero[1] and p.zero[2] and !p.zero[0]);

// Origin inside both parallel slabs → hit, x slab [1, 2].
const inside = box.rayInterval(Vec3.fromArray(.{ -1, 0.5, 0.5 }), p.inv, p.zero) orelse
return error.TestExpectedHit;
try testing.expectEqual(@as(f32, 1), inside.enter);
try testing.expectEqual(@as(f32, 2), inside.exit);

// Origin outside one parallel slab → miss, whatever the other axes say.
try testing.expect(box.rayInterval(Vec3.fromArray(.{ -1, 1.5, 0.5 }), p.inv, p.zero) == null);
try testing.expect(box.rayInterval(Vec3.fromArray(.{ -1, 0.5, -0.001 }), p.inv, p.zero) == null);

// Origin exactly ON a face plane of a parallel slab → hit (containment is
// face-inclusive). This is the case the true-zero branch exists for: on the
// reciprocal path the numerator is exactly zero and `inv` is infinite, so
// the product would be NaN.
const on_min = box.rayInterval(Vec3.fromArray(.{ -1, 0, 0.5 }), p.inv, p.zero) orelse
return error.TestExpectedHit;
try testing.expectEqual(@as(f32, 1), on_min.enter);
try testing.expectEqual(@as(f32, 2), on_min.exit);

const on_max = box.rayInterval(Vec3.fromArray(.{ -1, 1, 0.5 }), p.inv, p.zero) orelse
return error.TestExpectedHit;
try testing.expectEqual(@as(f32, 1), on_max.enter);
try testing.expectEqual(@as(f32, 2), on_max.exit);
}

test "rayInterval: face-grazing is inclusive" {
const box = Aabbf.fromMinMax(Vec3.zero, Vec3.one);

// A ray exactly along the +Y = 1 face plane, travelling +X: it grazes the
// face for the whole crossing. Inclusive convention → hit.
const along_face = rayParams(f32, Vec3.unit_x);
const grazed = box.rayInterval(Vec3.fromArray(.{ -1, 1, 0.5 }), along_face.inv, along_face.zero) orelse
return error.TestExpectedHit;
try testing.expectEqual(@as(f32, 1), grazed.enter);
try testing.expectEqual(@as(f32, 2), grazed.exit);

// A ray through the (1,1,·) edge touches the box on a single parameter:
// x slab [0, 1] from origin (0,2,0.5) along (1,-1,0), y slab [1, 2]
// → enter == exit == 1, a degenerate interval that still counts.
const corner = rayParams(f32, Vec3.fromArray(.{ 1, -1, 0 }));
const touch = box.rayInterval(Vec3.fromArray(.{ 0, 2, 0.5 }), corner.inv, corner.zero) orelse
return error.TestExpectedHit;
try testing.expectEqual(@as(f32, 1), touch.enter);
try testing.expectEqual(touch.enter, touch.exit);
}

test "rayInterval: a subnormal direction component on a face plane still hits" {
// `dir_is_zero` covers the EXACTLY zero component. A component small enough
// that its reciprocal overflows to infinity — but non-zero, so not masked —
// reaches the reciprocal path, and an origin sitting exactly on that axis'
// face plane makes the numerator exactly zero: `0 · inf = NaN`. The exact
// quotient of an exactly-zero numerator is zero, and the box IS hit; a NaN
// dropped by `@min`/`@max` would turn this into a silent miss.
const box = Aabbf.fromMinMax(Vec3.zero, Vec3.one);
const tiny = std.math.floatTrueMin(f32);
const p = rayParams(f32, Vec3.fromArray(.{ 1, tiny, 0 }));
try testing.expect(!p.zero[1]); // non-zero: the mask does not cover it
try testing.expect(std.math.isInf(p.inv.data[1])); // yet the reciprocal is infinite

const hit = box.rayInterval(Vec3.fromArray(.{ -1, 0, 0.5 }), p.inv, p.zero) orelse
return error.TestExpectedHit;
try testing.expectEqual(@as(f32, 1), hit.enter);
try testing.expectEqual(@as(f32, 2), hit.exit);
}

test "rayInterval: f64 instantiation matches the f32 closed form" {
const A = Aabb(f64);
const V = vec.Vec(3, f64);
const box = A.fromMinMax(V.fromArray(.{ -1, -2, -3 }), V.fromArray(.{ 1, 2, 3 }));
const p = rayParams(f64, V.one);

const hit = box.rayInterval(V.splat(-4), p.inv, p.zero) orelse return error.TestExpectedHit;
try testing.expectEqual(@as(f64, 3), hit.enter);
try testing.expectEqual(@as(f64, 5), hit.exit);

// The zero-direction fallback and its face-inclusive edge, at f64.
const unit = A.fromMinMax(V.zero, V.one);
const axis = rayParams(f64, V.unit_x);
const on_face = unit.rayInterval(V.fromArray(.{ -1, 0, 0.5 }), axis.inv, axis.zero) orelse
return error.TestExpectedHit;
try testing.expectEqual(@as(f64, 1), on_face.enter);
try testing.expectEqual(@as(f64, 2), on_face.exit);
try testing.expect(unit.rayInterval(V.fromArray(.{ -1, 1.5, 0.5 }), axis.inv, axis.zero) == null);
}

test "surfaceArea" {
// 2 × 3 × 4 box → 2·(2·3 + 3·4 + 4·2) = 2·(6 + 12 + 8) = 52.
const box = Aabbf.fromMinMax(Vec3.zero, Vec3.fromArray(.{ 2, 3, 4 }));
Expand Down
21 changes: 21 additions & 0 deletions src/modules/forge/api/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,27 @@ pub const BodyDescriptor = types.BodyDescriptor;
/// Physics pose (position + rotation, no scale).
pub const Transform = types.Transform;

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

/// Number of object layers a query mask can address; `addBody` rejects a body
/// beyond this domain with `error.InvalidCollisionLayer`.
pub const collision_layer_count = types.collision_layer_count;
/// Filtering shared by the whole query family (object-layer mask + exclusions).
/// Distinct from the `QueryFilter` of §6 `AIModule`.
pub const PhysicsQueryFilter = types.PhysicsQueryFilter;
/// A world-space ray query.
pub const RaycastQuery = types.RaycastQuery;
/// A cast of an arbitrary shape (one entry for sphere / box / capsule).
pub const ShapeCastQuery = types.ShapeCastQuery;
/// An overlap test of an arbitrary shape.
pub const OverlapQuery = types.OverlapQuery;
/// One ray hit.
pub const RaycastHit = types.RaycastHit;
/// One shape-cast hit (two sub-shapes: the cast one and the one hit).
pub const ShapeCastHit = types.ShapeCastHit;
/// Result of `closestPoint`.
pub const ClosestPointResult = types.ClosestPointResult;

// Pins so the inline tests in the sub-files are analysed when this module is
// built as a test target (engine-zig-conventions.md §13).
comptime {
Expand Down
Loading