Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
98563fb
docs(brief): add m1.1.3-hf-epa-frame-dependence milestone brief
guysenpai Jul 23, 2026
ac4b1dd
docs(brief): confirm specs read for m1.1.3-hf-epa-frame-dependence
guysenpai Jul 23, 2026
4212066
docs(brief): activate m1.1.3-hf-epa-frame-dependence
guysenpai Jul 23, 2026
ce7920b
test(forge): add EPA order-dependence reproduction suite (E1)
guysenpai Jul 23, 2026
2f8bc8a
docs(brief): journal E1 RED evidence for m1.1.3-hf-epa-frame-dependence
guysenpai Jul 23, 2026
450b5e5
test(forge): gate E1 RED pins so the suite stays green pre-push
guysenpai Jul 23, 2026
f8d6d2b
docs(brief): journal E1 push reconciliation (RED pins gated)
guysenpai Jul 23, 2026
38a8c9b
docs(brief): journal update
guysenpai Jul 23, 2026
6adfec4
fix(forge): epa deep-path robustness + diagnostics (E2)
guysenpai Jul 23, 2026
bebf1fb
docs(brief): journal update
guysenpai Jul 23, 2026
556641c
fix(forge): epa terminalFace skip-awareness + conditional pin (E2)
guysenpai Jul 23, 2026
c9b561e
docs(brief): journal update
guysenpai Jul 23, 2026
81eaf49
fix(forge): intrinsic point-segment degenerate normal (E3)
guysenpai Jul 23, 2026
b8d02fa
docs(brief): journal update
guysenpai Jul 23, 2026
b4370b3
test(forge): assert S3 bit-negation at manifold level (E3)
guysenpai Jul 23, 2026
9cc38d9
docs(brief): journal update
guysenpai Jul 23, 2026
54c0c24
fix(forge): epa skip-remap + terminalFace plane restriction (E4)
guysenpai Jul 23, 2026
f0fbb9d
test(forge): bilateral-tie SAT classifier in oracle (E4)
guysenpai Jul 23, 2026
2b4a699
test(forge): pin RD-4 gjk deep/shallow stall (gated)
guysenpai Jul 23, 2026
559388d
docs(brief): journal update
guysenpai Jul 23, 2026
d451324
fix(forge): gjk deep band closes RD-4 deep/shallow stall (E4)
guysenpai Jul 23, 2026
07d8d39
test(forge): un-gate order-equivalence sweep + epa diagnostics (E4a)
guysenpai Jul 23, 2026
93e372d
test(forge): narrow genericConsistent to SAT-tie or count-only (E4b)
guysenpai Jul 23, 2026
d7725bc
test(forge): q/-q + collidePair deep boundary pins (E4cd)
guysenpai Jul 23, 2026
6129837
docs: patch CLAUDE.md for the M1.1.3-HF close (E4e)
guysenpai Jul 23, 2026
f5c011b
docs(brief): close m1.1.3-hf-epa-frame-dependence
guysenpai Jul 23, 2026
ba375cb
test(forge): two-leg q/-q double-cover + unconditional generic checks
guysenpai Jul 23, 2026
05f3969
docs(forge): align GjkResult deep-band doc with rd-4
guysenpai Jul 23, 2026
e54fff5
docs(brief): journal e4-stop corrections + rd-6, re-close
guysenpai Jul 23, 2026
d23196a
docs: sync CLAUDE.md next-milestone line
guysenpai Jul 23, 2026
6b686f7
test(forge): pin rd-4 in-band false-deep benignity
guysenpai Jul 23, 2026
5208b08
docs(forge): sync rd-4 deep-band prose in gjk/epa docs
guysenpai Jul 23, 2026
e23dbd7
docs: sync CLAUDE.md M1.1.2 rd-4 deep-band prose
guysenpai Jul 23, 2026
759ffeb
docs(brief): journal light-stop round, test count 127
guysenpai Jul 23, 2026
2413f55
test(forge): exercise rd-4 band via inflated-box false-deep leg
guysenpai Jul 24, 2026
aa4215a
docs(forge): state inflated-touch shallow iff r_sum > contact_margin
guysenpai Jul 24, 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
14 changes: 7 additions & 7 deletions CLAUDE.md

Large diffs are not rendered by default.

204 changes: 204 additions & 0 deletions briefs/m1.1.3-hf-epa-frame-dependence.md

Large diffs are not rendered by default.

585 changes: 488 additions & 97 deletions src/modules/forge/forge_3d/pipeline/narrowphase/epa.zig

Large diffs are not rendered by default.

