nx_vmotion.nx source
↩ module page · 259 lines · 13104 B
1// nx_vmotion.nx -- sovereign BLOCK MOTION COMPENSATION: the core trick VP8/H.264 use to beat plain tile-delta.
2// When the camera pans or a head moves, EVERY tile's pixels change (tile-delta would resend them all), but the
3// content just SHIFTED -- so for each block we search the PREVIOUS frame for the best-matching block (minimum
4// SAD = sum of absolute differences) and send a tiny MOTION VECTOR (dx,dy) + a small residual instead of the
5// whole block. A pan that costs tile-delta ~100% of the frame costs motion-comp a few vectors + near-zero
6// residual. Pure integer buffer ops (no FPU, no syscalls) -> wasm-friendly sender-side encode. license_tier: ORIGINAL
7
8// F1111 ADAPTIVE MOTION-SEARCH RADIUS (rule-11: measured, not hardcoded). Motion search is 80.6pct of
9// P-frame encode (V8 profile of the shipped client, 416x320/emode673/qp22) and every call site searched
10// the full R=16 window = 1089 candidates/block. MEASURED on that client: a near-only window encodes the
11// same frame in 32.6ms vs 53.6ms at the SAME bytes, but a BARE small window costs ~2.5pct bytes at
12// matched PSNR on high-motion content (bgop qp sweep) -- so scan near first and EXTEND only for blocks
13// that missed. An extended block is candidate-for-candidate identical to the full-R search (see vm_scan),
14// so this can only ever cost TIME, never quality, relative to full R.
15// VC_ME_RNEAR=0 restores the exact pre-F1111 single-pass scan. VC_ME_EXTK MUST stay > 32 (see vm_search_q).
16const VC_ME_RNEAR: i64 = 8
17const VC_ME_EXTK: i64 = 94
18// F1111 rung-2 (predictor priming) was built, proven bit-exact, MEASURED 0.999x, and REMOVED 2026-07-27:
19// the adaptive window + deadzone floor + capped SAD already exhaust the cap-tightening lever. Full
20// design, proof and numbers: nishifamily.com/video-engineering Act IV + the video lane memory file.
21
22// SAD of 16 bytes at a vs b -- THE F618 SIMD SEAM (flipped 2026-07-21). Native builds execute this
23// scalar body (the bit-exact reference); the wat backend (nx_wasm.nx) INTERCEPTS calls to it BY NAME
24// and inlines the fused wasm-SIMD sequence (v128.load x2, sub_sat_u both ways, or, extadd-pairwise,
25// lane sum) -- same integer by construction. Do not rename without updating the intercept.
26func v128_sad16(a: *u8, b: *u8) -> i64 {
27 var s: i64 = 0
28 var i: i64 = 0
29 while i < 16 {
30 let d: i64 = (a[i] as i64) - (b[i] as i64)
31 if d < 0 { s = s - d } else { s = s + d }
32 i = i + 1
33 }
34 return s
35}
36
37// SAD between cur's block at (cx,cy) and prev's block at (px,py), T x T, over W-stride 8bpp buffers.
38// Row bases hoisted out of the pixel loop (the skeleton compiler doesn't CSE the y*W multiplies;
39// this runs on EVERY block's skip decision, so the hoist pays frame-wide). Arithmetic unchanged.
40// F618: T==16 rows ride v128_sad16 (one row = one 16-byte SAD; addition is associative so the row
41// total is the same integer); other T keep the exact scalar loop.
42func vm_sad(cur: *u8, prev: *u8, W: i64, cx: i64, cy: i64, px: i64, py: i64, T: i64) -> i64 {
43 var s: i64 = 0
44 var yy: i64 = 0
45 if T == 16 {
46 while yy < 16 {
47 let crow: i64 = (cy + yy) * W + cx
48 let prow: i64 = (py + yy) * W + px
49 let ca: i64 = (cur as i64) + crow
50 let pa: i64 = (prev as i64) + prow
51 let cp: *u8 = ca as *u8
52 let pp: *u8 = pa as *u8
53 let d: i64 = v128_sad16(cp, pp)
54 s = s + d
55 yy = yy + 1
56 }
57 return s
58 }
59 while yy < T {
60 let crow: i64 = (cy + yy) * W + cx
61 let prow: i64 = (py + yy) * W + px
62 var xx: i64 = 0
63 while xx < T {
64 let d: i64 = (cur[crow + xx] as i64) - (prev[prow + xx] as i64)
65 if d < 0 { s = s - d } else { s = s + d }
66 xx = xx + 1
67 }
68 yy = yy + 1
69 }
70 return s
71}
72// vm_sad with a CAP: abort row-granularly once the partial sum reaches cap. SAD only grows, so an
73// aborted candidate returns a partial s >= cap and the caller's strict (s < best) verdict is IDENTICAL
74// to the full computation -- any true winner (< cap) never aborts. Argmin (and thus the bitstream)
75// stays BIT-EXACT while losers stop early (the 2026-07-03 speed bench measured the un-exited full
76// search at 165ms/frame = the realtime blocker this closes down).
77func vm_sad_capped(cur: *u8, prev: *u8, W: i64, cx: i64, cy: i64, px: i64, py: i64, T: i64, cap: i64) -> i64 {
78 var s: i64 = 0
79 var yy: i64 = 0
80 if T == 16 {
81 while yy < 16 {
82 let crow: i64 = (cy + yy) * W + cx
83 let prow: i64 = (py + yy) * W + px
84 let ca: i64 = (cur as i64) + crow
85 let pa: i64 = (prev as i64) + prow
86 let cp: *u8 = ca as *u8
87 let pp: *u8 = pa as *u8
88 let d: i64 = v128_sad16(cp, pp)
89 s = s + d
90 if s >= cap { return s }
91 yy = yy + 1
92 }
93 return s
94 }
95 while yy < T {
96 let crow: i64 = (cy + yy) * W + cx
97 let prow: i64 = (py + yy) * W + px
98 var xx: i64 = 0
99 while xx < T {
100 let d: i64 = (cur[crow + xx] as i64) - (prev[prow + xx] as i64)
101 if d < 0 { s = s - d } else { s = s + d }
102 xx = xx + 1
103 }
104 if s >= cap { return s }
105 yy = yy + 1
106 }
107 return s
108}
109// evaluate one candidate (dx,dy); updates best/bdx/bdy via the out array b[0]=best b[1]=bdx b[2]=bdy.
110func vm_try(cur: *u8, prev: *u8, W: i64, H: i64, cx: i64, cy: i64, T: i64, dx: i64, dy: i64, b: *i64) -> i64 {
111 let px: i64 = cx + dx
112 let py: i64 = cy + dy
113 if px >= 0 { if py >= 0 { if px + T <= W { if py + T <= H {
114 let s: i64 = vm_sad_capped(cur, prev, W, cx, cy, px, py, T, b[0])
115 if s < b[0] { b[0] = s; b[1] = dx; b[2] = dy }
116 } } } }
117 return 0
118}
119// full-coverage search of the [-R,R] window, CENTER-FIRST ring order: radius 0, then each square ring
120// outward. Real video motion is small, so the near-optimal candidate lands FIRST and the capped SAD
121// aborts every far loser after a row or two -- the full window is still 100% covered (no candidates
122// dropped; this is exact search, only reordered). Tie-break change vs the old raster order: equal-SAD
123// candidates now resolve to the SMALLEST radius = the cheapest MV to code (strictly better, and the
124// decoder is agnostic -- any coded MV reconstructs exactly). Writes mv[0]=dx mv[1]=dy; returns best SAD.
125// THE ONE ring-scan body (F1111 DRY: this was duplicated verbatim into vm_search and vm_search_q; a
126// third copy for any future variant was the trap). vfloor==0 disables the deadzone early-exit entirely
127// (vm_search semantics, exact-original); vfloor>0 arms it (vm_search_q semantics). Callers own b[] init
128// and the mv[] writeback, so both entry points keep their exact published contracts.
129// r0 = FIRST ring to enumerate. r0==0 also evaluates the (0,0) centre candidate. A scan of rings 0..A
130// followed by a scan of rings A+1..B visits EXACTLY the candidates of a single 0..B scan, in EXACTLY the
131// same order, against the same monotonically-tightening b[] -- so a two-pass (adaptive) search is
132// candidate-for-candidate IDENTICAL to the one-pass search it extends to. That identity is what makes
133// vm_search_q's adaptive form safe: extending costs quality NOTHING, only the time it would have cost.
134func vm_scan(cur: *u8, prev: *u8, W: i64, H: i64, cx: i64, cy: i64, T: i64, R: i64, b: *i64, vfloor: i64, r0: i64) -> i64 {
135 if r0 == 0 { vm_try(cur, prev, W, H, cx, cy, T, 0, 0, b) }
136 var r: i64 = r0
137 if r < 1 { r = 1 }
138 while r <= R {
139 // rings 0-2 always search fully; the floor-exit prunes only the far rings (see vm_search_q note).
140 var prune: i64 = 0
141 if vfloor > 0 { if r > 2 { if b[0] < vfloor { prune = 1 } } }
142 if prune == 1 { r = R + 1 } else {
143 // top + bottom rows of the ring (full width)
144 var dx: i64 = 0 - r
145 while dx <= r {
146 vm_try(cur, prev, W, H, cx, cy, T, dx, 0 - r, b)
147 vm_try(cur, prev, W, H, cx, cy, T, dx, r, b)
148 dx = dx + 1
149 }
150 // left + right columns (excluding the corners already done)
151 var dy: i64 = 0 - r + 1
152 while dy <= r - 1 {
153 vm_try(cur, prev, W, H, cx, cy, T, 0 - r, dy, b)
154 vm_try(cur, prev, W, H, cx, cy, T, r, dy, b)
155 dy = dy + 1
156 }
157 r = r + 1
158 }
159 }
160 return b[0]
161}
162// scr3 (2026-07-29 root fix): the 3-slot best-box [sad,dx,dy] was a sys_mmap(32) PER SEARCH CALL --
163// per MB, on the hottest path. In the wasm tier sys_mmap was the 0-stub (silent address-0 aliasing,
164// seq234 class); natively it was an unmunmapped map per MB (the map-count crash class). The caller
165// provides 3 i64 of scratch (blk is dead during search in the codec; gates hoist ONE mmap).
166func vm_search(cur: *u8, prev: *u8, W: i64, H: i64, bx: i64, by: i64, T: i64, R: i64, mv: *i64, scr3: *i64) -> i64 {
167 let cx: i64 = bx * T
168 let cy: i64 = by * T
169 let b: *i64 = scr3
170 b[0] = 2147483647
171 b[1] = 0
172 b[2] = 0
173 vm_scan(cur, prev, W, H, cx, cy, T, R, b, 0, 0)
174 mv[0] = b[1]
175 mv[1] = b[2]
176 return b[0]
177}
178// DEADZONE-FLOOR EARLY-EXIT variant (825, encoder-only; the RICH/rct8 path's searcher -- vm_search above keeps
179// the exact-original semantics for the legacy stream). Once the best SAD is under the QUANT DEADZONE scale
180// (T*T*qp/8 ~= a residual that quantizes to ~nothing), no candidate can improve the CODED outcome -- stop
181// enumerating rings. On call content the true MV lands in ring 0-2 (center-first), cutting the 289-candidate
182// walk to ~10-25 for the coherent majority. First try (qp-blind T*T*2) MEASURED +2.02% BD (bus +5.17: at low
183// qp a sub-floor better match still pays) -> qp-scaled floor keeps the noise-fps win AND the low-qp quality.
184func vm_search_q(cur: *u8, prev: *u8, W: i64, H: i64, bx: i64, by: i64, T: i64, R: i64, mv: *i64, qp: i64, scr3: *i64) -> i64 {
185 let cx: i64 = bx * T
186 let cy: i64 = by * T
187 let b: *i64 = scr3
188 b[0] = 2147483647
189 b[1] = 0
190 b[2] = 0
191 // rings 0-2 always search fully (25 candidates -- the coherent-motion near window; exiting inside it
192 // measured +2.0% BD on real-motion seqs); the floor-exit prunes only the far rings (the 264-candidate
193 // tail that exists for rare large motion).
194 let vm_floor: i64 = (T * T * qp) / 8
195 // F1111: near window first, far rings only for blocks that did not find a match inside the measured
196 // translation band. VC_ME_RNEAR==0 restores the exact single-pass R scan (the pre-F1111 behaviour).
197 var rn: i64 = VC_ME_RNEAR
198 if rn <= 0 { rn = R }
199 if rn > R { rn = R }
200 vm_scan(cur, prev, W, H, cx, cy, T, rn, b, vm_floor, 0)
201 // ⚠the extend threshold MUST stay above vm_floor (VC_ME_EXTK > 32), else a block the deadzone prune
202 // already accepted could re-enter the far rings that a single-pass scan would have skipped, and the
203 // two-pass/one-pass equivalence below would not hold.
204 let ext_thresh: i64 = (T * T * qp * VC_ME_EXTK) / 256
205 if rn < R { if b[0] >= ext_thresh { vm_scan(cur, prev, W, H, cx, cy, T, R, b, vm_floor, rn + 1) } }
206 mv[0] = b[1]
207 mv[1] = b[2]
208 return b[0]
209}
210// the residual SAD with NO motion (dx=dy=0) -- i.e. what plain tile-delta "sees" as the block's change.
211func vm_sad_zero(cur: *u8, prev: *u8, W: i64, bx: i64, by: i64, T: i64) -> i64 {
212 let cx: i64 = bx * T
213 let cy: i64 = by * T
214 return vm_sad(cur, prev, W, cx, cy, cx, cy, T)
215}
216// reconstruct cur's block into dst from prev at the motion vector + the residual (dst = predicted + residual).
217// residual[k] = cur - predicted (signed, i16-range). For SAD==0 the residual is all-zero and predicted == cur.
218func vm_reconstruct(dst: *u8, prev: *u8, residual: *i64, W: i64, bx: i64, by: i64, T: i64, mv: *i64) -> i64 {
219 let cx: i64 = bx * T
220 let cy: i64 = by * T
221 let px: i64 = cx + mv[0]
222 let py: i64 = cy + mv[1]
223 var yy: i64 = 0
224 while yy < T {
225 var xx: i64 = 0
226 while xx < T {
227 let pred: i64 = prev[(py+yy)*W + (px+xx)] as i64
228 var v: i64 = pred + residual[yy*T + xx]
229 if v < 0 { v = 0 }
230 if v > 255 { v = 255 }
231 dst[(cy+yy)*W + (cx+xx)] = v as u8
232 xx = xx + 1
233 }
234 yy = yy + 1
235 }
236 return 0
237}
238// compute the residual (cur - predicted) for a block at MV into residual[T*T]; returns its SAD.
239func vm_residual(cur: *u8, prev: *u8, residual: *i64, W: i64, bx: i64, by: i64, T: i64, mv: *i64) -> i64 {
240 let cx: i64 = bx * T
241 let cy: i64 = by * T
242 let px: i64 = cx + mv[0]
243 let py: i64 = cy + mv[1]
244 var s: i64 = 0
245 var yy: i64 = 0
246 while yy < T {
247 var xx: i64 = 0
248 while xx < T {
249 let c: i64 = cur[(cy+yy)*W + (cx+xx)] as i64
250 let p: i64 = prev[(py+yy)*W + (px+xx)] as i64
251 let d: i64 = c - p
252 residual[yy*T + xx] = d
253 if d < 0 { s = s - d } else { s = s + d }
254 xx = xx + 1
255 }
256 yy = yy + 1
257 }
258 return s
259}