nx_vsubpel.nx source
↩ module page · 156 lines · 6779 B
1// nx_vsubpel.nx -- Performs half-pel and quarter-pel sub-pixel interpolation for motion compensation in video encoding.
2const K_MAGIC_2147483647: i64 = 2147483647
3// nx_vsubpel.nx -- HALF-PEL sub-pixel motion compensation: the single biggest video quality-per-bit lever past integer
4// motion (H.264/VP9 go to quarter-pel). Real pans/faces move by NON-integer amounts; an integer-pel reference can't
5// land on a 0.5px shift, leaving a large residual = many bits. Interpolating the reference at half-pixel positions
6// (bilinear here; a 6-tap filter is the later refinement) lets the predictor hit the true sub-pixel position -> near-
7// zero residual -> far fewer bits at IDENTICAL quality. The MV is carried in HALF-pel units (2*integer + half). Pure
8// buffer math, no syscalls (wasm-friendly); caller owns the TxT scratch block. license_tier: ORIGINAL
9
10// bilinear half-pel: build a TxT block sampled from prev at base (px,py) with half-pel offsets hx,hy in {0,1}.
11// hx=1 averages with the +1 column, hy=1 with the +1 row, both = the 2x2 average (the (0.5,0.5) diagonal sample).
12func sp_interp_block(prev: *u8, W: i64, px: i64, py: i64, hx: i64, hy: i64, T: i64, out: *i64) -> i64 {
13 var yy: i64 = 0
14 while yy < T {
15 var xx: i64 = 0
16 while xx < T {
17 let x0: i64 = px + xx
18 let y0: i64 = py + yy
19 let a: i64 = prev[y0*W + x0] as i64
20 var v: i64 = a
21 if hx == 1 {
22 if hy == 1 {
23 let b: i64 = prev[y0*W + (x0+1)] as i64
24 let c: i64 = prev[(y0+1)*W + x0] as i64
25 let d: i64 = prev[(y0+1)*W + (x0+1)] as i64
26 v = (a + b + c + d + 2) / 4
27 } else {
28 let b: i64 = prev[y0*W + (x0+1)] as i64
29 v = (a + b + 1) / 2
30 }
31 } else {
32 if hy == 1 {
33 let c: i64 = prev[(y0+1)*W + x0] as i64
34 v = (a + c + 1) / 2
35 }
36 }
37 out[yy*T + xx] = v
38 xx = xx + 1
39 }
40 yy = yy + 1
41 }
42 return 0
43}
44// per-PIXEL half-pel sample at reference (x,y) with half offsets hx,hy in {0,1}. IDENTICAL bilinear math to
45// sp_interp_block, so an encoder that SCORES candidates with sp_interp_block and RECONSTRUCTS per-pixel with hp_pixel
46// gets matching values -- and a decoder using hp_pixel reconstructs bit-exact. (x+1,y+1) must be in-bounds when hx/hy=1.
47func hp_pixel(prev: *u8, W: i64, x: i64, y: i64, hx: i64, hy: i64) -> i64 {
48 let a: i64 = prev[y*W + x] as i64
49 if hx == 1 {
50 if hy == 1 {
51 let b: i64 = prev[y*W + (x+1)] as i64
52 let c: i64 = prev[(y+1)*W + x] as i64
53 let d: i64 = prev[(y+1)*W + (x+1)] as i64
54 return (a + b + c + d + 2) / 4
55 }
56 return (a + (prev[y*W + (x+1)] as i64) + 1) / 2
57 }
58 if hy == 1 { return (a + (prev[(y+1)*W + x] as i64) + 1) / 2 }
59 return a
60}
61// general QUARTER-pel bilinear sample at (x + qx/4, y + qy/4), qx,qy in {0,1,2,3} -- the H.264/VP9 motion precision.
62// Subsumes integer (qx=qy=0) and half-pel (qx/qy=2) EXACTLY (matches hp_pixel). Only reads (x+1)/(y+1) when that axis
63// has fractional weight, so the caller's bounds guard is the same as half-pel's.
64func qp_pixel(prev: *u8, W: i64, x: i64, y: i64, qx: i64, qy: i64) -> i64 {
65 let a: i64 = prev[y*W + x] as i64
66 if qx == 0 { if qy == 0 { return a } }
67 let wx: i64 = 4 - qx
68 let wy: i64 = 4 - qy
69 var acc: i64 = wx * wy * a
70 if qx > 0 { acc = acc + qx * wy * (prev[y*W + (x+1)] as i64) }
71 if qy > 0 { acc = acc + wx * qy * (prev[(y+1)*W + x] as i64) }
72 if qx > 0 { if qy > 0 { acc = acc + qx * qy * (prev[(y+1)*W + (x+1)] as i64) } }
73 return (acc + 8) / 16
74}
75// refine an integer MV (idx,idy) to QUARTER-pel: test the 16 (qx,qy) offsets, write best quarter MV (mvq[0]=4*idx+qx,
76// mvq[1]=4*idy+qy) and return its SAD over the TxT block (streaming, no scratch). Caller ensures a 1px edge guard.
77func qp_refine(cur: *u8, prev: *u8, W: i64, H: i64, cx: i64, cy: i64, idx: i64, idy: i64, T: i64, mvq: *i64) -> i64 {
78 let px: i64 = cx + idx
79 let py: i64 = cy + idy
80 var best: i64 = K_MAGIC_2147483647
81 var bqx: i64 = 0
82 var bqy: i64 = 0
83 var qy: i64 = 0
84 while qy <= 3 {
85 var qx: i64 = 0
86 while qx <= 3 {
87 var ok: i64 = 1
88 if qx > 0 { if px + T >= W { ok = 0 } }
89 if qy > 0 { if py + T >= H { ok = 0 } }
90 if ok == 1 {
91 var sad: i64 = 0
92 var ry: i64 = 0
93 while ry < T {
94 var rx: i64 = 0
95 while rx < T {
96 let dd: i64 = (cur[(cy+ry)*W + (cx+rx)] as i64) - qp_pixel(prev, W, px+rx, py+ry, qx, qy)
97 if dd < 0 { sad = sad - dd } else { sad = sad + dd }
98 rx = rx + 1
99 }
100 ry = ry + 1
101 }
102 if sad < best { best = sad; bqx = qx; bqy = qy }
103 }
104 qx = qx + 1
105 }
106 qy = qy + 1
107 }
108 mvq[0] = 4*idx + bqx
109 mvq[1] = 4*idy + bqy
110 return best
111}
112// SAD between cur's block at (cx,cy) and an interpolated i64 block
113func sp_sad(cur: *u8, W: i64, cx: i64, cy: i64, blk: *i64, T: i64) -> i64 {
114 var s: i64 = 0
115 var yy: i64 = 0
116 while yy < T {
117 var xx: i64 = 0
118 while xx < T {
119 let d: i64 = (cur[(cy+yy)*W + (cx+xx)] as i64) - blk[yy*T + xx]
120 if d < 0 { s = s - d } else { s = s + d }
121 xx = xx + 1
122 }
123 yy = yy + 1
124 }
125 return s
126}
127// refine an integer MV (idx,idy) to HALF-pel: test the 4 (hx,hy) offsets at that integer position, write the best
128// half-pel MV (mvh[0]=2*idx+hx, mvh[1]=2*idy+hy) and return its SAD. Caller guarantees a 1px guard at the frame edge.
129func sp_refine(cur: *u8, prev: *u8, W: i64, H: i64, cx: i64, cy: i64, idx: i64, idy: i64, T: i64, tmp: *i64, mvh: *i64) -> i64 {
130 let px: i64 = cx + idx
131 let py: i64 = cy + idy
132 var best: i64 = K_MAGIC_2147483647
133 var bhx: i64 = 0
134 var bhy: i64 = 0
135 var hy: i64 = 0
136 while hy <= 1 {
137 var hx: i64 = 0
138 while hx <= 1 {
139 var ok: i64 = 1
140 if px < 0 { ok = 0 }
141 if py < 0 { ok = 0 }
142 if px + T - 1 + hx >= W { ok = 0 }
143 if py + T - 1 + hy >= H { ok = 0 }
144 if ok == 1 {
145 sp_interp_block(prev, W, px, py, hx, hy, T, tmp)
146 let s: i64 = sp_sad(cur, W, cx, cy, tmp, T)
147 if s < best { best = s; bhx = hx; bhy = hy }
148 }
149 hx = hx + 1
150 }
151 hy = hy + 1
152 }
153 mvh[0] = 2*idx + bhx
154 mvh[1] = 2*idy + bhy
155 return best
156}