code wiki / (root) / nx_jpeg_decode.nx

nx_jpeg_decode.nx source

↩ module page · 337 lines · 16816 B

1// nx_jpeg_decode.nx -- top-level baseline-JPEG decoder. The 2// marker-walking front-end that ties every brick in the JPEG arc 3// into a bytes-in / sample-planes-out API. 4// 5// Pipeline: 6// jpeg_bytes 7// -> nx_jpeg_seg_next (marker scan) 8// for each segment: 9// DQT -> nx_jpeg_dqt_parse into scratch, then PLACED by Tq (a redefinition replaces) 10// DHT -> nx_jpeg_dht_parse into scratch, then PLACED by (Tc,Th) (a redefinition replaces) 11// SOF0 -> nx_jpeg_sof_parse, store frame 12// SOS -> nx_jpeg_sos_parse, store scan + record entropy_off 13// (entropy bytes run from segment end to EOI) 14// else -> skip (APP / COM / etc.) 15// -> nx_jpeg_decode_image_resolve (per-component table lookup) 16// -> nx_jpeg_decode_image_walk_mcus (MCU grid driver) 17// 18// Output: per-component sample planes filled. Caller upsamples 19// + YCbCr->RGB to produce the final RGB framebuffer. 20// 21// Caller supplies all the storage (frame + scan + qtables + htables 22// + per-component planes). Substrate adds no malloc beyond the 23// scratch arenas internal to per-block primitives. 24// 25// nx_safety_envelope: 26// intended_use: "Top-level baseline-JPEG decode -- bytes-in, 27// sample-planes-out. Last brick before 28// YCbCr->RGB upsample step." 29// sil_target: SIL1 30// evidence: [composes_marker_dqt_dht_sof_sos_decode_image, 31// sealed_verdicts_per_segment_kind, 32// bounded_iteration] 33// hazard_register: [bug-tape-progressive-mistaken-baseline, 34// bug-tape-restart-marker-not-honored, 35// bug-tape-app-segment-length-overrun] 36// residual_risk: "Baseline (SOF0) only. Progressive (SOF2), 37// extended-sequential (SOF1), and arithmetic- 38// coded variants reject with NOT_BASELINE." 39// verdict: NOT_YET_EVALUATED 40 41import "nx_syscalls.nx" 42import "nx_jpeg_marker.nx" 43import "nx_jpeg_dqt.nx" 44import "nx_jpeg_dht.nx" 45import "nx_jpeg_sof.nx" 46import "nx_jpeg_sos.nx" 47import "nx_jpeg_mcu.nx" 48import "nx_jpeg_decode_image.nx" 49 50const NX_JPEG_DEC_TOP_OK: i64 = 0 51const NX_JPEG_DEC_TOP_NO_SOI: i64 = 1 52const NX_JPEG_DEC_TOP_NO_SOF: i64 = 2 53const NX_JPEG_DEC_TOP_NO_SOS: i64 = 3 54const NX_JPEG_DEC_TOP_BAD_DQT: i64 = 4 55const NX_JPEG_DEC_TOP_BAD_DHT: i64 = 5 56const NX_JPEG_DEC_TOP_BAD_SOF: i64 = 6 57const NX_JPEG_DEC_TOP_BAD_SOS: i64 = 7 58const NX_JPEG_DEC_TOP_NOT_BASELINE: i64 = 8 59const NX_JPEG_DEC_TOP_IMAGE_FAIL: i64 = 9 // resolve / walk_mcus error 60const NX_JPEG_DEC_TOP_RESULT_N: i64 = 10 61 62func nx_jpeg_dec_top_result_is_valid(v: i64) -> i64 { 63 if v < 0 { return 0 } 64 if v >= NX_JPEG_DEC_TOP_RESULT_N { return 0 } 65 return 1 66} 67 68// Caller-supplied storage for the decoder's intermediate state. 69// Allows the decoder to be invoked with no internal allocation. 70struct NxJpegDecCtx { 71 frame: *NxJpegFrame, 72 scan: *NxJpegScan, 73 sof_comps: *NxJpegSofComponent, // 4 slots 74 sos_comps: *NxJpegSosComponent, // 4 slots 75 qtables: *NxJpegQTable, // 4 slots 76 qvalue_bufs: *i64, // 4 * 64 i64 77 htables: *NxJpegHTable, // 4 slots 78 hbits_pool: *i64, // 4 * 17 i64 79 hhuffval_pool: *i64, // 4 * 256 i64 80 hmincode_pool: *i64, // 4 * 17 i64 81 hmaxcode_pool: *i64, // 4 * 17 i64 82 hvalptr_pool: *i64, // 4 * 17 i64 83 cstates: *NxJpegDecCompState, // 4 slots 84 n_qtables: i64, 85 n_htables: i64 86} 87 88// 15 fields x 8 B: 13 pointers + n_qtables + n_htables. THIS CONSTANT WAS 112 (14 fields) WHILE 89// THE STRUCT ABOVE HAS 15 -- the hand-count drifted when a field was added, so the last field 90// write in nx_jpeg_dec_ctx_init (`ctx.n_htables = 0`) landed 8 B past EVERY allocation, firing the 91// arena canary as "ARENA-OVERRUN prev_alloc_size=112" on every decode (nx_jpegmem_probe, 92// 2026-08-23). A hand-counted length beside a struct is a second copy of the struct's shape and 93// the two drift silently: IF YOU ADD A FIELD, THIS NUMBER MOVES WITH IT. 94const NX_JPEG_DEC_CTX_BYTES: i64 = 120 95 96// TABLE SLOTS ARE KEYED BY THE NAME THE STANDARD GIVES THEM, NEVER BY ORDER OF APPEARANCE (2026-09-16, debt 1789603770). 97// T.81 B.2.4.2: a DHT or DQT definition names its table (Tc 0..1 with Th 0..3, or Tq 0..3) and a later definition of the 98// same name REPLACES the earlier one. Both decoders appended every parsed table to a four-slot array (`n_htables + count`), 99// so a progressive file, which carries one DHT per scan (ri_diora_baird_1: 7 tables over 6 scans; ri_diora_baird_4: 10 over 100// 10), wrote its fifth table 64 bytes past the array -- the arena canary named the victim by size on every real decode 101// (ARENA-OVERRUN prev_alloc_size=256 = 4 x NX_JPEG_HTABLE_BYTES) -- and the baseline lookup took the FIRST match, so a 102// redefined table lost to its stale definition. Now a segment is parsed into SCRATCH slots (a segment carries at most four 103// tables, the parser's own bound) and each table is COPIED into the slot its name selects; the keyed area is fixed, so no 104// input can move a write past it, and a lookup by name finds exactly one table, the latest. Free slots carry tc/tq = -1, 105// which no scan can name, so the existing linear lookups stay correct without knowing about slots at all. 106const NX_JPEG_TC_CLASSES: i64 = 2 107const NX_JPEG_TH_PER_CLASS: i64 = 4 108const NX_JPEG_HSLOTS: i64 = 8 109const NX_JPEG_HSCRATCH: i64 = 4 110const NX_JPEG_QSLOTS: i64 = 4 111const NX_JPEG_QSCRATCH: i64 = 4 112const NX_JPEG_COMP_SLOTS: i64 = 4 113const NX_JPEG_HBITS_N: i64 = 17 114const NX_JPEG_HVAL_N: i64 = 256 115const NX_JPEG_QVAL_N: i64 = 64 116const NX_JPEG_TSLOT_FREE: i64 = 0 - 1 117const NX_JPEG_I64: i64 = 8 118 119func nx_jpeg_ctx_htable_at(ctx: *NxJpegDecCtx, slot: i64) -> *NxJpegHTable { 120 return (ctx.htables as i64 + slot * NX_JPEG_HTABLE_BYTES) as *NxJpegHTable 121} 122func nx_jpeg_ctx_qtable_at(ctx: *NxJpegDecCtx, slot: i64) -> *NxJpegQTable { 123 return (ctx.qtables as i64 + slot * NX_JPEG_QTABLE_BYTES) as *NxJpegQTable 124} 125// every slot, keyed or scratch, owns fixed rows of the pools; a slot's struct always points at its own rows 126func nx_jpeg_ctx_htable_bind(ctx: *NxJpegDecCtx, slot: i64) -> i64 { 127 let t: *NxJpegHTable = nx_jpeg_ctx_htable_at(ctx, slot) 128 t.bits = (ctx.hbits_pool as i64 + slot * NX_JPEG_HBITS_N * NX_JPEG_I64) as *i64 129 t.huffval = (ctx.hhuffval_pool as i64 + slot * NX_JPEG_HVAL_N * NX_JPEG_I64) as *i64 130 t.mincode = (ctx.hmincode_pool as i64 + slot * NX_JPEG_HBITS_N * NX_JPEG_I64) as *i64 131 t.maxcode = (ctx.hmaxcode_pool as i64 + slot * NX_JPEG_HBITS_N * NX_JPEG_I64) as *i64 132 t.valptr = (ctx.hvalptr_pool as i64 + slot * NX_JPEG_HBITS_N * NX_JPEG_I64) as *i64 133 return 0 134} 135// every keyed slot FREE: a decode starts with no tables (the progressive decoder resets once, before its marker walk) 136func nx_jpeg_ctx_tables_reset(ctx: *NxJpegDecCtx) -> i64 { 137 var s: i64 = 0 138 while s < NX_JPEG_HSLOTS { 139 let t: *NxJpegHTable = nx_jpeg_ctx_htable_at(ctx, s) 140 t.tc = NX_JPEG_TSLOT_FREE 141 t.th = NX_JPEG_TSLOT_FREE 142 t.n_symbols = 0 143 nx_jpeg_ctx_htable_bind(ctx, s) 144 s = s + 1 145 } 146 s = 0 147 while s < NX_JPEG_QSLOTS { 148 let q: *NxJpegQTable = nx_jpeg_ctx_qtable_at(ctx, s) 149 q.tq = NX_JPEG_TSLOT_FREE 150 q.pq = 0 151 q.values = (ctx.qvalue_bufs as i64 + s * NX_JPEG_QVAL_N * NX_JPEG_I64) as *i64 152 s = s + 1 153 } 154 ctx.n_qtables = NX_JPEG_QSLOTS 155 ctx.n_htables = NX_JPEG_HSLOTS 156 return 0 157} 158// the table a scan names, or 0 when that name was never defined 159func nx_jpeg_ctx_htable(ctx: *NxJpegDecCtx, tc: i64, th: i64) -> *NxJpegHTable { 160 if tc < 0 { return 0 as *NxJpegHTable } 161 if tc >= NX_JPEG_TC_CLASSES { return 0 as *NxJpegHTable } 162 if th < 0 { return 0 as *NxJpegHTable } 163 if th >= NX_JPEG_TH_PER_CLASS { return 0 as *NxJpegHTable } 164 let t: *NxJpegHTable = nx_jpeg_ctx_htable_at(ctx, tc * NX_JPEG_TH_PER_CLASS + th) 165 if t.tc == NX_JPEG_TSLOT_FREE { return 0 as *NxJpegHTable } 166 return t 167} 168func nx_jpeg_ctx_qtable(ctx: *NxJpegDecCtx, tq: i64) -> *NxJpegQTable { 169 if tq < 0 { return 0 as *NxJpegQTable } 170 if tq >= NX_JPEG_QSLOTS { return 0 as *NxJpegQTable } 171 let q: *NxJpegQTable = nx_jpeg_ctx_qtable_at(ctx, tq) 172 if q.tq == NX_JPEG_TSLOT_FREE { return 0 as *NxJpegQTable } 173 return q 174} 175func nx_jpeg_copy_i64(dst: *i64, src: *i64, n: i64) -> i64 { 176 var i: i64 = 0 177 while i < n { dst[i] = src[i]; i = i + 1 } 178 return 0 179} 180// parse one DHT payload into the scratch slots and place every table in the slot its (Tc,Th) names; the parser's verdict is 181// returned unchanged, and a refused payload places nothing 182func nx_jpeg_ctx_parse_dht(ctx: *NxJpegDecCtx, payload: *u8, payload_len: i64) -> i64 { 183 let sb: i64 = NX_JPEG_HSLOTS 184 let cnt: *i64 = sys_mmap(NX_JPEG_I64) as *i64 185 let rc: i64 = nx_jpeg_dht_parse(payload, payload_len, nx_jpeg_ctx_htable_at(ctx, sb), 186 (ctx.hbits_pool as i64 + sb * NX_JPEG_HBITS_N * NX_JPEG_I64) as *i64, 187 (ctx.hhuffval_pool as i64 + sb * NX_JPEG_HVAL_N * NX_JPEG_I64) as *i64, 188 (ctx.hmincode_pool as i64 + sb * NX_JPEG_HBITS_N * NX_JPEG_I64) as *i64, 189 (ctx.hmaxcode_pool as i64 + sb * NX_JPEG_HBITS_N * NX_JPEG_I64) as *i64, 190 (ctx.hvalptr_pool as i64 + sb * NX_JPEG_HBITS_N * NX_JPEG_I64) as *i64, cnt) 191 if rc != NX_JPEG_DHT_OK { return rc } 192 var i: i64 = 0 193 while i < cnt[0] { 194 let src: *NxJpegHTable = nx_jpeg_ctx_htable_at(ctx, sb + i) 195 let key: i64 = src.tc * NX_JPEG_TH_PER_CLASS + src.th // the parser bounded tc to 0..1 and th to 0..3 196 let dst: *NxJpegHTable = nx_jpeg_ctx_htable_at(ctx, key) 197 nx_jpeg_ctx_htable_bind(ctx, key) 198 dst.tc = src.tc 199 dst.th = src.th 200 dst.n_symbols = src.n_symbols 201 nx_jpeg_copy_i64(dst.bits, src.bits, NX_JPEG_HBITS_N) 202 nx_jpeg_copy_i64(dst.huffval, src.huffval, NX_JPEG_HVAL_N) 203 nx_jpeg_copy_i64(dst.mincode, src.mincode, NX_JPEG_HBITS_N) 204 nx_jpeg_copy_i64(dst.maxcode, src.maxcode, NX_JPEG_HBITS_N) 205 nx_jpeg_copy_i64(dst.valptr, src.valptr, NX_JPEG_HBITS_N) 206 i = i + 1 207 } 208 return NX_JPEG_DHT_OK 209} 210func nx_jpeg_ctx_parse_dqt(ctx: *NxJpegDecCtx, payload: *u8, payload_len: i64) -> i64 { 211 let sb: i64 = NX_JPEG_QSLOTS 212 let cnt: *i64 = sys_mmap(NX_JPEG_I64) as *i64 213 let rc: i64 = nx_jpeg_dqt_parse(payload, payload_len, nx_jpeg_ctx_qtable_at(ctx, sb), 214 (ctx.qvalue_bufs as i64 + sb * NX_JPEG_QVAL_N * NX_JPEG_I64) as *i64, cnt) 215 if rc != NX_JPEG_DQT_OK { return rc } 216 var i: i64 = 0 217 while i < cnt[0] { 218 let src: *NxJpegQTable = nx_jpeg_ctx_qtable_at(ctx, sb + i) 219 let dst: *NxJpegQTable = nx_jpeg_ctx_qtable_at(ctx, src.tq) // the parser bounded tq to 0..3 220 dst.tq = src.tq 221 dst.pq = src.pq 222 dst.values = (ctx.qvalue_bufs as i64 + src.tq * NX_JPEG_QVAL_N * NX_JPEG_I64) as *i64 223 nx_jpeg_copy_i64(dst.values, src.values, NX_JPEG_QVAL_N) 224 i = i + 1 225 } 226 return NX_JPEG_DQT_OK 227} 228 229// Allocate every working buffer the decoder needs. Caller still 230// owns the planes + strides arrays + the jpeg byte buffer. 231func nx_jpeg_dec_ctx_init(ctx: *NxJpegDecCtx) -> i64 { 232 let hs: i64 = NX_JPEG_HSLOTS + NX_JPEG_HSCRATCH 233 let qs: i64 = NX_JPEG_QSLOTS + NX_JPEG_QSCRATCH 234 ctx.frame = sys_mmap(NX_JPEG_FRAME_BYTES) as *NxJpegFrame 235 ctx.scan = sys_mmap(NX_JPEG_SCAN_BYTES) as *NxJpegScan 236 ctx.sof_comps = sys_mmap(NX_JPEG_SOF_COMP_BYTES * NX_JPEG_COMP_SLOTS) as *NxJpegSofComponent 237 ctx.sos_comps = sys_mmap(NX_JPEG_SOS_COMP_BYTES * NX_JPEG_COMP_SLOTS) as *NxJpegSosComponent 238 ctx.qtables = sys_mmap(NX_JPEG_QTABLE_BYTES * qs) as *NxJpegQTable 239 ctx.qvalue_bufs = sys_mmap(qs * NX_JPEG_QVAL_N * NX_JPEG_I64) as *i64 240 ctx.htables = sys_mmap(NX_JPEG_HTABLE_BYTES * hs) as *NxJpegHTable 241 ctx.hbits_pool = sys_mmap(hs * NX_JPEG_HBITS_N * NX_JPEG_I64) as *i64 242 ctx.hhuffval_pool = sys_mmap(hs * NX_JPEG_HVAL_N * NX_JPEG_I64) as *i64 243 ctx.hmincode_pool = sys_mmap(hs * NX_JPEG_HBITS_N * NX_JPEG_I64) as *i64 244 ctx.hmaxcode_pool = sys_mmap(hs * NX_JPEG_HBITS_N * NX_JPEG_I64) as *i64 245 ctx.hvalptr_pool = sys_mmap(hs * NX_JPEG_HBITS_N * NX_JPEG_I64) as *i64 246 ctx.cstates = sys_mmap(NX_JPEG_DEC_COMPSTATE_BYTES * NX_JPEG_COMP_SLOTS) as *NxJpegDecCompState 247 ctx.frame.components = ctx.sof_comps 248 ctx.scan.components = ctx.sos_comps 249 nx_jpeg_ctx_tables_reset(ctx) 250 return 0 251} 252 253// Top-level decode: walks markers in `jpeg`, gathers metadata, 254// then drives the MCU walker to fill per-component sample planes. 255// 256// jpeg, jpeg_len -- the full JPEG byte buffer 257// ctx -- caller-allocated working state 258// planes -- packed array of *u8 (per scan component) 259// strides -- packed array of i64 (per scan component) 260// 261// Returns NX_JPEG_DEC_TOP_OK on success. 262func nx_jpeg_decode(jpeg: *u8, jpeg_len: i64, 263 ctx: *NxJpegDecCtx, 264 planes: *u8, strides: *i64) -> i64 { 265 let cur: *NxJpegCursor = sys_mmap(NX_JPEG_CURSOR_BYTES) as *NxJpegCursor 266 nx_jpeg_seg_init(cur, jpeg, jpeg_len) 267 let seg: *NxJpegSegment = sys_mmap(NX_JPEG_SEG_BYTES) as *NxJpegSegment 268 269 var saw_soi: i64 = 0 270 var saw_sof: i64 = 0 271 var saw_sos: i64 = 0 272 var entropy_off: i64 = 0 273 var restart_interval: i64 = 0 274 275 var walking: i64 = 1 276 while walking == 1 { 277 let rc: i64 = nx_jpeg_seg_next(cur, seg) 278 if rc == NX_JPEG_SEG_EOF { walking = 0 } 279 if rc != NX_JPEG_SEG_OK { if rc != NX_JPEG_SEG_EOF { walking = 0 } } 280 if walking == 1 { 281 if seg.kind == NX_JPEG_M_SOI { saw_soi = 1 } 282 if seg.kind == NX_JPEG_M_EOI { walking = 0 } 283 if seg.kind == NX_JPEG_M_SOF0 { 284 let sof_rc: i64 = nx_jpeg_sof_parse(jpeg + seg.payload_off, seg.payload_len, ctx.frame) 285 if sof_rc != NX_JPEG_SOF_OK { return NX_JPEG_DEC_TOP_BAD_SOF } 286 saw_sof = 1 287 } 288 if seg.kind == NX_JPEG_M_SOF2 { return NX_JPEG_DEC_TOP_NOT_BASELINE } 289 if seg.kind == NX_JPEG_M_SOF_OTHER { return NX_JPEG_DEC_TOP_NOT_BASELINE } 290 if seg.kind == NX_JPEG_M_DQT { 291 let qrc: i64 = nx_jpeg_ctx_parse_dqt(ctx, jpeg + seg.payload_off, seg.payload_len) 292 if qrc != NX_JPEG_DQT_OK { return NX_JPEG_DEC_TOP_BAD_DQT } 293 } 294 if seg.kind == NX_JPEG_M_DHT { 295 let hrc: i64 = nx_jpeg_ctx_parse_dht(ctx, jpeg + seg.payload_off, seg.payload_len) 296 if hrc != NX_JPEG_DHT_OK { return NX_JPEG_DEC_TOP_BAD_DHT } 297 } 298 if seg.kind == NX_JPEG_M_DRI { 299 // DRI payload = u16 BE restart interval in MCUs (T.81 B.2.4.4); 0 disables. 300 if seg.payload_len >= 2 { 301 restart_interval = ((jpeg[seg.payload_off] as i64) << 8) | (jpeg[seg.payload_off + 1] as i64) 302 } 303 } 304 if seg.kind == NX_JPEG_M_SOS { 305 let srs: i64 = nx_jpeg_sos_parse(jpeg + seg.payload_off, seg.payload_len, ctx.scan) 306 if srs == NX_JPEG_SOS_NOT_BASELINE { return NX_JPEG_DEC_TOP_NOT_BASELINE } 307 if srs != NX_JPEG_SOS_OK { return NX_JPEG_DEC_TOP_BAD_SOS } 308 entropy_off = seg.payload_off + seg.payload_len 309 saw_sos = 1 310 walking = 0 // entropy stream starts here; stop marker walk 311 } 312 } 313 } 314 315 if saw_soi == 0 { return NX_JPEG_DEC_TOP_NO_SOI } 316 if saw_sof == 0 { return NX_JPEG_DEC_TOP_NO_SOF } 317 if saw_sos == 0 { return NX_JPEG_DEC_TOP_NO_SOS } 318 319 let rrc: i64 = nx_jpeg_decode_image_resolve(ctx.frame, ctx.scan, 320 ctx.qtables, ctx.n_qtables, 321 ctx.htables, ctx.n_htables, 322 planes, strides, ctx.cstates) 323 if rrc != NX_JPEG_DEC_OK { return NX_JPEG_DEC_TOP_IMAGE_FAIL } 324 325 let bs: *NxJpegBitStream = sys_mmap(NX_JPEG_BITSTREAM_BYTES) as *NxJpegBitStream 326 bs.src = jpeg 327 bs.src_end = jpeg_len 328 bs.byte_idx = entropy_off 329 bs.bit_off = 0 330 331 let sc: *NxJpegMcuScratch = sys_mmap(NX_JPEG_MCU_SCRATCH_BYTES) as *NxJpegMcuScratch 332 nx_jpeg_mcu_scratch_init(sc) 333 334 let wrc: i64 = nx_jpeg_decode_image_walk_mcus_ri(ctx.frame, ctx.scan, ctx.cstates, bs, sc, restart_interval) 335 if wrc != NX_JPEG_DEC_OK { return NX_JPEG_DEC_TOP_IMAGE_FAIL } 336 return NX_JPEG_DEC_TOP_OK 337}