code wiki / (root) / nx_vmotion.nx

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}