nx_vmotion.nx source
↩ module page · 337 lines · 18292 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
18const VC_HEX_TRUST_DIV: i64 = 256 // hexagon trust bar divisor (see vm_search_hex); 256 == the far-ring trigger; 512 was measured and rejected
19// F1111 rung-2 (predictor priming) was built, proven bit-exact, MEASURED 0.999x, and REMOVED 2026-07-27:
20// the adaptive window + deadzone floor + capped SAD already exhaust the cap-tightening lever. Full
21// design, proof and numbers: nishifamily.com/video-engineering Act IV + the video lane memory file.
22
23// SAD of 16 bytes at a vs b -- THE F618 SIMD SEAM (flipped 2026-07-21). Native builds execute this
24// scalar body (the bit-exact reference); the wat backend (nx_wasm.nx) INTERCEPTS calls to it BY NAME
25// and inlines the fused wasm-SIMD sequence (v128.load x2, sub_sat_u both ways, or, extadd-pairwise,
26// lane sum) -- same integer by construction. Do not rename without updating the intercept.
27func v128_sad16(a: *u8, b: *u8) -> i64 {
28 var s: i64 = 0
29 var i: i64 = 0
30 while i < 16 {
31 let d: i64 = (a[i] as i64) - (b[i] as i64)
32 if d < 0 { s = s - d } else { s = s + d }
33 i = i + 1
34 }
35 return s
36}
37
38// SAD between cur's block at (cx,cy) and prev's block at (px,py), T x T, over W-stride 8bpp buffers.
39// Row bases hoisted out of the pixel loop (the skeleton compiler doesn't CSE the y*W multiplies;
40// this runs on EVERY block's skip decision, so the hoist pays frame-wide). Arithmetic unchanged.
41// F618: T==16 rows ride v128_sad16 (one row = one 16-byte SAD; addition is associative so the row
42// total is the same integer); other T keep the exact scalar loop.
43func vm_sad(cur: *u8, prev: *u8, W: i64, cx: i64, cy: i64, px: i64, py: i64, T: i64) -> i64 {
44 var s: i64 = 0
45 var yy: i64 = 0
46 if T == 16 {
47 while yy < 16 {
48 let crow: i64 = (cy + yy) * W + cx
49 let prow: i64 = (py + yy) * W + px
50 let ca: i64 = (cur as i64) + crow
51 let pa: i64 = (prev as i64) + prow
52 let cp: *u8 = ca as *u8
53 let pp: *u8 = pa as *u8
54 let d: i64 = v128_sad16(cp, pp)
55 s = s + d
56 yy = yy + 1
57 }
58 return s
59 }
60 while yy < T {
61 let crow: i64 = (cy + yy) * W + cx
62 let prow: i64 = (py + yy) * W + px
63 var xx: i64 = 0
64 while xx < T {
65 let d: i64 = (cur[crow + xx] as i64) - (prev[prow + xx] as i64)
66 if d < 0 { s = s - d } else { s = s + d }
67 xx = xx + 1
68 }
69 yy = yy + 1
70 }
71 return s
72}
73// vm_sad with a CAP: abort row-granularly once the partial sum reaches cap. SAD only grows, so an
74// aborted candidate returns a partial s >= cap and the caller's strict (s < best) verdict is IDENTICAL
75// to the full computation -- any true winner (< cap) never aborts. Argmin (and thus the bitstream)
76// stays BIT-EXACT while losers stop early (the 2026-07-03 speed bench measured the un-exited full
77// search at 165ms/frame = the realtime blocker this closes down).
78func vm_sad_capped(cur: *u8, prev: *u8, W: i64, cx: i64, cy: i64, px: i64, py: i64, T: i64, cap: i64) -> i64 {
79 var s: i64 = 0
80 var yy: i64 = 0
81 if T == 16 {
82 while yy < 16 {
83 let crow: i64 = (cy + yy) * W + cx
84 let prow: i64 = (py + yy) * W + px
85 let ca: i64 = (cur as i64) + crow
86 let pa: i64 = (prev as i64) + prow
87 let cp: *u8 = ca as *u8
88 let pp: *u8 = pa as *u8
89 let d: i64 = v128_sad16(cp, pp)
90 s = s + d
91 if s >= cap { return s }
92 yy = yy + 1
93 }
94 return s
95 }
96 while yy < T {
97 let crow: i64 = (cy + yy) * W + cx
98 let prow: i64 = (py + yy) * W + px
99 var xx: i64 = 0
100 while xx < T {
101 let d: i64 = (cur[crow + xx] as i64) - (prev[prow + xx] as i64)
102 if d < 0 { s = s - d } else { s = s + d }
103 xx = xx + 1
104 }
105 if s >= cap { return s }
106 yy = yy + 1
107 }
108 return s
109}
110// evaluate one candidate (dx,dy); updates best/bdx/bdy via the out array b[0]=best b[1]=bdx b[2]=bdy.
111func vm_try(cur: *u8, prev: *u8, W: i64, H: i64, cx: i64, cy: i64, T: i64, dx: i64, dy: i64, b: *i64) -> i64 {
112 let px: i64 = cx + dx
113 let py: i64 = cy + dy
114 if px >= 0 { if py >= 0 { if px + T <= W { if py + T <= H {
115 let s: i64 = vm_sad_capped(cur, prev, W, cx, cy, px, py, T, b[0])
116 if s < b[0] { b[0] = s; b[1] = dx; b[2] = dy }
117 } } } }
118 return 0
119}
120// full-coverage search of the [-R,R] window, CENTER-FIRST ring order: radius 0, then each square ring
121// outward. Real video motion is small, so the near-optimal candidate lands FIRST and the capped SAD
122// aborts every far loser after a row or two -- the full window is still 100% covered (no candidates
123// dropped; this is exact search, only reordered). Tie-break change vs the old raster order: equal-SAD
124// candidates now resolve to the SMALLEST radius = the cheapest MV to code (strictly better, and the
125// decoder is agnostic -- any coded MV reconstructs exactly). Writes mv[0]=dx mv[1]=dy; returns best SAD.
126// THE ONE ring-scan body (F1111 DRY: this was duplicated verbatim into vm_search and vm_search_q; a
127// third copy for any future variant was the trap). vfloor==0 disables the deadzone early-exit entirely
128// (vm_search semantics, exact-original); vfloor>0 arms it (vm_search_q semantics). Callers own b[] init
129// and the mv[] writeback, so both entry points keep their exact published contracts.
130// r0 = FIRST ring to enumerate. r0==0 also evaluates the (0,0) centre candidate. A scan of rings 0..A
131// followed by a scan of rings A+1..B visits EXACTLY the candidates of a single 0..B scan, in EXACTLY the
132// same order, against the same monotonically-tightening b[] -- so a two-pass (adaptive) search is
133// candidate-for-candidate IDENTICAL to the one-pass search it extends to. That identity is what makes
134// vm_search_q's adaptive form safe: extending costs quality NOTHING, only the time it would have cost.
135func 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 {
136 if r0 == 0 { vm_try(cur, prev, W, H, cx, cy, T, 0, 0, b) }
137 var r: i64 = r0
138 if r < 1 { r = 1 }
139 while r <= R {
140 // rings 0-2 always search fully; the floor-exit prunes only the far rings (see vm_search_q note).
141 var prune: i64 = 0
142 if vfloor > 0 { if r > 2 { if b[0] < vfloor { prune = 1 } } }
143 if prune == 1 { r = R + 1 } else {
144 // top + bottom rows of the ring (full width)
145 var dx: i64 = 0 - r
146 while dx <= r {
147 vm_try(cur, prev, W, H, cx, cy, T, dx, 0 - r, b)
148 vm_try(cur, prev, W, H, cx, cy, T, dx, r, b)
149 dx = dx + 1
150 }
151 // left + right columns (excluding the corners already done)
152 var dy: i64 = 0 - r + 1
153 while dy <= r - 1 {
154 vm_try(cur, prev, W, H, cx, cy, T, 0 - r, dy, b)
155 vm_try(cur, prev, W, H, cx, cy, T, r, dy, b)
156 dy = dy + 1
157 }
158 r = r + 1
159 }
160 }
161 return b[0]
162}
163// scr3 (2026-07-29 root fix): the 3-slot best-box [sad,dx,dy] was a sys_mmap(32) PER SEARCH CALL --
164// per MB, on the hottest path. In the wasm tier sys_mmap was the 0-stub (silent address-0 aliasing,
165// seq234 class); natively it was an unmunmapped map per MB (the map-count crash class). The caller
166// provides 3 i64 of scratch (blk is dead during search in the codec; gates hoist ONE mmap).
167func vm_search(cur: *u8, prev: *u8, W: i64, H: i64, bx: i64, by: i64, T: i64, R: i64, mv: *i64, scr3: *i64) -> i64 {
168 let cx: i64 = bx * T
169 let cy: i64 = by * T
170 let b: *i64 = scr3
171 b[0] = 2147483647
172 b[1] = 0
173 b[2] = 0
174 vm_scan(cur, prev, W, H, cx, cy, T, R, b, 0, 0)
175 mv[0] = b[1]
176 mv[1] = b[2]
177 return b[0]
178}
179// DEADZONE-FLOOR EARLY-EXIT variant (825, encoder-only; the RICH/rct8 path's searcher -- vm_search above keeps
180// the exact-original semantics for the legacy stream). Once the best SAD is under the QUANT DEADZONE scale
181// (T*T*qp/8 ~= a residual that quantizes to ~nothing), no candidate can improve the CODED outcome -- stop
182// enumerating rings. On call content the true MV lands in ring 0-2 (center-first), cutting the 289-candidate
183// walk to ~10-25 for the coherent majority. First try (qp-blind T*T*2) MEASURED +2.02% BD (bus +5.17: at low
184// qp a sub-floor better match still pays) -> qp-scaled floor keeps the noise-fps win AND the low-qp quality.
185func 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 {
186 let cx: i64 = bx * T
187 let cy: i64 = by * T
188 let b: *i64 = scr3
189 b[0] = 2147483647
190 b[1] = 0
191 b[2] = 0
192 // rings 0-2 always search fully (25 candidates -- the coherent-motion near window; exiting inside it
193 // measured +2.0% BD on real-motion seqs); the floor-exit prunes only the far rings (the 264-candidate
194 // tail that exists for rare large motion).
195 let vm_floor: i64 = (T * T * qp) / 8
196 // F1111: near window first, far rings only for blocks that did not find a match inside the measured
197 // translation band. VC_ME_RNEAR==0 restores the exact single-pass R scan (the pre-F1111 behaviour).
198 var rn: i64 = VC_ME_RNEAR
199 if rn <= 0 { rn = R }
200 if rn > R { rn = R }
201 vm_scan(cur, prev, W, H, cx, cy, T, rn, b, vm_floor, 0)
202 // ⚠the extend threshold MUST stay above vm_floor (VC_ME_EXTK > 32), else a block the deadzone prune
203 // already accepted could re-enter the far rings that a single-pass scan would have skipped, and the
204 // two-pass/one-pass equivalence below would not hold.
205 let ext_thresh: i64 = (T * T * qp * VC_ME_EXTK) / 256
206 if rn < R { if b[0] >= ext_thresh { vm_scan(cur, prev, W, H, cx, cy, T, R, b, vm_floor, rn + 1) } }
207 mv[0] = b[1]
208 mv[1] = b[2]
209 return b[0]
210}
211// HEXAGON SEARCH (encoder-only, selected by emode bit15 VC_EMODE_HEXME in the block coder, 2026-09-02). The exhaustive
212// centre-first ring scan above costs 289 (near window) to 1089 candidates per MB; MEASURED by nx_vcodec_stage_bench on
213// foreman at the live point the integer search was 37 percent of the frame at ~35 us per MB. The field's RTC encoders
214// (x264 hex/umh, libvpx hex, SVT-AV1 HME) walk a PATTERN instead: a 6-point large hexagon (radius 2) re-centred on its
215// argmin until the centre holds, then a 4-point small diamond (radius 1) -- typically 15-40 candidates. Same vm_try
216// (frame bounds + capped SAD, so no out-of-frame read) and the same qp-scaled deadzone floor exit as vm_search_q; the
217// step count is bounded so |mv| never exceeds R. A pattern search CAN miss a distant minimum the exhaustive scan finds,
218// so its cost is BD-rate MEASURED (nx_vcodec_stage_bench bd -> nx_bd_calc), never claimed exact.
219func vm_search_hex(cur: *u8, prev: *u8, W: i64, H: i64, bx: i64, by: i64, T: i64, R: i64, mv: *i64, qp: i64, scr3: *i64) -> i64 {
220 let cx: i64 = bx * T
221 let cy: i64 = by * T
222 let b: *i64 = scr3
223 b[0] = 2147483647
224 b[1] = 0
225 b[2] = 0
226 let vm_floor: i64 = (T * T * qp) / 8
227 vm_try(cur, prev, W, H, cx, cy, T, 0, 0, b)
228 // SPATIAL PREDICTOR SEED (MEASURED 2026-09-02: the pattern from (0,0) alone cost +7.9 percent BD on foreman even with
229 // the exhaustive fallback -- coherent medium motion where the hexagon settles for a match under the bar). The caller
230 // hands the previous macroblock's vector in scr3[3..4] (raster order: the left neighbour, or the last block of the row
231 // above); the hexagon starts from the better of (0,0) and that predictor. |predictor| is clamped to R.
232 var pmx: i64 = scr3[3]
233 var pmy: i64 = scr3[4]
234 if pmx > R { pmx = R }
235 if pmx < 0 - R { pmx = 0 - R }
236 if pmy > R { pmy = R }
237 if pmy < 0 - R { pmy = 0 - R }
238 if pmx != 0 { vm_try(cur, prev, W, H, cx, cy, T, pmx, pmy, b) } else { if pmy != 0 { vm_try(cur, prev, W, H, cx, cy, T, pmx, pmy, b) } }
239 var cxm: i64 = b[1]
240 var cym: i64 = b[2]
241 var steps: i64 = 0
242 let max_steps: i64 = (R - 1) / 2
243 var go: i64 = 1
244 while go == 1 {
245 if b[0] < vm_floor { go = 0 } else {
246 vm_try(cur, prev, W, H, cx, cy, T, cxm + 2, cym, b)
247 vm_try(cur, prev, W, H, cx, cy, T, cxm - 2, cym, b)
248 vm_try(cur, prev, W, H, cx, cy, T, cxm + 1, cym + 2, b)
249 vm_try(cur, prev, W, H, cx, cy, T, cxm - 1, cym + 2, b)
250 vm_try(cur, prev, W, H, cx, cy, T, cxm + 1, cym - 2, b)
251 vm_try(cur, prev, W, H, cx, cy, T, cxm - 1, cym - 2, b)
252 if b[1] == cxm { if b[2] == cym { go = 0 } }
253 cxm = b[1]
254 cym = b[2]
255 steps = steps + 1
256 if steps >= max_steps { go = 0 }
257 }
258 }
259 vm_try(cur, prev, W, H, cx, cy, T, cxm + 1, cym, b)
260 vm_try(cur, prev, W, H, cx, cy, T, cxm - 1, cym, b)
261 vm_try(cur, prev, W, H, cx, cy, T, cxm, cym + 1, b)
262 vm_try(cur, prev, W, H, cx, cy, T, cxm, cym - 1, b)
263 // EXHAUSTIVE FALLBACK (MEASURED 2026-09-02): the bare pattern cost +10.4 percent BD on foreman and +22.4 on bus --
264 // a hexagon walked from (0,0) parks in a local minimum on real translation. The estate already OWNS the "this match is
265 // not good enough" bar: vm_search_q's far-ring extension threshold (T*T*qp*VC_ME_EXTK/256). A pattern result still
266 // above it takes the SAME two-pass scan vm_search_q runs (near window, then far rings if still above the bar),
267 // seeded with the pattern's best so the capped SAD aborts every loser early. Easy blocks (the static majority)
268 // never enter the scan; hard blocks keep the exhaustive answer.
269 // TRUST BAR -- MEASURED 2026-09-02 (nx_vcodec_stage_bench bd, 48f CIF, q 8/16/24/32/44, BD vs the exhaustive anchor):
270 // /256 (bar == the far-ring trigger): akiyo +0.8 foreman +3.5 bus 0.0 mobile +0.2 percent BD; encode -21..-29 percent
271 // /512 (pattern must be twice as good): akiyo +0.3 foreman +1.5 bus -0.6 mobile 0.0; encode win GONE on bus/mobile
272 // Neither meets the pre-declared +1.0 percent bound on foreman, so VC_EMODE_HEXME is NOT composed into the live emode;
273 // the /256 bar is kept because it is the variant with the real speed win. Halving the bar buys BD by sending most
274 // moving-content blocks through BOTH searches, which is why the time win disappears. Next mechanism, not next number:
275 // a second predictor (top neighbour, needs an MV row buffer) or a merged pattern+ring scan that shares the SAD work.
276 let ext_thresh: i64 = (T * T * qp * VC_ME_EXTK) / VC_HEX_TRUST_DIV
277 if b[0] >= ext_thresh {
278 var rn: i64 = VC_ME_RNEAR
279 if rn <= 0 { rn = R }
280 if rn > R { rn = R }
281 vm_scan(cur, prev, W, H, cx, cy, T, rn, b, vm_floor, 1)
282 if rn < R { if b[0] >= ext_thresh { vm_scan(cur, prev, W, H, cx, cy, T, R, b, vm_floor, rn + 1) } }
283 }
284 mv[0] = b[1]
285 mv[1] = b[2]
286 return b[0]
287}
288// the residual SAD with NO motion (dx=dy=0) -- i.e. what plain tile-delta "sees" as the block's change.
289func vm_sad_zero(cur: *u8, prev: *u8, W: i64, bx: i64, by: i64, T: i64) -> i64 {
290 let cx: i64 = bx * T
291 let cy: i64 = by * T
292 return vm_sad(cur, prev, W, cx, cy, cx, cy, T)
293}
294// reconstruct cur's block into dst from prev at the motion vector + the residual (dst = predicted + residual).
295// residual[k] = cur - predicted (signed, i16-range). For SAD==0 the residual is all-zero and predicted == cur.
296func vm_reconstruct(dst: *u8, prev: *u8, residual: *i64, W: i64, bx: i64, by: i64, T: i64, mv: *i64) -> i64 {
297 let cx: i64 = bx * T
298 let cy: i64 = by * T
299 let px: i64 = cx + mv[0]
300 let py: i64 = cy + mv[1]
301 var yy: i64 = 0
302 while yy < T {
303 var xx: i64 = 0
304 while xx < T {
305 let pred: i64 = prev[(py+yy)*W + (px+xx)] as i64
306 var v: i64 = pred + residual[yy*T + xx]
307 if v < 0 { v = 0 }
308 if v > 255 { v = 255 }
309 dst[(cy+yy)*W + (cx+xx)] = v as u8
310 xx = xx + 1
311 }
312 yy = yy + 1
313 }
314 return 0
315}
316// compute the residual (cur - predicted) for a block at MV into residual[T*T]; returns its SAD.
317func vm_residual(cur: *u8, prev: *u8, residual: *i64, W: i64, bx: i64, by: i64, T: i64, mv: *i64) -> i64 {
318 let cx: i64 = bx * T
319 let cy: i64 = by * T
320 let px: i64 = cx + mv[0]
321 let py: i64 = cy + mv[1]
322 var s: i64 = 0
323 var yy: i64 = 0
324 while yy < T {
325 var xx: i64 = 0
326 while xx < T {
327 let c: i64 = cur[(cy+yy)*W + (cx+xx)] as i64
328 let p: i64 = prev[(py+yy)*W + (px+xx)] as i64
329 let d: i64 = c - p
330 residual[yy*T + xx] = d
331 if d < 0 { s = s - d } else { s = s + d }
332 xx = xx + 1
333 }
334 yy = yy + 1
335 }
336 return s
337}