58 changes: 48 additions & 10 deletions src/modules/forge/forge_3d/pipeline/narrowphase/gjk.zig
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,12 @@ pub const max_gjk_iterations: u32 = 32;
/// - `.separated` / `.shallow`: `distance` (core distance) and `closest_a` /
/// `closest_b` (the closest points on each core, **world** space) are valid;
/// `simplex_count` is 0.
/// - `.deep`: `simplex[0..simplex_count]` is the terminal origin-enclosing
/// simplex — the EPA seed for M1.1.3; `distance` is 0 and the closest points
/// are unspecified. No depth / normal is computed here (that is M1.1.3).
/// - `.deep`: `simplex[0..simplex_count]` is the terminal simplex — either an
/// origin-ENCLOSING simplex, OR (M1.1.3-HF RD-4) a terminal within the
/// accumulated-rounding band of the origin (a Minkowski witness at noise
/// distance from the origin, NOT necessarily enclosing) — the EPA seed;
/// `distance` is 0 and the closest points are unspecified. No depth / normal is
/// computed here (that is M1.1.3 / EPA).
pub fn GjkResult(comptime T: type) type {
return struct {
const Vec3T = math.Vec(3, T);
Expand All @@ -331,7 +334,8 @@ pub fn GjkResult(comptime T: type) type {
closest_a: Vec3T,
/// Closest point on B's core, world space (`.separated`/`.shallow`).
closest_b: Vec3T,
/// Terminal origin-enclosing simplex (`.deep`; entries `[0..simplex_count]`).
/// Terminal `.deep` simplex — origin-enclosing OR the RD-4 rounding-band
/// terminal (not necessarily enclosing); entries `[0..simplex_count]`.
simplex: [4]Vertex,
/// Number of valid `simplex` entries (`.deep`: 1..4; otherwise 0).
simplex_count: u8,
Expand All @@ -348,10 +352,17 @@ pub fn GjkResult(comptime T: type) type {
///
/// Classification (brief Notes): if the terminal simplex encloses the origin the
/// cores intersect → `.deep`; otherwise the converged core distance `dist` gives
/// `.separated` iff `dist − (r_a + r_b)` exceeds the contact margin (an absolute
/// float-noise margin `conv_k · floatEps(T) · coordScale`, see the tolerance
/// block), else `.shallow`. An exact inflated touch (`dist == r_a + r_b`), and a
/// separation within that noise margin, both count as shallow.
/// `.deep` iff `dist ≤ the contact margin` (the RD-4 witness band, m1.1.3-hf — a
/// Minkowski point at noise distance from the origin; the terminal is NOT
/// necessarily enclosing), `.separated` iff `dist − (r_a + r_b)` exceeds it, else
/// `.shallow`. The contact margin is an absolute float-noise bound
/// `conv_k · floatEps(T) · coordScale` (see the tolerance block). An exact inflated
/// touch (`dist == r_a + r_b`) stays shallow iff `r_sum > contact_margin` — the
/// RD-4 band is evaluated FIRST, so a sub-noise inflation radius
/// (`0 < r_sum <= contact_margin`) classifies deep; benign either way: EPA clamps
/// depth to ~0 and the manifold penetration is ~`r_sum` in both regimes. For hard
/// cores (`r_sum == 0`) the shallow band is empty — an exact touch (`dist == 0`) is
/// the RD-4 deep band.
pub fn gjk(
comptime T: type,
shape_a: support.SupportShape(T),
Expand Down Expand Up @@ -492,11 +503,38 @@ pub fn gjk(
// independent — unlike the terminal simplex's A-frame support magnitude,
// which after cancellation reflects only who is A and made a tangency read
// `.separated` in one order and `.shallow` in the other (P1c). The comparison
// is additive on the already-computed `dist`; the frozen convention keeps an
// exact inflated touch (`dist == r_sum`) shallow.
// is additive on the already-computed `dist`. It is reached only after the
// RD-4 band below (`dist <= contact_margin`) did not fire, so the frozen
// convention keeps an exact inflated touch (`dist == r_sum`) shallow exactly
// when `r_sum > contact_margin` — a sub-noise `r_sum` is caught deep above.
const r_sum = shape_a.radius + shape_b.radius;
const coord_scale = pos_b.sub(pos_a).length() + coreExtent(T, shape_a) + coreExtent(T, shape_b);
const contact_margin = conv_k * std.math.floatEps(T) * coord_scale;
// RD-4 — deep band (m1.1.3-hf, C′). A terminal within `contact_margin` of the
// ORIGIN is a POSITIVE witness of enclosure: a Minkowski point at noise
// distance from the origin ⇒ the cores touch to measurement precision ⇒ the
// deep regime by definition. This holds at EVERY loop exit — the progress-test
// break, the anti-cycling duplicate break, and the iteration bound — so a
// single post-loop test covers all three (a convergence stall can leave a
// NON-enclosing terminal ~2.66·floatEps·scale from the origin, above the
// `degenerateOriginReached` noise floor, which then read `.shallow` at dist ≈ 0
// on a genuinely-deep overlap: the arm64 f32-unit / f64-×0.01 stall). The
// margin REUSES `contact_margin` in its existing role — the accumulated
// `dist`-rounding bound — applied to the origin-side boundary of the SAME
// classification; `noise_k` (the distinct in-loop point-noise floor) is
// UNTOUCHED, so the P1b/P2 calibration is preserved. `coord_scale` is symmetric
// under an A/B swap by construction (see above), so the band is
// order-independent. Three bands result: `[0, m]` deep, `(m, r_sum + m]`
// shallow, beyond `r_sum + m` separated — the inflated touch `dist == r_sum`
// stays shallow IFF `r_sum > m` (the `[0, m]` deep band is checked FIRST, so a
// sub-noise radius `0 < r_sum <= m` lands the touch in the deep band); for
// `r_sum == 0` the `[0, m]` band absorbs `dist == 0` (hard cores want EPA's MTV,
// not a pen ≈ 0 witness). Benign either way — EPA clamps depth to ≈ 0 and the
// manifold penetration is ≈ r_sum in both regimes. A false-deep on a true
// near-touch (`dist ∈ (0, m]`, cores actually disjoint) seeds EPA from a
// non-enclosing terminal → depth clamps to 0 → manifold penetration
// `r_sum − 0`, the former shallow result to within ε — safe.
if (dist <= contact_margin) return deepResult(T, verts, count);
return .{
.status = if (dist - r_sum > contact_margin) Res.Status.separated else Res.Status.shallow,
.distance = dist,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ pub fn collideOrderedGeneric(
var closest_b: Vec3T = undefined;
var base_penetration: T = undefined;
if (g.status == .deep) {
const e = epa_mod.epa(T, shape_a, pos_a, rot_a, relpose, shape_b, g);
const e = epa_mod.epa(T, shape_a, pos_a, rot_a, relpose, shape_b, rot_b, g, null);
n_world = e.normal;
closest_a = e.closest_a;
closest_b = e.closest_b;
Expand Down
3 changes: 3 additions & 0 deletions src/modules/forge/forge_3d/pipeline/narrowphase/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ pub const gjk = gjk_mod.gjk;

/// EPA penetration result (world normal A→B, core depth, world closest points).
pub const EpaResult = epa_mod.EpaResult;
/// Optional per-call EPA diagnostics (exit kind, iterations, skips, fallback) —
/// a test/tooling seam, not part of the frozen `EpaResult` contract.
pub const EpaDiagnostics = epa_mod.EpaDiagnostics;
/// The EPA expansion iteration ceiling (scalar-independent).
pub const max_epa_iterations = epa_mod.max_epa_iterations;
/// EPA over a `.deep` GJK seed — penetration axis + core depth.
Expand Down
5 changes: 3 additions & 2 deletions src/modules/forge/forge_3d/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ pub const ContactPoint = narrowphase.ContactPoint(Real);

/// EPA over a `.deep` GJK seed — penetration axis + core depth. The `Real`-bound
/// entry; `collide` runs it internally on the deep path.
pub fn epa(shape_a: SupportShape, pos_a: Vec3r, rot_a: Quatr, relpose: RelativePose, shape_b: SupportShape, seed: GjkResult) EpaResult {
return narrowphase.epa(Real, shape_a, pos_a, rot_a, relpose, shape_b, seed);
pub fn epa(shape_a: SupportShape, pos_a: Vec3r, rot_a: Quatr, relpose: RelativePose, shape_b: SupportShape, rot_b: Quatr, seed: GjkResult) EpaResult {
return narrowphase.epa(Real, shape_a, pos_a, rot_a, relpose, shape_b, rot_b, seed, null);
}

/// Full narrowphase (GJK → shallow/deep contact manifold) between two support
Expand Down Expand Up @@ -159,4 +159,5 @@ comptime {
_ = @import("tests/epa_test.zig");
_ = @import("tests/manifold_test.zig");
_ = @import("tests/fast_paths_test.zig");
_ = @import("tests/epa_robustness_test.zig");
}
Loading