code wiki / _hdl_build / nx_collide2d.nx
nx_collide2d.nx source
↩ module page · 128 lines · 5723 B
1// nx_collide2d.nx -- certified composable part: CONTINUOUS (swept) 2D collision with slide response
2// and impulse integration. The gamebench board's own bar for a physics-collision HAVE names exactly
3// three absences (nx_gamebench.nx bit-5 comment): swept/continuous collision, slide-along-surface
4// response, velocity/impulse integration -- this part is those three, gated and mutation-proven.
5// LIB, no main (ecosystem convention, cf. nx_gamesave/nx_swgpu). Integer-only: positions/velocities in
6// plain i64 world units, time-of-impact as a Q8 fraction of one step. Deterministic by construction.
7// Restitution is a Q8 PARAMETER (rule 11: coefficients are data, never buried constants); rest=0 gives
8// pure slide (normal component killed, tangent PRESERVED), rest=C2_Q gives a perfect bounce.
9// license_tier: ORIGINAL expect_exit: 0
10import "nx_syscalls.nx"
11const C2_Q: i64 = 256 // Q8 fixed-point denominator for step fractions + restitution
12const C2_MAXIT: i64 = 4 // contact resolutions per step (corner = 2 walls in one step)
13const C2_CKMOD: i64 = 1000000007 // rolling-checksum modulus (bounded, overflow-free)
14
15// ---- swept point-vs-inflated-AABB (Minkowski: circle of radius r vs box == point vs box grown by r).
16// walls = flat rows [x0,y0,x1,y1]; (vx,vy) = the full displacement THIS substep.
17// Returns Q8 time of FIRST impact in [0..C2_Q), or C2_Q if the path is free.
18// Writes the contact normal (axis-aligned, unit) to nrm[0..1].
19func c2_sweep(px: i64, py: i64, vx: i64, vy: i64, r: i64, walls: *i64, nw: i64, nrm: *i64) -> i64 {
20 var best: i64 = C2_Q
21 nrm[0] = 0
22 nrm[1] = 0
23 var w: i64 = 0
24 while w < nw {
25 let bx0: i64 = walls[w*4] - r
26 let by0: i64 = walls[w*4+1] - r
27 let bx1: i64 = walls[w*4+2] + r
28 let by1: i64 = walls[w*4+3] + r
29 var ok: i64 = 1
30 var t0: i64 = 0
31 var t1: i64 = C2_Q
32 var n0x: i64 = 0
33 var n0y: i64 = 0
34 // X slab
35 if vx == 0 {
36 if px < bx0 { ok = 0 }
37 if px > bx1 { ok = 0 }
38 } else {
39 var ta: i64 = (bx0 - px)*C2_Q/vx
40 var tb: i64 = (bx1 - px)*C2_Q/vx
41 var nsx: i64 = 0 - 1
42 if ta > tb { let tt: i64 = ta; ta = tb; tb = tt; nsx = 1 }
43 // >= not >: a contact at entry time EXACTLY 0 (touching after a slide) must still record
44 // its normal, or the second wall of a corner is silently dropped and the mover tunnels
45 // (found by T8/T5 on first run -- the gate biting its own part)
46 if ta >= t0 { if ta >= 0 { t0 = ta; n0x = nsx; n0y = 0 } }
47 if tb < t1 { t1 = tb }
48 }
49 // Y slab
50 if vy == 0 {
51 if py < by0 { ok = 0 }
52 if py > by1 { ok = 0 }
53 } else {
54 var tc: i64 = (by0 - py)*C2_Q/vy
55 var td: i64 = (by1 - py)*C2_Q/vy
56 var nsy: i64 = 0 - 1
57 if tc > td { let tu: i64 = tc; tc = td; td = tu; nsy = 1 }
58 if tc >= t0 { if tc >= 0 { t0 = tc; n0x = 0; n0y = nsy } }
59 if td < t1 { t1 = td }
60 }
61 if ok == 1 { if t0 <= t1 { if t0 < best { if t0 >= 0 {
62 // a zero normal means we started inside both slabs with no entry crossing -- not a hit
63 var hasn: i64 = 0
64 if n0x != 0 { hasn = 1 }
65 if n0y != 0 { hasn = 1 }
66 if hasn == 1 {
67 best = t0
68 nrm[0] = n0x
69 nrm[1] = n0y
70 }
71 }}}}
72 w = w + 1
73 }
74 return best
75}
76
77// ---- one simulation step: gravity impulse -> iterative swept resolve (slide + restitution).
78// st: [0]=px [1]=py [2]=vx [3]=vy [4]=r [5]=contact-count [6]=spare (pure i64 vector => gs_save-able)
79// gy = gravity impulse per step; rest = restitution Q8 (0 = pure slide, 256 = perfect bounce).
80func c2_step(st: *i64, walls: *i64, nw: i64, gy: i64, rest: i64, nrm: *i64) -> i64 {
81 st[3] = st[3] + gy
82 var rem: i64 = C2_Q
83 var it: i64 = 0
84 while it < C2_MAXIT {
85 var doit: i64 = 1
86 if rem < 1 { doit = 0 }
87 if doit == 1 {
88 let sx: i64 = st[2]*rem/C2_Q
89 let sy: i64 = st[3]*rem/C2_Q
90 var still: i64 = 1
91 if sx != 0 { still = 0 }
92 if sy != 0 { still = 0 }
93 if still == 1 {
94 rem = 0
95 } else {
96 let t: i64 = c2_sweep(st[0], st[1], sx, sy, st[4], walls, nw, nrm)
97 if t >= C2_Q {
98 st[0] = st[0] + sx
99 st[1] = st[1] + sy
100 rem = 0
101 } else {
102 st[0] = st[0] + sx*t/C2_Q
103 st[1] = st[1] + sy*t/C2_Q
104 st[5] = st[5] + 1
105 // impulse on the CONTACT AXIS only; the tangent component is PRESERVED = slide
106 if nrm[0] != 0 { st[2] = 0 - st[2]*rest/C2_Q }
107 if nrm[1] != 0 { st[3] = 0 - st[3]*rest/C2_Q }
108 // 1-unit nudge off the surface so integer truncation can never leave us embedded
109 st[0] = st[0] + nrm[0]
110 st[1] = st[1] + nrm[1]
111 rem = rem*(C2_Q - t)/C2_Q
112 }
113 }
114 }
115 it = it + 1
116 }
117 return 0
118}
119
120// bounded positive rolling checksum over the state vector (the harness's independent walker uses the
121// same shape over positions only -- transparency law: the verifier must not flow through the save path)
122func c2_ck(ck0: i64, v: i64) -> i64 {
123 var vv: i64 = v % C2_CKMOD
124 if vv < 0 { vv = vv + C2_CKMOD }
125 var ck: i64 = ck0 % C2_CKMOD
126 if ck < 0 { ck = ck + C2_CKMOD }
127 return (ck*31 + vv) % C2_CKMOD
128}