code wiki / (root) / nx_simd_sat_test.nx

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}