nx_simd_sat_test.nx source
↩ module page · 148 lines · 6007 B
1// nx_simd_sat_test.nx -- i16x16 signed saturating add/sub.
2//
3// Audio DSP requirement: when two 16-bit signed samples sum past
4// INT16_MAX, the result must clip to INT16_MAX (32767), not wrap
5// to a negative value (which would produce a pop/click). Same
6// for INT16_MIN underflow.
7//
8// vpaddsw on x86 and vsadd.vv on RV-V do this in hardware --
9// previously callers needed manual clip(a+b, INT16_MIN, INT16_MAX)
10// with branching that defeats SIMD.
11
12import "nx_kernel_v2.nx"
13import "nx_log.nx"
14
15func pack4_i16(a: i64, b: i64, c: i64, d: i64) -> i64 {
16 return (a & 0xFFFF)
17 | ((b & 0xFFFF) << 16)
18 | ((c & 0xFFFF) << 32)
19 | ((d & 0xFFFF) << 48)
20}
21
22func t1_sat_add_clipping_high() -> i64 {
23 // a = [30000, 30000, 30000, 30000, ...] (16 lanes)
24 // b = [10000, 10000, 10000, 10000, ...]
25 // sum without sat = 40000 -> wraps to -25536
26 // sum WITH sat = 32767 (INT16_MAX)
27 let a_raw: *u8 = sys_mmap(64)
28 let b_raw: *u8 = sys_mmap(64)
29 let r_raw: *u8 = sys_mmap(64)
30 let a: *i64 = a_raw as *i64
31 let b: *i64 = b_raw as *i64
32 let r: *i64 = r_raw as *i64
33 a[0] = pack4_i16(30000, 30000, 30000, 30000)
34 a[1] = pack4_i16(30000, 30000, 30000, 30000)
35 a[2] = pack4_i16(30000, 30000, 30000, 30000)
36 a[3] = pack4_i16(30000, 30000, 30000, 30000)
37 b[0] = pack4_i16(10000, 10000, 10000, 10000)
38 b[1] = pack4_i16(10000, 10000, 10000, 10000)
39 b[2] = pack4_i16(10000, 10000, 10000, 10000)
40 b[3] = pack4_i16(10000, 10000, 10000, 10000)
41 let va: i64 = __simd_vload_i16_x16(a as *i64)
42 let vb: i64 = __simd_vload_i16_x16(b as *i64)
43 let vr: i64 = __simd_vsadd_i16_x16(va, vb)
44 __simd_vstore_i16_x16(vr, r as *i64)
45 let expected: i64 = pack4_i16(32767, 32767, 32767, 32767)
46 if r[0] != expected { return 1 }
47 if r[1] != expected { return 2 }
48 if r[2] != expected { return 3 }
49 if r[3] != expected { return 4 }
50 return 0
51}
52
53func t2_sat_sub_clipping_low() -> i64 {
54 // a = [-30000, ...], b = [10000, ...]
55 // a - b = -40000 -> wraps to +25536
56 // sat: INT16_MIN = -32768
57 let a_raw: *u8 = sys_mmap(64)
58 let b_raw: *u8 = sys_mmap(64)
59 let r_raw: *u8 = sys_mmap(64)
60 let a: *i64 = a_raw as *i64
61 let b: *i64 = b_raw as *i64
62 let r: *i64 = r_raw as *i64
63 a[0] = pack4_i16(-30000, -30000, -30000, -30000)
64 a[1] = pack4_i16(-30000, -30000, -30000, -30000)
65 a[2] = pack4_i16(-30000, -30000, -30000, -30000)
66 a[3] = pack4_i16(-30000, -30000, -30000, -30000)
67 b[0] = pack4_i16(10000, 10000, 10000, 10000)
68 b[1] = pack4_i16(10000, 10000, 10000, 10000)
69 b[2] = pack4_i16(10000, 10000, 10000, 10000)
70 b[3] = pack4_i16(10000, 10000, 10000, 10000)
71 let va: i64 = __simd_vload_i16_x16(a as *i64)
72 let vb: i64 = __simd_vload_i16_x16(b as *i64)
73 let vr: i64 = __simd_vssub_i16_x16(va, vb)
74 __simd_vstore_i16_x16(vr, r as *i64)
75 let expected: i64 = pack4_i16(-32768, -32768, -32768, -32768)
76 if r[0] != expected { return 10 }
77 if r[1] != expected { return 11 }
78 if r[2] != expected { return 12 }
79 if r[3] != expected { return 13 }
80 return 0
81}
82
83func t3_sat_in_range_unchanged() -> i64 {
84 // Mid-range values should be unchanged by saturating add.
85 let a_raw: *u8 = sys_mmap(64)
86 let b_raw: *u8 = sys_mmap(64)
87 let r_raw: *u8 = sys_mmap(64)
88 let a: *i64 = a_raw as *i64
89 let b: *i64 = b_raw as *i64
90 let r: *i64 = r_raw as *i64
91 a[0] = pack4_i16(100, 200, 300, 400)
92 a[1] = pack4_i16(500, 600, 700, 800)
93 a[2] = pack4_i16(-100, -200, -300, -400)
94 a[3] = pack4_i16(-500, -600, -700, -800)
95 b[0] = pack4_i16(1, 2, 3, 4)
96 b[1] = pack4_i16(5, 6, 7, 8)
97 b[2] = pack4_i16(9, 10, 11, 12)
98 b[3] = pack4_i16(13, 14, 15, 16)
99 let va: i64 = __simd_vload_i16_x16(a as *i64)
100 let vb: i64 = __simd_vload_i16_x16(b as *i64)
101 let vr: i64 = __simd_vsadd_i16_x16(va, vb)
102 __simd_vstore_i16_x16(vr, r as *i64)
103 if r[0] != pack4_i16(101, 202, 303, 404) { return 20 }
104 if r[1] != pack4_i16(505, 606, 707, 808) { return 21 }
105 if r[2] != pack4_i16(-91, -190, -289, -388) { return 22 }
106 if r[3] != pack4_i16(-487, -586, -685, -784) { return 23 }
107 return 0
108}
109
110func t4_sat_boundary_exact() -> i64 {
111 // a = INT16_MAX = 32767, b = 1. Sum sat to 32767.
112 let a_raw: *u8 = sys_mmap(64)
113 let b_raw: *u8 = sys_mmap(64)
114 let r_raw: *u8 = sys_mmap(64)
115 let a: *i64 = a_raw as *i64
116 let b: *i64 = b_raw as *i64
117 let r: *i64 = r_raw as *i64
118 a[0] = pack4_i16(32767, 32766, -32768, -32767)
119 a[1] = pack4_i16(0, 0, 0, 0)
120 a[2] = pack4_i16(0, 0, 0, 0)
121 a[3] = pack4_i16(0, 0, 0, 0)
122 b[0] = pack4_i16(1, 1, -1, -1)
123 b[1] = pack4_i16(0, 0, 0, 0)
124 b[2] = pack4_i16(0, 0, 0, 0)
125 b[3] = pack4_i16(0, 0, 0, 0)
126 let va: i64 = __simd_vload_i16_x16(a as *i64)
127 let vb: i64 = __simd_vload_i16_x16(b as *i64)
128 let vr: i64 = __simd_vsadd_i16_x16(va, vb)
129 __simd_vstore_i16_x16(vr, r as *i64)
130 // lanes: 32767+1=32767 (sat), 32766+1=32767 (exact), -32768-1=-32768 (sat), -32767-1=-32768 (sat)
131 if r[0] != pack4_i16(32767, 32767, -32768, -32768) { return 30 }
132 return 0
133}
134
135func main() -> nx_exit {
136 println("=== nx_simd_sat smoke (i16x16 signed saturating arith) ===" as *u8)
137 if t1_sat_add_clipping_high() != 0 { println("FAIL T1" as *u8); return 1 }
138 println("T1 sat add high PASS 30000+10000 -> 32767 (clip vs wrap-25536)" as *u8)
139 if t2_sat_sub_clipping_low() != 0 { println("FAIL T2" as *u8); return 2 }
140 println("T2 sat sub low PASS -30000-10000 -> -32768 (clip vs wrap+25536)" as *u8)
141 if t3_sat_in_range_unchanged() != 0 { println("FAIL T3" as *u8); return 3 }
142 println("T3 in-range PASS no clip for non-overflowing lanes" as *u8)
143 if t4_sat_boundary_exact() != 0 { println("FAIL T4" as *u8); return 4 }
144 println("T4 sat boundary PASS exact INT16_MAX/MIN edges" as *u8)
145 println("" as *u8)
146 println("Saturating arith OPTIMAL on RV-V (vsadd.vv/vssub.vv) and x86 (vpaddsw/vpsubsw)." as *u8)
147 return 0
148}