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}