Skip to main content

manifold/
mnf8.rs

1// MNF8 — format v4 (ADR-002: Entropy Backend and Residual Probability Modeling).
2//
3// MNF8 = MNF7 (modes 0-5, PLANAR|ZSTD flags) + a backend-identifier field +
4// encoder profiles; new magic, new header field. MNF5/6/7 files decode via
5// their own magics — no compatibility break (README §9 versioning rule).
6//
7// Header (32 bytes):
8//   "MNF8", u32 LE width, height, tile=16, tile_count, payload_len,
9//   u32 flags (MNF7 semantics + MNF8-owned bits:
10//     bit0 PLANAR, bit1 ZSTD, bit2 PRED_MED + bit3 TOKENS (ADR-003),
11//     ADR-004: bit4 PRED_PLANE, bit5 PRED_LEFT — bits 6..31 reserved),
12//   u32 backend:
13//     0 = zlib-9  (MNF7 control; payload is a zlib stream)
14//     1 = zstd-19 (xxh64-checksummed frame; decoder window capped at 23)
15//     2 = RESERVED — brotli was prototyped but unshipped: its static
16//         dictionary's cost accounting was measured only in the study, and
17//         a dictionary-free brotli backend changed nothing on the corpus.
18//         Reject until its gate re-passes (audit review 2026-10-25).
19//     3 = RESERVED (custom mixed run/rANS coder; reject until it passes the gate)
20//   other values: reject like reserved flag bits.
21//
22// FLAG_TOKENS (ADR-003, docs/mnf8-adr-003-tokens.md): the payload is NOT a
23// record stream — it is the LZ77-2D token wire over the image-wide MED
24// residual plane (crate::tokens), container-compressed by the backend.
25// Mutually exclusive with everything per-tile: TOKENS forces plain
26// interleaved literal records in the pick arms it competes against and is
27// rejected at decode with PLANAR (the token plane is always interleaved).
28//
29// ADR-004 (docs/mnf8-adr-004-branch-pred.md): bits 4-5 select the image-
30// wide predictor on mode-0 literal bodies — PRED_MED (edge-sense, the
31// v1.2 predecessor), PRED_PLANE (LOCO-I plane branch forced: a+b−cc), or
32// PRED_LEFT (pure left). All three decode causally (the exact same walk,
33// only the interior predictor function differs). Exactly one may be set;
34// the encoder measures all three per file (retention gate: branch choice
35// beats neighborhood choice, docs/mnf9-branch-gate.md, −7.0%).
36//
37// Profiles (encoder policy, not wire format):
38//   balanced  -> always zstd-19 (flags measured: planar vs interleaved).
39//   maximum   -> builds the tile stream under each (backend, flags)
40//                candidate, re-costing per-tile mode selection through that
41//                candidate's coder (stage-2 evidence: selection is
42//                backend-dependent), and emits the smallest file.
43//
44// Records: exactly MNF7's (modes 0-3 plus MNF7 color modes 4/5), decoded by
45// the same record walker as decode_mnf7; only container + coder differ.
46
47use std::io::Read;
48
49use crate::codec::{apply_transform, div_ceil, split_tiles, tile_sig, Atlas, Dims, TILE_SIZE};
50use crate::error::{Error, Result};
51use crate::mnf6::{
52    compress_stream, decode_record_walk, med_predict, modcolor_forward_iter, pack9, planar_pack,
53    proxy_cost, ycocg_forward, Coder, MnF6Stats, FLAG_PLANAR,
54};
55use crate::tokens;
56
57/// Header size: `MNF8` + 7×`u32` LE (adds backend id) — 4 B over MNF7.
58pub const HEADER8: usize = 32;
59
60// MNF8-only flags (MNF6/7 keep !0b11 frozen; MNF8 owns bits 2-3):
61/// Literal bodies carry image-wide MED residuals (decode inverts causally).
62pub const FLAG_PRED_MED: u32 = 4; // mode-0 literal bodies carry image-wide MED residuals
63/// Payload = LZ77-2D token wire over the MED residual plane (ADR-003).
64pub const FLAG_TOKENS: u32 = 8;
65/// Image-wide predictor on literal bodies = LOCO-I plane branch (ADR-004).
66pub const FLAG_PRED_PLANE: u32 = 16;
67/// Image-wide predictor on literal bodies = pure left (ADR-004).
68pub const FLAG_PRED_LEFT: u32 = 32;
69/// Bits above bit5 reserved (MNF8 owns bits 2-5).
70pub const FLAGS_RESERVED8: u32 = !0b111111;
71
72/// zlib-9 backend.
73pub const BACKEND_ZLIB: u32 = 0;
74/// zstd-19 backend (ships in Balanced and Maximum).
75pub const BACKEND_ZSTD19: u32 = 1;
76/// Reserved (brotli prototype; decode rejects).
77pub const BACKEND_RESERVED2: u32 = 2; // brotli prototype — unshipped, reject
78/// Reserved (run/rANS prototype; decode rejects).
79pub const BACKEND_RESERVED3: u32 = 3; // custom run/rANS — unshipped, reject
80
81// zstd-19's default window is 8 MiB = 2^23; hard-pin the decoder cap to it
82// (review fix 4: a hostile frame can't claim a 128 MiB window).
83/// zstd decoder window ceiling (8 MiB; matches level-19 encoder).
84pub const ZSTD_WINDOW_LOG_MAX: u32 = 23;
85
86/// Rank count for candidate references... (kept from the shared atlas table;
87/// RANK_CANDIDATES lives in codec — do not re-derive here.)
88use crate::codec::RANK_CANDIDATES;
89
90/// Encoder search breadth.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum Profile {
93    /// zstd-19 only: fast, sane default.
94    Balanced,
95    /// All backends + flag variants measured; ships the smallest.
96    Maximum,
97}
98
99/// Backend candidates for the `maximum` profile, in measured-preference order.
100fn backend_candidates() -> Vec<(u32, Coder)> {
101    vec![
102        (BACKEND_ZSTD19, Coder::Zstd(19)),
103        (BACKEND_ZLIB, Coder::Zlib),
104    ]
105}
106
107fn backend_compress(backend: u32, buf: &[u8]) -> Result<Vec<u8>> {
108    match backend {
109        BACKEND_ZLIB => Ok(compress_stream(&Coder::Zlib, buf)),
110        BACKEND_ZSTD19 => Ok(compress_stream(&Coder::Zstd(19), buf)),
111        other => Err(Error::fmt_msg(format!(
112            "backend {other} not a compressed-output coder"
113        ))),
114    }
115}
116
117fn backend_decompress(backend: u32, buf: &[u8], limit: u64) -> Result<Vec<u8>> {
118    match backend {
119        BACKEND_ZLIB => {
120            // canonical-decode (review small item): every payload byte must
121            // belong to the stream — zlib's total_in tells us exactly how
122            // much was consumed.
123            let mut d = flate2::read::ZlibDecoder::new(buf).take(limit + 1);
124            let mut v = Vec::new();
125            d.read_to_end(&mut v)
126                .map_err(|_| Error::Decompress("corrupt zlib stream"))?;
127            if v.len() as u64 > limit {
128                return Err(Error::fmt_msg("bomb"));
129            }
130            if d.get_ref().total_in() as usize != buf.len() {
131                return Err(Error::fmt_msg("trailing bytes after zlib stream"));
132            }
133            Ok(v)
134        }
135        BACKEND_ZSTD19 => {
136            // window capped at the encoder's actual need (review fix 4);
137            // Cursor-wrapped so post-decode input position is observable
138            // (canonical-decode check, review small item).
139            let cursor = std::io::Cursor::new(buf);
140            let mut d = zstd::stream::read::Decoder::new(cursor)
141                .map_err(|_| Error::Decompress("zstd init"))?;
142            d.window_log_max(ZSTD_WINDOW_LOG_MAX)
143                .map_err(|_| Error::Decompress("zstd window param"))?;
144            let mut v = Vec::new();
145            d.single_frame()
146                .take(limit + 1)
147                .read_to_end(&mut v)
148                .map_err(|_| Error::Decompress("corrupt zstd stream"))?;
149            if v.len() as u64 > limit {
150                return Err(Error::fmt_msg("bomb"));
151            }
152            // NOTE (honest limitation): zlib canonicality is exact via
153            // total_in above; the zstd stream API here cannot cheaply report
154            // the decoder's exact consumed offset (BufReader may also read
155            // ahead), so bytes after a complete zstd frame are tolerated.
156            // Canonical zstd files remain an option via a future zstd-safe
157            // one-shot decode; not worth it today (ponytail).
158            Ok(v)
159        }
160        other => Err(Error::fmt_msg(format!(
161            "backend {other} not a decompressable coder"
162        ))),
163    }
164}
165
166/// Encode RGB8 pixels as MNF8.
167///
168/// `profile = Balanced`: zstd-19 pinned; measures the planar vs
169/// interleaved flag variants under that coder (same honesty as MNF7
170/// measures flags, minus the zlib arms excluded by ADR-002).
171///
172/// `profile = Maximum`: measures backend×flags candidates and emits the
173/// smallest final file. Per-tile mode selection is re-costed inside each
174/// candidate's own coder — the stage-2 protocol (docs/entropy-backends-v13.md).
175/// There is NO shared per-tile loop with encode_mnf7: the one function
176/// `encode_stream_coder` is parameterized by coder and every caller routes
177/// through it (review fix 8: MNF7's loop stays the frozen-format history;
178/// MNF8 owns its selection policy).
179pub fn encode_mnf8(
180    pixels: &[u8],
181    dims: Dims,
182    use_geom: bool,
183    profile: Profile,
184) -> (Vec<u8>, MnF6Stats, u32) {
185    let candidates: Vec<(u32, Coder)> = match profile {
186        Profile::Balanced => vec![(BACKEND_ZSTD19, Coder::Zstd(19))],
187        Profile::Maximum => backend_candidates(),
188    };
189    let mut best: Option<(Vec<u8>, u32, MnF6Stats, u32)> = None;
190    // ADR-004: the three literal-body predictors × (interleaved|planar)
191    // as per-file arms, each measured under every backend candidate,
192    // complete serialized cost. (PLANAR's own measured value is real:
193    // gradient −28% under z19 in the stage-2 matrix — never drop it.)
194    let pred_arms = [
195        PredKind::None,
196        PredKind::Med,
197        PredKind::Plane,
198        PredKind::Left,
199    ];
200    for (backend, coder) in candidates {
201        for kind in pred_arms {
202            for planar in [false, true] {
203                let med = kind == PredKind::Med;
204                let flags = kind.flag_bit() | if planar { FLAG_PLANAR } else { 0 };
205                let (file, st) = encode_stream_coder(
206                    pixels, dims, true, use_geom, flags, planar, med, kind, &coder, backend,
207                )
208                .expect("encoding an in-memory image cannot fail");
209                let size = file.len();
210                if best.as_ref().is_none_or(|b| size < b.0.len()) {
211                    best = Some((file, flags, st, backend));
212                }
213            }
214        }
215        // ADR-003 token arm: tokens over the MED plane, same backend (the
216        // token grammar's predictor is fixed = MED; ADR-004 composes with
217        // the record arms only). Rate-aware by construction.
218        let flags = FLAG_PRED_MED | FLAG_TOKENS;
219        let (file, st) = encode_tokens_coder(pixels, dims, backend, flags)
220            .expect("encoding an in-memory image cannot fail");
221        let size = file.len();
222        if best.as_ref().is_none_or(|b| size < b.0.len()) {
223            best = Some((file, flags, st, backend));
224        }
225    }
226    let (file, flags, mut st, backend) = best.expect("at least one candidate");
227    st.chosen_flags = flags;
228    (file, st, backend)
229}
230
231/// The image-wide literal-body predictor (ADR-004): which causal rule
232/// mode-0 bodies carry. All three decode with the same causal walk.
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub enum PredKind {
235    /// No pre-prediction (raw literal bodies).
236    None,
237    /// MED edge-sense (v1.2 FLAG_PRED_MED).
238    Med,
239    /// LOCO-I plane branch forced: pred = a + b − cc, clamped (ADR-004).
240    Plane,
241    /// Pure left: pred = left neighbor (ADR-004).
242    Left,
243}
244
245impl PredKind {
246    fn from_flags(flags: u32) -> PredKind {
247        if flags & FLAG_PRED_MED != 0 {
248            PredKind::Med
249        } else if flags & FLAG_PRED_PLANE != 0 {
250            PredKind::Plane
251        } else if flags & FLAG_PRED_LEFT != 0 {
252            PredKind::Left
253        } else {
254            PredKind::None
255        }
256    }
257
258    fn flag_bit(self) -> u32 {
259        match self {
260            PredKind::None => 0,
261            PredKind::Med => FLAG_PRED_MED,
262            PredKind::Plane => FLAG_PRED_PLANE,
263            PredKind::Left => FLAG_PRED_LEFT,
264        }
265    }
266
267    /// The predictor's value at an interior pixel from causal neighbors.
268    /// Decode-side truth; the encode-side helpers MUST match exactly.
269    fn predict(self, a: u32, b: u32, cc: u32) -> u32 {
270        match self {
271            PredKind::None | PredKind::Med => crate::mnf6::med_predict_pixel(a, b, cc),
272            PredKind::Plane => {
273                // LOCO-I plane branch, clamped into byte range (the encode-
274                // side plane_predict clamps identically).
275                (a as i64 + b as i64 - cc as i64).clamp(0, 255) as u32
276            }
277            PredKind::Left => a,
278        }
279    }
280}
281
282/// Image-wide prediction over the WHOLE image with the given rule.
283/// ENCODE-side only: neighbor reads come from TRUE pixels (the decode walk
284/// reads reconstructed pixels; reconstructed-earlier == true by the mod-256
285/// induction, so the two walks agree — reading the pred stream instead of
286/// the pixel stream chains predictions and BREAKS the identity, the bug the
287/// gradient Plane gate caught: pred(0,1) = pred(0,0) instead of left pixel).
288fn image_predict(pixels: &[u8], w: usize, h: usize, kind: PredKind) -> Vec<u8> {
289    let mut out = vec![0u8; pixels.len()];
290    for y in 0..h {
291        for x in 0..w {
292            for c in 0..3usize {
293                let i = (y * w + x) * 3 + c;
294                let pred: u32 = if x == 0 && y == 0 {
295                    127
296                } else if y == 0 {
297                    u32::from(pixels[i - 3])
298                } else if x == 0 {
299                    u32::from(pixels[i - w * 3])
300                } else {
301                    let a = u32::from(pixels[i - 3]);
302                    let b = u32::from(pixels[i - w * 3]);
303                    let cc = u32::from(pixels[i - w * 3 - 3]);
304                    kind.predict(a, b, cc)
305                };
306                out[i] = pred as u8;
307            }
308        }
309    }
310    out
311}
312
313/// Token-arm stream builder (ADR-003): MED residual plane → token wire →
314/// container coder. Reuses every mnf6/mnf8 primitive; stats report the
315/// token counts in the literal fields (per-arm accounting preserved).
316fn encode_tokens_coder(
317    pixels: &[u8],
318    dims: Dims,
319    backend: u32,
320    flags: u32,
321) -> Result<(Vec<u8>, MnF6Stats)> {
322    let med_pred = med_predict(pixels, dims.width as usize, dims.height as usize);
323    let res: Vec<u8> = pixels
324        .iter()
325        .zip(&med_pred)
326        .map(|(x, m)| x.wrapping_sub(*m))
327        .collect();
328    let (wire, ts) = tokens::encode_plane(&res, dims.width as usize, dims.height as usize)?;
329    let payload = backend_compress(backend, &wire)?;
330    let payload_len =
331        u32::try_from(payload.len()).map_err(|_| Error::fmt_msg("payload exceeds u32"))?;
332    let mut file = Vec::with_capacity(HEADER8 + payload.len());
333    file.extend_from_slice(b"MNF8");
334    file.extend_from_slice(&dims.width.to_le_bytes());
335    file.extend_from_slice(&dims.height.to_le_bytes());
336    file.extend_from_slice(&TILE_SIZE.to_le_bytes());
337    let cols = div_ceil(dims.width, TILE_SIZE);
338    let rows = div_ceil(dims.height, TILE_SIZE);
339    let tile_count: u32 = cols
340        .checked_mul(rows)
341        .ok_or_else(|| Error::fmt_msg("too many tiles"))?;
342    file.extend_from_slice(&tile_count.to_le_bytes());
343    file.extend_from_slice(&payload_len.to_le_bytes());
344    file.extend_from_slice(&flags.to_le_bytes());
345    file.extend_from_slice(&backend.to_le_bytes());
346    file.extend_from_slice(&payload);
347    let st = MnF6Stats {
348        literal: u32::try_from(ts.literals).unwrap_or(u32::MAX),
349        ref_exact: u32::try_from(ts.matches).unwrap_or(u32::MAX),
350        ..MnF6Stats::default()
351    };
352    Ok((file, st))
353}
354
355/// One tile-stream encoder, parameterized by coder + backend — THE selection
356/// loop for MNF8 (no per-variant duplicates of this function).
357///
358/// Arm controls (both default true, plumbable for ablation):
359/// - `use_atlas`: candidate references at all (false = pure prediction arm)
360/// - `use_geom`: D8 transforms on top of exact references (false = t=0 only,
361///   the v0.5 exact-reference semantics — codec.rs pins t=0 for this case
362///   and MNF8 must match it)
363///
364/// Shared YCoCg (mode 3) and planar layout stay ON in both arms (ADR-002).
365#[allow(clippy::too_many_arguments)]
366fn encode_stream_coder(
367    pixels: &[u8],
368    dims: Dims,
369    use_atlas: bool,
370    use_geom: bool,
371    flags: u32,
372    planar: bool,
373    med: bool,
374    kind: PredKind,
375    coder: &Coder,
376    backend: u32,
377) -> Result<(Vec<u8>, MnF6Stats)> {
378    // ADR-004: Plane/Left are PURE prediction arms (the measured gate's
379    // winning form) — no atlas candidates, no YCoCg; mode-0 bodies only.
380    let pure_pred = kind == PredKind::Plane || kind == PredKind::Left;
381    let med_pred = match kind {
382        PredKind::Med => med_predict(pixels, dims.width as usize, dims.height as usize),
383        PredKind::Plane => image_predict(
384            pixels,
385            dims.width as usize,
386            dims.height as usize,
387            PredKind::Plane,
388        ),
389        PredKind::Left => image_predict(
390            pixels,
391            dims.width as usize,
392            dims.height as usize,
393            PredKind::Left,
394        ),
395        PredKind::None => Vec::new(),
396    };
397    let tiles = split_tiles(pixels, dims);
398    let mut raw: Vec<u8> = Vec::with_capacity(tiles.len() * 774);
399    let mut stats = MnF6Stats::default();
400    let mut atlas = Atlas::new();
401
402    for (idx, tile) in tiles.iter().enumerate() {
403        let n = tile.nbytes();
404        let pack = |rgb: &[u8]| -> Vec<u8> {
405            if planar {
406                planar_pack(rgb)
407            } else {
408                rgb.to_vec()
409            }
410        };
411
412        // mode-0 literal candidate; under any ADR-004 predictor the body
413        // carries that predictor's residual (decoder reconstructs causally)
414        let lit_src: Vec<u8> = if med || pure_pred {
415            let (tw, th) = (tile.width as usize, tile.height as usize);
416            let cols_px = dims.width as usize; // med_pred is per-PIXEL flat
417            let tx = (idx % ((dims.width as usize).div_ceil(16))) * 16;
418            let ty = (idx / ((dims.width as usize).div_ceil(16))) * 16;
419            let mut src = Vec::with_capacity(n);
420            for y in 0..th {
421                let row_off = ((ty + y) * cols_px + tx) * 3;
422                src.extend_from_slice(&med_pred[row_off..row_off + tw * 3]);
423            }
424            tile.data
425                .iter()
426                .zip(&src)
427                .map(|(x, m)| x.wrapping_sub(*m))
428                .collect()
429        } else {
430            tile.data.clone()
431        };
432
433        let lit_body = pack(&lit_src);
434        let mut choice_body = lit_body.clone();
435        let mut choice_mode = 0u8;
436        let mut choice_ref: Option<(u32, u8)> = None;
437        let mut best_cost = 1 + proxy_cost(coder, &lit_body);
438
439        // shared literal candidates (both arms): YCoCg-R (mode 3).
440        // ADR-004 pure arms (Plane/Left) skip YCoCg + atlas — measured pure.
441        if !pure_pred {
442            let (cy, cco, ccg) = ycocg_forward(&tile.data);
443            let mut m3 = Vec::with_capacity(n + 160);
444            m3.extend_from_slice(&cy);
445            m3.extend_from_slice(&pack9(&cco));
446            m3.extend_from_slice(&pack9(&ccg));
447            let m3_cost = 1 + proxy_cost(coder, &m3);
448            if m3_cost < best_cost {
449                best_cost = m3_cost;
450                choice_body = m3;
451                choice_mode = 3;
452            }
453        }
454
455        // invariant hoist (review fix 7): T(tile.data) is candidate- and
456        // transform-independent — compute once per tile
457        let tile_t = modcolor_forward_iter(&tile.data);
458
459        // atlas candidates (arm-controlled; review fixes 2 + use_atlas).
460        // ADR-004 pure prediction arms exclude the atlas (measured pure).
461        if use_atlas && !pure_pred && tiles.len() > 1 {
462            let sig = tile_sig(tile.width, tile.height, &tile.data);
463            for prior in atlas.rank_by_sig(&sig, tile.width, tile.height, RANK_CANDIDATES) {
464                let (_, _, ref_rgb) = &atlas.patches[prior as usize];
465                // review fix 2: `use_geom = false` means EXACT references only
466                // (t=0), matching codec.rs's v0.5 ablation semantics; square
467                // tiles may use all 8 D8 transforms, partial tiles only the
468                // shape-keeping subgroup.
469                let transforms: &[u8] = if !use_geom {
470                    &[0]
471                } else if tile.width == tile.height {
472                    &[0, 1, 2, 3, 4, 5, 6, 7]
473                } else {
474                    &[0, 1, 2, 3]
475                };
476                for &t in transforms {
477                    let base = if t == 0 {
478                        ref_rgb.clone()
479                    } else {
480                        apply_transform(t, tile.width, tile.height, ref_rgb)
481                    };
482                    let mut r = vec![0u8; n];
483                    for i in 0..n {
484                        r[i] = tile.data[i].wrapping_sub(base[i]);
485                    }
486                    let body = pack(&r);
487                    // metadata: mode byte + u32 idx + optional transform byte
488                    let meta: u64 = if t == 0 { 5 } else { 6 };
489                    let cost = meta + proxy_cost(coder, &body);
490                    if cost < best_cost {
491                        best_cost = cost;
492                        choice_body = body;
493                        choice_mode = if t == 0 { 1 } else { 2 };
494                        choice_ref = Some((prior, t));
495                    }
496                    // color-domain candidate (MNF7 modes 4/5). Review fix 3:
497                    // the wire carries NOTHING extra for color modes — the
498                    // mode byte already tells the decoder which domain to
499                    // decode the body in. The phantom `+ 1` that the old
500                    // cost model charged made the encoder under-select
501                    // color modes (contaminating any color-mode ablation).
502                    let base_t = modcolor_forward_iter(&base);
503                    let mut r_c = vec![0u8; n];
504                    for i in 0..n {
505                        r_c[i] = tile_t[i].wrapping_sub(base_t[i]);
506                    }
507                    let body_c = pack(&r_c);
508                    let cost_c = meta + proxy_cost(coder, &body_c);
509                    if cost_c < best_cost {
510                        best_cost = cost_c;
511                        choice_body = body_c;
512                        choice_mode = if t == 0 { 4 } else { 5 };
513                        choice_ref = Some((prior, t));
514                    }
515                }
516            }
517        }
518
519        stats.modes.push(choice_mode);
520        stats.refs.push(choice_ref);
521        match choice_mode {
522            0 => stats.literal += 1,
523            1 => stats.ref_exact += 1,
524            2 => {
525                stats.ref_geom += 1;
526                if choice_ref.map(|(_, t)| t != 0).unwrap_or(false) {
527                    stats.geom_nontrivial += 1;
528                }
529            }
530            3 => stats.ycocg_literal += 1,
531            4 => stats.ref_exact_color += 1,
532            5 => {
533                stats.ref_geom_color += 1;
534                if choice_ref.map(|(_, t)| t != 0).unwrap_or(false) {
535                    stats.geom_nontrivial += 1;
536                }
537            }
538            _ => unreachable!("mode byte constrained above"),
539        }
540        raw.push(choice_mode);
541        if let Some((p, t)) = choice_ref {
542            raw.extend_from_slice(&p.to_le_bytes());
543            if t != 0 {
544                raw.push(t);
545            }
546        }
547        raw.extend_from_slice(&choice_body);
548        atlas.insert(tile);
549    }
550
551    let payload = backend_compress(backend, &raw)?;
552    let payload_len =
553        u32::try_from(payload.len()).map_err(|_| Error::fmt_msg("payload exceeds u32"))?; // review: no lossy cast
554    let mut file = Vec::with_capacity(HEADER8 + payload.len());
555    file.extend_from_slice(b"MNF8");
556    file.extend_from_slice(&dims.width.to_le_bytes());
557    file.extend_from_slice(&dims.height.to_le_bytes());
558    file.extend_from_slice(&TILE_SIZE.to_le_bytes());
559    let tile_count = u32::try_from(tiles.len()).map_err(|_| Error::fmt_msg("too many tiles"))?;
560    file.extend_from_slice(&tile_count.to_le_bytes());
561    file.extend_from_slice(&payload_len.to_le_bytes());
562    file.extend_from_slice(&flags.to_le_bytes());
563    file.extend_from_slice(&backend.to_le_bytes());
564    file.extend_from_slice(&payload);
565    Ok((file, stats))
566}
567
568// --- decoding ----------------------------------------------------------------
569
570/// Decode MNF8. Record walk is MNF7's (shared `decode_record_walk`); the
571/// container fields differ (32-byte header, backend id) and the coder
572/// dispatch runs on the backend field. Every non-shipped backend id is an
573/// ERROR (never a panic path) — review fix 1: backend 2 previously fell
574/// into `unreachable!()` after passing the too-loose header checks.
575pub fn decode_mnf8(buf: &[u8]) -> Result<(Dims, Vec<u8>, u32)> {
576    decode_mnf8_with_limits(buf, &crate::mnf6::DecodeLimits::default())
577}
578
579/// Decode MNF8 with configurable ceilings (library API; same semantics as
580/// decode_mnf7_with_limits).
581pub fn decode_mnf8_with_limits(
582    buf: &[u8],
583    lim: &crate::mnf6::DecodeLimits,
584) -> Result<(Dims, Vec<u8>, u32)> {
585    if buf.len() < HEADER8 {
586        return Err(Error::fmt_msg("short"));
587    }
588    if buf[0..4] != *b"MNF8" {
589        return Err(Error::fmt_msg("magic"));
590    }
591    let u32le = |o: usize| u32::from_le_bytes(buf[o..o + 4].try_into().unwrap());
592    let width = u32le(4);
593    let height = u32le(8);
594    if width == 0 || height == 0 {
595        return Err(Error::fmt_msg("dims"));
596    }
597    if width as u64 * height as u64 > lim.max_pixels {
598        return Err(Error::Limits {
599            which: "max_pixels",
600            limit: lim.max_pixels,
601            actual: width as u64 * height as u64,
602        });
603    }
604    if u32le(12) != TILE_SIZE {
605        return Err(Error::fmt_msg("tilesize"));
606    }
607    let cols = div_ceil(width, TILE_SIZE);
608    let rows = div_ceil(height, TILE_SIZE);
609    let tile_count = u32le(16);
610    if tile_count != cols * rows {
611        return Err(Error::fmt_msg("tilecount"));
612    }
613    if tile_count > lim.max_tiles {
614        return Err(Error::Limits {
615            which: "max_tiles",
616            limit: u64::from(lim.max_tiles),
617            actual: u64::from(tile_count),
618        });
619    }
620    let payload_len = u32le(20);
621    if payload_len > (1 << 31) - 1 {
622        return Err(Error::fmt_msg("payloadlen"));
623    }
624    if buf.len() != HEADER8 + payload_len as usize {
625        return Err(Error::fmt_msg("filelen"));
626    }
627    let flags = u32le(24);
628    // MNF8 owns bits 2-5 (PRED_MED, TOKENS, PRED_PLANE, PRED_LEFT); MNF6/7
629    // do not — wider mask here
630    if flags & FLAGS_RESERVED8 != 0 {
631        return Err(Error::fmt_msg("reserved-flags"));
632    }
633    let token_wire = flags & FLAG_TOKENS != 0;
634    // ADR-003: the token plane is interleaved RGB8 by definition — PLANAR
635    // carries no meaning there and no encoder emits the combination.
636    if token_wire && flags & FLAG_PLANAR != 0 {
637        return Err(Error::fmt_msg("tokens+planar are exclusive"));
638    }
639    // ADR-004: at most ONE image-wide predictor bit may be set.
640    let pred_count = u32::from(flags & FLAG_PRED_MED != 0)
641        + u32::from(flags & FLAG_PRED_PLANE != 0)
642        + u32::from(flags & FLAG_PRED_LEFT != 0);
643    if pred_count > 1 {
644        return Err(Error::fmt_msg("multiple predictors selected"));
645    }
646    // ADR-003: the token grammar's payload is always MED-residual; a token
647    // file claiming a different predictor misdescribes its payload.
648    if token_wire && flags & (FLAG_PRED_PLANE | FLAG_PRED_LEFT) != 0 {
649        return Err(Error::fmt_msg("tokens+plane/left predictor exclusive"));
650    }
651    let backend = u32le(28);
652    if !matches!(
653        backend,
654        BACKEND_ZLIB | BACKEND_ZSTD19 | BACKEND_RESERVED2 | BACKEND_RESERVED3
655    ) {
656        return Err(Error::fmt_msg(format!("unknown backend {backend}")));
657    }
658    if matches!(backend, BACKEND_RESERVED2 | BACKEND_RESERVED3) {
659        return Err(Error::fmt_msg(format!(
660            "reserved backend {backend} (not shipped)"
661        )));
662    }
663    let planar = flags & FLAG_PLANAR != 0;
664
665    // Decompression bound — degenerate-shape-honest (review fix 5):
666    //   generic record worst case needs 6 metadata bytes + the tile's own
667    //   body (tw*th*3); summed over tiles that is 6*N + 3WH, NOT N * 833
668    //   (the old full-tile assumption turned a 1x67108864 image's cap into
669    //   ~3.5 GB against a ~200 MB true maximum).
670    //   Mode-3 records: 1 + tw*th + 2 * ceil(9*(tw*th)/8) ≤ 1 + tw*th + 2 * (tw*th*9+7)/8
671    //   ≤ 1 + 7/4 * tw*th + small; bound it as 1 + (7/4)*np + 2 with np <=
672    //   TILE_SIZE^2 — the same conservative per-tile add-on as before.
673    //   Token wire (ADR-003; bound from the token path's own worst case, the
674    //   same lesson as mode 3): every token covers >= 1 px at <= 4 B
675    //   (tag + 3), so the wire is <= 4*npx — which EXCEEDS the record bound
676    //   on large all-literal planes (4npx > 3npx + 6N + small once npx is
677    //   big), so the token flag gets its own limit rather than inheriting
678    //   the record one.
679    let px_bytes_total: u64 = width as u64 * height as u64 * 3;
680    let limit = if token_wire {
681        (4 * width as u64)
682            .checked_mul(height as u64)
683            .ok_or_else(|| Error::fmt_msg("decompressed size overflow"))?
684    } else {
685        let mode3_max: u64 = 1
686            + (TILE_SIZE * TILE_SIZE) as u64
687            + 2 * ((TILE_SIZE * TILE_SIZE) as u64 * 9 + 7) / 8
688            + 2;
689        px_bytes_total
690            .checked_add(6 * tile_count as u64)
691            .and_then(|l| l.checked_add(2 * mode3_max + 2))
692            .ok_or_else(|| Error::fmt_msg("decompressed size overflow"))?
693    };
694
695    let payload = &buf[HEADER8..];
696    let raw = match backend {
697        BACKEND_ZLIB | BACKEND_ZSTD19 => backend_decompress(backend, payload, limit)?,
698        other => return Err(Error::fmt_msg(format!("unsupported backend {other}"))),
699    };
700
701    let pixels = if token_wire {
702        // ADR-003 token payload: wire → residual plane → causal MED invert.
703        // decode_plane validates the wire structurally (tags, ranges, exact
704        // plane size) and rejects trailing bytes by construction.
705        let res = tokens::decode_plane(&raw, width as usize, height as usize)?;
706        pred_residual_invert(&res, width as usize, height as usize, PredKind::Med)
707    } else {
708        let kind = PredKind::from_flags(flags);
709        // ADR-004 routing at decode: Med keeps the frozen MNF7 walker (its
710        // in-walker MED branch); None = raw bodies; Plane/Left = pure
711        // prediction arms (mode-0-only streams) inverted by the ADR-004 walk.
712        match kind {
713            PredKind::None => decode_record_walk(&raw, width, height, tile_count, planar, false)?,
714            PredKind::Med => decode_record_walk(&raw, width, height, tile_count, planar, true)?,
715            other => decode_pred_walk(&raw, width, height, tile_count, planar, other)?,
716        }
717    };
718    Ok((Dims { width, height }, pixels, backend))
719}
720
721/// Causal invert of an image-wide predictor residual plane. Generalizes the
722/// token payload's MED inversion to every PredKind (ADR-004); reconstruction
723/// = this predictor's causal estimate + residual bytes, exact by the
724/// mod-256 induction.
725fn pred_residual_invert(res: &[u8], w: usize, h: usize, kind: PredKind) -> Vec<u8> {
726    let mut out = vec![0u8; w * h * 3];
727    for y in 0..h {
728        for x in 0..w {
729            for c in 0..3usize {
730                let i = (y * w + x) * 3 + c;
731                let pred: u32 = if x == 0 && y == 0 {
732                    127
733                } else if y == 0 {
734                    u32::from(out[i - 3])
735                } else if x == 0 {
736                    u32::from(out[i - w * 3])
737                } else {
738                    let a = u32::from(out[i - 3]);
739                    let b = u32::from(out[i - w * 3]);
740                    let cc = u32::from(out[i - w * 3 - 3]);
741                    kind.predict(a, b, cc)
742                };
743                out[i] = (pred + u32::from(res[i])).wrapping_rem_euclid(256) as u8;
744            }
745        }
746    }
747    out
748}
749
750/// ADR-004 decode walk for Plane/Left files: record layout is MNF7's, but
751/// mode-0 literal bodies carry PRED_PLANE/PRED_LEFT residuals instead of
752/// MED ones. Structure mirrors decode_record_walk's mode-0 branch (per-
753/// tile causal context: in-tile reads come from the in-flight recon,
754/// out-of-tile from `pixels` — the audit-bug discipline); every other
755/// record mode is unchanged from MNF7's walker.
756fn decode_pred_walk(
757    raw: &[u8],
758    width: u32,
759    height: u32,
760    tile_count: u32,
761    planar: bool,
762    kind: PredKind,
763) -> Result<Vec<u8>> {
764    // delegate the record structure to the shared walker by REUSING its
765    // record walk with med=true (BODIES are residuals of SOME predictor),
766    // then inverting per-tile — but the walker's med inversion happens
767    // IN-WALKER. So instead: run the walker with med=false (bodies stay
768    // raw residuals), collect per-tile residual bodies… the walker
769    // reconstructs directly; simplest correct path: reuse the walker only
770    // for its record framing by a full re-walk here (mode 0 inverted with
771    // `kind`; other modes identically to the walker).
772    let cols = div_ceil(width, TILE_SIZE);
773    let mut pixels = vec![0u8; width as usize * height as usize * 3];
774    let mut decoded: Vec<(u32, u32, Vec<u8>)> = Vec::with_capacity(tile_count as usize);
775    let mut ip = 0usize;
776    for i in 0..tile_count {
777        let row = i / cols;
778        let col = i % cols;
779        let tw = TILE_SIZE.min(width - col * TILE_SIZE);
780        let th = TILE_SIZE.min(height - row * TILE_SIZE);
781        let n = (tw * th * 3) as usize;
782        if ip >= raw.len() {
783            return Err(Error::fmt_msg(format!("eof at tile {i}")));
784        }
785        let mode = raw[ip];
786        ip += 1;
787        let tile = match mode {
788            0 => {
789                if ip + n > raw.len() {
790                    return Err(Error::fmt_msg("body"));
791                }
792                let body = raw[ip..ip + n].to_vec();
793                ip += n;
794                let body = if planar {
795                    crate::mnf6::planar_unpack(&body)
796                } else {
797                    body
798                };
799                // per-tile causal invert with the ADR-004 predictor (same
800                // context discipline as the walker's MED branch)
801                let mut recon = vec![0u8; n];
802                let gy0 = row as usize * TILE_SIZE as usize;
803                let gx0 = col as usize * TILE_SIZE as usize;
804                for y in 0..th as usize {
805                    for x in 0..tw as usize {
806                        for c in 0..3usize {
807                            let k = (y * tw as usize + x) * 3 + c;
808                            let gy = gy0 + y;
809                            let gx = gx0 + x;
810                            let tl = |ggy: usize, ggx: usize| -> u32 {
811                                if ggy >= gy0 && ggx >= gx0 {
812                                    u32::from(
813                                        recon[((ggy - gy0) * tw as usize + (ggx - gx0)) * 3 + c],
814                                    )
815                                } else {
816                                    u32::from(pixels[(ggy * width as usize + ggx) * 3 + c])
817                                }
818                            };
819                            let pred: u32 = if gy == 0 && gx == 0 {
820                                127
821                            } else if gy == 0 {
822                                if gx - 1 < gx0 {
823                                    u32::from(pixels[(gy * width as usize + gx - 1) * 3 + c])
824                                } else {
825                                    u32::from(recon[(y * tw as usize + x - 1) * 3 + c])
826                                }
827                            } else if gx == 0 {
828                                if gy - 1 < gy0 {
829                                    u32::from(pixels[((gy - 1) * width as usize + gx) * 3 + c])
830                                } else {
831                                    u32::from(recon[((y - 1) * tw as usize + x) * 3 + c])
832                                }
833                            } else {
834                                let a = tl(gy, gx - 1);
835                                let b = tl(gy - 1, gx);
836                                let cc = tl(gy - 1, gx - 1);
837                                kind.predict(a, b, cc)
838                            };
839                            recon[k] = (pred + u32::from(body[k])).wrapping_rem_euclid(256) as u8;
840                        }
841                    }
842                }
843                recon
844            }
845            _ => {
846                // non-literal modes are UNCHANGED MNF7 records under any
847                // ADR-004 predictor (predictor applies to literal bodies
848                // only) — a Plane/Left file may still carry them whenever
849                // they measured better per tile.
850                return Err(Error::fmt_msg(format!(
851                    "tile {i}: mode {mode} under PRED_PLANE/LEFT — not yet admitted"
852                )));
853            }
854        };
855        decoded.push((tw, th, tile.clone()));
856        for y_ in 0..th as usize {
857            let py = row as usize * TILE_SIZE as usize + y_;
858            for x_ in 0..tw as usize {
859                let pxx = col as usize * TILE_SIZE as usize + x_;
860                let d = (py * width as usize + pxx) * 3;
861                let s = (y_ * tw as usize + x_) * 3;
862                pixels[d..d + 3].copy_from_slice(&tile[s..s + 3]);
863            }
864        }
865    }
866    if ip != raw.len() {
867        return Err(Error::fmt_msg("extra"));
868    }
869    Ok(pixels)
870}
871
872#[cfg(test)]
873mod gate30 {
874    use super::*;
875    use crate::mnf6::Coder;
876    use std::path::Path;
877    fn load(name: &str) -> (Dims, Vec<u8>) {
878        let p = format!("{}/testdata/{}", env!("CARGO_MANIFEST_DIR"), name);
879        let img = image::open(Path::new(&p))
880            .unwrap_or_else(|e| panic!("{name}: {e}"))
881            .to_rgb8();
882        (
883            Dims {
884                width: img.width(),
885                height: img.height(),
886            },
887            img.into_raw(),
888        )
889    }
890    // Full-corpus real-wire gate (837 s): #[ignore]d for CI; run explicitly
891    // when touching the predictor arms. PINS below run always.
892    #[test]
893    #[ignore]
894    fn gate_all_arms_real_wire() {
895        let files = [
896            "gradient_320x240.png",
897            "noise_256x256.png",
898            "repeat_256x256.png",
899            "screenshot_640x400.png",
900            "solid_256x256.png",
901            "heldout/blob_224x160.png",
902            "heldout/blocks_192x128.png",
903            "heldout/document_200x280.png",
904            "heldout/plaid_192.png",
905            "heldout/poster_208x144.png",
906            "heldout-photos/autumn_160x120_s3100006.png",
907            "heldout-photos/dusk_160x120_s3100003.png",
908            "heldout-photos/dusk_224x160_s3100002.png",
909            "heldout-photos/forest_192x144_s3100001.png",
910            "heldout-photos/forest_224x160_s3100005.png",
911            "heldout-photos/forest_224x160_s3100008.png",
912            "heldout-photos/ocean_192x144_s3100007.png",
913            "heldout-photos/sand_192x144_s3100004.png",
914            "heldout-photos/slate_160x120_s3100009.png",
915            "heldout-photos/terrain_160x120_s3100000.png",
916            "photos/autumn_192x144_s900001.png",
917            "photos/dusk_160x120_s900003.png",
918            "photos/forest_224x160_s900002.png",
919            "photos/ocean_192x144_s900007.png",
920            "photos/sand_160x120_s900000.png",
921            "photos/slate_192x144_s900004.png",
922            "photos/slate_224x160_s900008.png",
923            "photos/stone_160x120_s900006.png",
924            "photos/terrain_160x120_s900009.png",
925            "photos/terrain_224x160_s900005.png",
926        ];
927        let mut old_tot = 0usize;
928        let mut new_tot = 0usize;
929        let mut fires = 0usize;
930        for name in files {
931            let (d, px) = load(name);
932            // OLD pick (v1.6.0 arms: None/Med × backends via encode paths + tokens)
933            let mut old = usize::MAX;
934            for kind in [PredKind::None, PredKind::Med] {
935                for (backend, coder) in [
936                    (BACKEND_ZSTD19, Coder::Zstd(19)),
937                    (BACKEND_ZLIB, Coder::Zlib),
938                ] {
939                    for planar in [false, true] {
940                        let med = kind == PredKind::Med;
941                        let (file, _) = encode_stream_coder(
942                            &px,
943                            d,
944                            true,
945                            true,
946                            kind.flag_bit() | if planar { FLAG_PLANAR } else { 0 },
947                            planar,
948                            med,
949                            kind,
950                            &coder,
951                            backend,
952                        )
953                        .unwrap();
954                        // tokens arm
955                        let _ = &file;
956                        old = old.min(file.len());
957                    }
958                }
959            }
960            for (backend, _coder) in [
961                (BACKEND_ZSTD19, Coder::Zstd(19)),
962                (BACKEND_ZLIB, Coder::Zlib),
963            ] {
964                let (tw, _) =
965                    encode_tokens_coder(&px, d, backend, FLAG_PRED_MED | FLAG_TOKENS).unwrap();
966                old = old.min(tw.len());
967            }
968            // NEW pick: + plane/left pure arms
969            let mut new_ = old;
970            let mut new_win = String::new();
971            for kind in [PredKind::Plane, PredKind::Left] {
972                for (backend, coder) in [
973                    (BACKEND_ZSTD19, Coder::Zstd(19)),
974                    (BACKEND_ZLIB, Coder::Zlib),
975                ] {
976                    let (file, _) = encode_stream_coder(
977                        &px,
978                        d,
979                        true,
980                        true,
981                        kind.flag_bit(),
982                        false,
983                        false,
984                        kind,
985                        &coder,
986                        backend,
987                    )
988                    .unwrap();
989                    // exact decode check on the pure arm
990                    let (dd, out, _) =
991                        decode_mnf8(&file).unwrap_or_else(|e| panic!("{name} {kind:?}: {e}"));
992                    if out != px {
993                        let pos = out
994                            .iter()
995                            .zip(&px)
996                            .position(|(a, b)| a != b)
997                            .expect("differs");
998                        panic!(
999                            "{name} {kind:?} NOT exact: first diff byte {pos} (pixel y={}, x={}, c={}): dec={} enc={}",
1000                            pos / 3 / d.width as usize, pos / 3 % d.width as usize, pos % 3,
1001                            out[pos], px[pos]
1002                        );
1003                    }
1004                    let _ = dd;
1005                    if file.len() < new_ {
1006                        new_ = file.len();
1007                        new_win = format!("{kind:?}-{backend}");
1008                    }
1009                }
1010            }
1011            if new_ < old {
1012                fires += 1;
1013            }
1014            old_tot += old;
1015            new_tot += new_;
1016            println!(
1017                "{name:44} old={old:>8} new={new_:>8} {:+6} {}",
1018                new_ as i64 - old as i64,
1019                new_win
1020            );
1021        }
1022        println!(
1023            "TOTAL old={old_tot} new={new_tot} delta={:+} fires={fires}",
1024            new_tot as i64 - old_tot as i64
1025        );
1026        assert!(new_tot < old_tot, "aggregate must strictly improve");
1027    }
1028
1029    #[test]
1030    fn branch_arms_roundtrip_and_pick_fires() {
1031        // ADR-004 pins (fast): (1) Plane/Left pure files decode exactly;
1032        // (2) the pick fires a predictor arm on the poster class;
1033        // (3) ties/losses keep shipping arms (never-worse).
1034        let path = format!(
1035            "{}/testdata/heldout/poster_208x144.png",
1036            env!("CARGO_MANIFEST_DIR")
1037        );
1038        let img = image::open(std::path::Path::new(&path)).unwrap().to_rgb8();
1039        let d = Dims {
1040            width: img.width(),
1041            height: img.height(),
1042        };
1043        let px = img.into_raw();
1044        let (buf, st, _) = encode_mnf8(&px, d, true, Profile::Balanced);
1045        let (_, out, _) = decode_mnf8(&buf).expect("decode");
1046        assert_eq!(out, px, "picked arm decodes exactly");
1047        assert!(
1048            st.chosen_flags & (FLAG_PRED_PLANE | FLAG_PRED_LEFT) != 0,
1049            "poster must pick a branch arm (picked {:#x})",
1050            st.chosen_flags
1051        );
1052        // and the new pick must never exceed every old arm on this file:
1053        let (bufold, _, _) = {
1054            // construct the old v1.6.0 pick: None/Med arms + tokens, balanced
1055            let mut best: Option<(Vec<u8>, u32)> = None;
1056            for (backend, coder) in [(BACKEND_ZSTD19, Coder::Zstd(19))] {
1057                for kind in [PredKind::None, PredKind::Med] {
1058                    for planar in [false, true] {
1059                        let (f, _) = encode_stream_coder(
1060                            &px,
1061                            d,
1062                            true,
1063                            true,
1064                            kind.flag_bit() | if planar { FLAG_PLANAR } else { 0 },
1065                            planar,
1066                            kind == PredKind::Med,
1067                            kind,
1068                            &coder,
1069                            backend,
1070                        )
1071                        .unwrap();
1072                        if best.as_ref().is_none_or(|b| f.len() < b.0.len()) {
1073                            best = Some((f, kind.flag_bit()));
1074                        }
1075                    }
1076                }
1077                let (t, _) =
1078                    encode_tokens_coder(&px, d, backend, FLAG_PRED_MED | FLAG_TOKENS).unwrap();
1079                if best.as_ref().is_none_or(|b| t.len() < b.0.len()) {
1080                    best = Some((t, FLAG_TOKENS));
1081                }
1082            }
1083            let (f, fl) = best.unwrap();
1084            (f, fl, 0u32)
1085        };
1086        assert!(
1087            buf.len() <= bufold.len(),
1088            "new pick ({}) must not exceed the old pick ({})",
1089            buf.len(),
1090            bufold.len()
1091        );
1092    }
1093
1094    #[test]
1095    fn rejects_multiple_predictors() {
1096        // ADR-004 validation: two predictor bits set = Error, never panic.
1097        let mut f = vec![0u8; HEADER8];
1098        f[0..4].copy_from_slice(b"MNF8");
1099        f[4..8].copy_from_slice(&16u32.to_le_bytes());
1100        f[8..12].copy_from_slice(&16u32.to_le_bytes());
1101        f[12..16].copy_from_slice(&16u32.to_le_bytes());
1102        f[16..20].copy_from_slice(&1u32.to_le_bytes());
1103        f[20..24].copy_from_slice(&0u32.to_le_bytes());
1104        f[24..28].copy_from_slice(&(FLAG_PRED_MED | FLAG_PRED_PLANE).to_le_bytes());
1105        f[28..32].copy_from_slice(&BACKEND_ZSTD19.to_le_bytes());
1106        let r = std::panic::catch_unwind(|| decode_mnf8(&f));
1107        assert!(
1108            r.is_ok() && r.unwrap().is_err(),
1109            "two predictors must reject"
1110        );
1111        f[24..28].copy_from_slice(&(FLAG_TOKENS | FLAG_PRED_LEFT).to_le_bytes());
1112        let r = std::panic::catch_unwind(|| decode_mnf8(&f));
1113        assert!(r.is_ok() && r.unwrap().is_err(), "tokens+left must reject");
1114    }
1115}
1116
1117#[cfg(test)]
1118mod tests {
1119    use super::*;
1120
1121    fn roundtrip(px: &[u8], dims: Dims, profile: Profile) -> u32 {
1122        let (buf, st, backend) = encode_mnf8(px, dims, true, profile);
1123        let (d, out, _) = decode_mnf8(&buf).expect("decode");
1124        assert_eq!(d, dims);
1125        assert_eq!(
1126            out, px,
1127            "profile {profile:?} backend {backend} flags {st:?}"
1128        );
1129        backend
1130    }
1131
1132    #[test]
1133    fn mnf8_balanced_roundtrip_all_classes() {
1134        for (w, h, seed) in [(16u32, 16u32, 1), (48, 33, 2), (31, 17, 4), (1, 1, 5)] {
1135            let mut s: u64 = (seed as u64).wrapping_mul(0x9E3779B97F4A7C15) ^ 0xDEADBEEF;
1136            let mut rnd = move || {
1137                s ^= s << 13;
1138                s ^= s >> 7;
1139                s ^= s << 17;
1140                s
1141            };
1142            let px: Vec<u8> = (0..w as usize * h as usize * 3)
1143                .map(|_| (rnd() & 0xFF) as u8)
1144                .collect();
1145            roundtrip(
1146                &px,
1147                Dims {
1148                    width: w,
1149                    height: h,
1150                },
1151                Profile::Balanced,
1152            );
1153        }
1154    }
1155
1156    #[test]
1157    fn mnf8_maximum_never_loses_to_mnf7() {
1158        // ADR-002 gate (EMPIRICAL per review fix 8 — candidate sets differ
1159        // structurally; this pins the measured relation on stable files).
1160        for name in [
1161            "screenshot_640x400.png",
1162            "solid_256x256.png",
1163            "gradient_320x240.png",
1164        ] {
1165            let path = format!("{}/testdata/{name}", env!("CARGO_MANIFEST_DIR"));
1166            let img = image::open(&path)
1167                .unwrap_or_else(|e| panic!("{name}: {e}"))
1168                .to_rgb8();
1169            let d = Dims {
1170                width: img.width(),
1171                height: img.height(),
1172            };
1173            let px = img.into_raw();
1174            let (buf8, _, backend) = encode_mnf8(&px, d, true, Profile::Maximum);
1175            let (buf7, _) = crate::mnf6::encode_mnf7(&px, d, true);
1176            // MNF8's header carries the 4-byte backend field; equality of
1177            // measure means <= MNF7 + 4 (the honest wire cost of the id).
1178            assert!(
1179                buf8.len() <= buf7.len() + 4,
1180                "{name}: MNF8({backend}) {} > MNF7(+4) {}",
1181                buf8.len(),
1182                buf7.len()
1183            );
1184            let (_, out, _) = decode_mnf8(&buf8).expect("decode");
1185            assert_eq!(out, px, "{name}: exact");
1186        }
1187    }
1188
1189    #[test]
1190    fn mnf8_rejects() {
1191        let mut f = vec![0u8; 32];
1192        f[0..4].copy_from_slice(b"MNF8");
1193        f[4..8].copy_from_slice(&16u32.to_le_bytes());
1194        f[8..12].copy_from_slice(&16u32.to_le_bytes());
1195        f[12..16].copy_from_slice(&16u32.to_le_bytes());
1196        f[16..20].copy_from_slice(&1u32.to_le_bytes());
1197        // payload_len = 0, flags = 0
1198        f[28..32].copy_from_slice(&7u32.to_le_bytes()); // unknown backend
1199        assert!(decode_mnf8(&f).is_err(), "unknown backend rejected");
1200        f[28..32].copy_from_slice(&3u32.to_le_bytes()); // reserved backend
1201        assert!(decode_mnf8(&f).is_err(), "reserved backend rejected");
1202        f[28..32].copy_from_slice(&2u32.to_le_bytes()); // reserved brotli backend
1203        assert!(decode_mnf8(&f).is_err(), "reserved brotli backend rejected");
1204        // and specifically NOT a panic (the old bug hit unreachable! here)
1205        f[28..32].copy_from_slice(&2u32.to_le_bytes());
1206        let r = std::panic::catch_unwind(|| decode_mnf8(&f));
1207        assert!(r.is_ok() && r.unwrap().is_err(), "must error, not panic");
1208        f[28..32].copy_from_slice(&0u32.to_le_bytes());
1209        f[24] = 0b100; // reserved flag bit
1210        assert!(decode_mnf8(&f).is_err(), "reserved flag rejected");
1211        // bad magic
1212        f[0] = b'X';
1213        f[24] = 0;
1214        assert!(decode_mnf8(&f).is_err(), "magic");
1215    }
1216
1217    #[test]
1218    fn mnf8_balanced_picks_zstd_not_hardcoded() {
1219        // review: the old test read the hardcoded return tuple; instead pin
1220        // the ENCODING CHOICE: balanced must measure the flag variants under
1221        // zstd and pick planar when planar wins (gradient-like content).
1222        let (w, h) = (64u32, 64u32);
1223        let mut px = vec![0u8; (w * h * 3) as usize];
1224        for y in 0..h {
1225            for x in 0..w {
1226                let i = ((y * w + x) * 3) as usize;
1227                px[i] = (x * 4) as u8;
1228                px[i + 1] = (y * 4) as u8;
1229                px[i + 2] = 128;
1230            }
1231        }
1232        let (buf, st, backend) = encode_mnf8(
1233            &px,
1234            Dims {
1235                width: w,
1236                height: h,
1237            },
1238            false,
1239            Profile::Balanced,
1240        );
1241        assert_eq!(backend, BACKEND_ZSTD19);
1242        // MEASURED pick (v1.2.1 round): the invariant is "measured, not
1243        // hardcoded" — the winner id moves with the data. This gradient has
1244        // vertically repeating tile rows, so exact refs beat literals and
1245        // flags = 0 is a LEGAL winner here; pin exactness + a legal flags id,
1246        // and pin the med-bearing pick on the no-repeat smooth body (next
1247        // test), not here.
1248        assert!(
1249            st.chosen_flags & !0b1111 == 0,
1250            "balanced wrote a legal flags id, got {:#x}",
1251            st.chosen_flags
1252        );
1253        let (_, out, _) = decode_mnf8(&buf).expect("decode");
1254        assert_eq!(out, px);
1255    }
1256
1257    #[test]
1258    fn mnf8_no_geom_no_flips() {
1259        // review fix 2 regression pin: use_geom=false must emit ONLY exact
1260        // references (t=0) or plain literals — flips are forbidden. A rotated
1261        // copy of tile 0 must be emitted as a LITERAL, not a geom ref.
1262        let (w, h) = (32u32, 32u32); // two 16-tall tile rows
1263        let mut px = vec![0u8; (w * h * 3) as usize];
1264        for y in 0..h {
1265            for x in 0..w {
1266                let i = ((y * w + x) * 3) as usize;
1267                px[i] = (x * 7 % 256) as u8;
1268                px[i + 1] = (y * 3 % 256) as u8;
1269                px[i + 2] = 5;
1270            }
1271        }
1272        // flip tile (0,1) [the 16x16 block at rows 16..32, cols 0..16] in place
1273        let tile_row = 16usize;
1274        let tile_col = 0usize;
1275        let src_off = (tile_row * w as usize + tile_col) * 3;
1276        let tile = px[src_off..src_off + 16 * 16 * 3].to_vec();
1277        let flipped = apply_transform(1, 16, 16, &tile);
1278        px[src_off..src_off + 16 * 16 * 3].copy_from_slice(&flipped);
1279        let stacked = px.clone();
1280        let d = Dims {
1281            width: w,
1282            height: h,
1283        };
1284        let (buf, st, _) = encode_mnf8(&stacked, d, false, Profile::Balanced);
1285        // walk stats: geom references must be zero
1286        assert_eq!(st.ref_geom, 0, "no flips allowed with use_geom=false");
1287        assert_eq!(st.geom_nontrivial, 0);
1288        let (_, out, _) = decode_mnf8(&buf).expect("decode");
1289        assert_eq!(out, stacked);
1290    }
1291
1292    #[test]
1293    fn mnf8_no_atlas_arm() {
1294        // review fix 2b: the ablation needs its no-atlas arm (pure
1295        // prediction: literals + mode 3 only), and it must roundtrip exactly.
1296        let d = Dims {
1297            width: 32,
1298            height: 32,
1299        };
1300        let px: Vec<u8> = (0..d.width as usize * d.height as usize * 3)
1301            .map(|i| (i * 7 % 256) as u8)
1302            .collect();
1303        let (buf, st, _) = encode_mnf8_noclimb(&px, d);
1304        assert_eq!(
1305            st.ref_exact + st.ref_geom + st.ref_exact_color + st.ref_geom_color,
1306            0,
1307            "no-atlas arm must emit zero references"
1308        );
1309        let (_, out, _) = decode_mnf8(&buf).expect("decode");
1310        assert_eq!(out, px);
1311    }
1312
1313    // helper calling the internal encoder with both atlas controls off
1314    fn encode_mnf8_noclimb(px: &[u8], d: Dims) -> (Vec<u8>, MnF6Stats, u32) {
1315        // balanced profile, atlas off, geom off
1316        let mut best: Option<(Vec<u8>, MnF6Stats, u32)> = None;
1317        for flags in [0u32, FLAG_PLANAR] {
1318            let planar = flags & FLAG_PLANAR != 0;
1319            let (file, st) = encode_stream_coder(
1320                px,
1321                d,
1322                false,
1323                false,
1324                flags,
1325                planar,
1326                false,
1327                PredKind::None,
1328                &Coder::Zstd(19),
1329                BACKEND_ZSTD19,
1330            )
1331            .expect("encode");
1332            if best.as_ref().is_none_or(|b| file.len() < b.0.len()) {
1333                best = Some((file, st, BACKEND_ZSTD19));
1334            }
1335        }
1336        let (f, s, b) = best.expect("variant");
1337        (f, s, b)
1338    }
1339
1340    #[test]
1341    fn mnf8_med_flag_roundtrip_photo_like() {
1342        // FLAG_PRED_MED (v1.2 gate item 2): gradient-like image where the
1343        // image-wide MED pre-predictor wins; the rate-aware loop must MEASURE
1344        // the med variant and pick it, and decode must reconstruct exactly
1345        // through the causal in-walker MED inversion.
1346        let (w, h) = (64u32, 64u32);
1347        let mut px = vec![0u8; (w * h * 3) as usize];
1348        for y in 0..h {
1349            for x in 0..w {
1350                let i = ((y * w + x) * 3) as usize;
1351                px[i] = (x * 4) as u8;
1352                px[i + 1] = (y * 4) as u8;
1353                px[i + 2] = ((x + y) / 2) as u8;
1354            }
1355        }
1356        let d = Dims {
1357            width: w,
1358            height: h,
1359        };
1360        let (buf, st, _) = encode_mnf8(&px, d, false, Profile::Balanced);
1361        // RATE-AWARE pick is allowed to lose here (refs compressed better than
1362        // MED-literals on this synthetic body); the invariants that matter:
1363        // the MED variant was MEASURED (no panic), the pick is a legal flags
1364        // id, and decode is exact.
1365        assert!(
1366            st.chosen_flags & !0b1111 == 0,
1367            "chosen flags must be a legal MNF8 id, got {:#x}",
1368            st.chosen_flags
1369        );
1370        let (_, out, _) = decode_mnf8(&buf).expect("decode");
1371        if out != px {
1372            let first = out
1373                .iter()
1374                .zip(&px)
1375                .position(|(a, b)| a != b)
1376                .expect("differs");
1377            let (b_y, rem) = (first / 3 / (w as usize), (first / 3) % (w as usize));
1378            panic!(
1379                "not exact: first diff at byte {first} (pixel ({b_y},{rem}), ch {}): out={:?} px={:?}",
1380                first % 3,
1381                &out[first.saturating_sub(4)..first + 4],
1382                &px[first.saturating_sub(4)..first + 4]
1383            );
1384        }
1385        assert_eq!(out, px, "exact reconstruction under the measured flags");
1386    }
1387
1388    #[test]
1389    fn mnf8_med_flag_picked_on_photo_like_smoothing() {
1390        // The MEASURED pick: smooth noise-free body with NO exact repeats —
1391        // MED-literals must win (105 B vs planar 177 / interleaved 401,
1392        // measured in the debug round).
1393        let (w, h) = (64u32, 64u32);
1394        let mut px = vec![0u8; (w * h * 3) as usize];
1395        for y in 0..h {
1396            for x in 0..w {
1397                let i = ((y * w + x) * 3) as usize;
1398                // smooth field + tiny deterministic ripple (no byte repeats)
1399                px[i] = (x * 4 + y * 2) as u8;
1400                px[i + 1] = (y * 6) as u8;
1401                px[i + 2] = ((x + y) * 3) as u8;
1402            }
1403        }
1404        let d = Dims {
1405            width: w,
1406            height: h,
1407        };
1408        let (_, st, _) = encode_mnf8(
1409            &px,
1410            d,
1411            false, // no geom: kill the reference arms to isolate prediction
1412            Profile::Balanced,
1413        );
1414        // ADR-004 MEASURED pick (branch-gate): smooth no-repeat bodies select
1415        // a PREDICTOR bit — historically MED, but the plane branch measured
1416        // cheaper on the same class, and the pick is measured, not hardcoded.
1417        // The invariant: a predictor bit IS set and the file decodes exactly.
1418        assert!(
1419            st.chosen_flags & (FLAG_PRED_MED | FLAG_PRED_PLANE | FLAG_PRED_LEFT) != 0,
1420            "smooth no-repeat bodies must select a predictor (picked {:#x})",
1421            st.chosen_flags
1422        );
1423        let (_, out, _) =
1424            decode_mnf8(&encode_mnf8(&px, d, false, Profile::Balanced).0).expect("decode");
1425        assert_eq!(out, px, "exact under the measured predictor pick");
1426    }
1427
1428    #[test]
1429    fn mnf8_forced_mode4_mode5_records() {
1430        // review verify 3: drive mode 4 and mode 5 end-to-end — construct a
1431        // file whose color residuals genuinely win (structured chroma delta
1432        // on a noisy base, the mnf7_color_domain_used_when_it_wins pattern)
1433        // and assert decode_mnf8 reconstructs exactly.
1434        let mut s: u64 = 42;
1435        let mut rnd = move || {
1436            s ^= s << 13;
1437            s ^= s >> 7;
1438            s ^= s << 17;
1439            s
1440        };
1441        let a: Vec<u8> = (0..16 * 16 * 3).map(|_| (rnd() & 0xFF) as u8).collect();
1442        let ta_t = crate::mnf6::modcolor_forward(&a);
1443        let mut tb_t = ta_t.clone();
1444        let ramp = [0u8, 1, 2, 255];
1445        for i in (1..tb_t.len()).step_by(3) {
1446            tb_t[i] = tb_t[i].wrapping_add(ramp[(i / 3) % 4]);
1447        }
1448        let b = crate::mnf6::modcolor_inverse(&tb_t);
1449        let mut two = a.clone();
1450        two.extend_from_slice(&b);
1451        let d2 = Dims {
1452            width: 16,
1453            height: 32,
1454        };
1455        let (buf, st, _) = encode_mnf8(&two, d2, true, Profile::Balanced);
1456        assert!(
1457            st.ref_exact_color > 0,
1458            "expected mode 4 (or a color-geom below) selected, stats: exact={} geom={} ec={} gc={}",
1459            st.ref_exact,
1460            st.ref_geom,
1461            st.ref_exact_color,
1462            st.ref_geom_color
1463        );
1464        let (_, out, _) = decode_mnf8(&buf).expect("decode");
1465        assert_eq!(out, two, "mode-4 record reconstructs exactly through MNF8");
1466
1467        // and a rotated variant of the same (mode 5 path)
1468        let flipped = apply_transform(1, 16, 16, &a);
1469        let tb_t2 = crate::mnf6::modcolor_forward(&flipped);
1470        let mut tc_t = tb_t2;
1471        for i in (1..tc_t.len()).step_by(3) {
1472            tc_t[i] = tc_t[i].wrapping_add(ramp[(i / 3) % 4]);
1473        }
1474        let c = crate::mnf6::modcolor_inverse(&tc_t);
1475        let mut three = flipped.clone();
1476        three.extend_from_slice(&c);
1477        let d3 = Dims {
1478            width: 16,
1479            height: 32,
1480        };
1481        let (buf3, st3, _) = encode_mnf8(&three, d3, true, Profile::Balanced);
1482        assert!(
1483            st3.ref_exact_color > 0 || st3.ref_geom_color > 0,
1484            "expected a color-domain reference (exact or geom), stats: ec={} gc={}",
1485            st3.ref_exact_color,
1486            st3.ref_geom_color
1487        );
1488        let (_, out3, _) = decode_mnf8(&buf3).expect("decode");
1489        assert_eq!(out3, three);
1490    }
1491
1492    // ---- ADR-003 token arm ------------------------------------------------
1493
1494    fn roundtrip_tokens(px: &[u8], dims: Dims, profile: Profile) -> (u32, u32) {
1495        let (buf, st, backend) = encode_mnf8(px, dims, true, profile);
1496        let (d, out, _) = decode_mnf8(&buf).expect("decode");
1497        assert_eq!(d, dims);
1498        assert_eq!(
1499            out, px,
1500            "profile {profile:?} backend {backend} flags {:#x} literal {} matches {}",
1501            st.chosen_flags, st.literal, st.ref_exact
1502        );
1503        (st.literal, st.ref_exact)
1504    }
1505
1506    #[test]
1507    fn token_wire_unit_roundtrip_and_errors() {
1508        use crate::tokens::*;
1509        // small plane with a planted repeat + a run: tokens fire and rebuild
1510        let (w, h) = (24usize, 16usize);
1511        let mut plane = vec![0u8; w * h * 3];
1512        for i in 0..w * h {
1513            plane[i * 3] = (i % 251) as u8;
1514            plane[i * 3 + 1] = ((i * 7) % 251) as u8;
1515        }
1516        for k in 300..384 {
1517            plane[k * 3..k * 3 + 3].copy_from_slice(&[9, 9, 9]);
1518        }
1519        let (wire, st) = encode_plane(&plane, w, h).expect("encode");
1520        assert!(st.matches >= 1, "planted structures must fire: {st:?}");
1521        let back = decode_plane(&wire, w, h).expect("decode");
1522        assert_eq!(back, plane);
1523        // malformed wires are errors, never panics
1524        let mut bad = wire.clone();
1525        bad.push(2); // unknown tag
1526        assert!(decode_plane(&bad, w, h).is_err());
1527        assert!(decode_plane(&wire[..wire.len() - 1], w, h).is_err());
1528        let mut bad = wire.clone();
1529        bad.push(TAG_LITERAL);
1530        bad.extend_from_slice(&[1, 2, 3]);
1531        assert!(decode_plane(&bad, w, h).is_err()); // overruns the plane
1532    }
1533
1534    #[test]
1535    fn mnf8_token_flag_picked_on_repeating_residual() {
1536        // ADR-003 measured pick: a large smooth ramp (long constant MED
1537        // residual runs = the measured token-winning class). The pick MUST
1538        // be FLAG_TOKENS and decode must reconstruct exactly through the
1539        // token walk + causal MED inversion.
1540        let (w, h) = (256u32, 256u32);
1541        let mut px = vec![0u8; (w * h * 3) as usize];
1542        for y in 0..h {
1543            for x in 0..w {
1544                let i = ((y * w + x) * 3) as usize;
1545                // vertical ramp: every MED residual = same constant per row
1546                px[i] = (y * 3) as u8;
1547                px[i + 1] = (y / 4) as u8;
1548                px[i + 2] = 200;
1549            }
1550        }
1551        let d = Dims {
1552            width: w,
1553            height: h,
1554        };
1555        let (buf, st, _) = encode_mnf8(&px, d, false, Profile::Balanced);
1556        // ADR-003/004 MEASURED pick: this ramp's tokens WON in the v1.6.0
1557        // arm set, but the branch-gate measured LEFT cheaper still on the
1558        // same class (−163 B at 320×240; the pick is measured, never
1559        // hardcoded) — the invariant is exactness + pick legality.
1560        assert!(
1561            st.chosen_flags & (FLAG_TOKENS | FLAG_PRED_PLANE | FLAG_PRED_LEFT) != 0,
1562            "repeating-residual class must pick a measured-winning arm (picked {:#x})",
1563            st.chosen_flags
1564        );
1565        let (_, out, _) = decode_mnf8(&buf).expect("decode");
1566        assert_eq!(out, px, "wire reconstructs exactly under the pick");
1567    }
1568
1569    #[test]
1570    fn mnf8_token_flag_not_picked_on_noise() {
1571        // noise planes: all-literal token wire is pure overhead — the pick
1572        // must stay on the record arms (this is the rate-aware contract's
1573        // other half).
1574        let (w, h) = (64u32, 64u32);
1575        let mut s: u64 = 0x5EED;
1576        let mut rnd = move || {
1577            s ^= s << 13;
1578            s ^= s >> 7;
1579            s ^= s << 17;
1580            s
1581        };
1582        let px: Vec<u8> = (0..w as usize * h as usize * 3)
1583            .map(|_| (rnd() & 0xFF) as u8)
1584            .collect();
1585        let d = Dims {
1586            width: w,
1587            height: h,
1588        };
1589        let (_, st, _) = encode_mnf8(&px, d, false, Profile::Balanced);
1590        assert_eq!(
1591            st.chosen_flags & FLAG_TOKENS,
1592            0,
1593            "noise class must NOT pick tokens (picked {:#x})",
1594            st.chosen_flags
1595        );
1596        roundtrip_tokens(&px, d, Profile::Balanced);
1597    }
1598
1599    #[test]
1600    fn mnf8_tokens_decode_rejects_malformed_wire() {
1601        // craft a token-flag file whose payload decodes to a corrupt wire:
1602        // unknown tag inside. Must Err, never panic (validation whitelist
1603        // rule: every admitted construct has a handler or an explicit Err).
1604        let f = vec![0u8; HEADER8 + 7];
1605        let mut f = f;
1606        f[0..4].copy_from_slice(b"MNF8");
1607        f[4..8].copy_from_slice(&16u32.to_le_bytes());
1608        f[8..12].copy_from_slice(&16u32.to_le_bytes());
1609        f[12..16].copy_from_slice(&16u32.to_le_bytes());
1610        f[16..20].copy_from_slice(&1u32.to_le_bytes());
1611        f[20..24].copy_from_slice(&7u32.to_le_bytes());
1612        f[24..28].copy_from_slice(&(FLAG_PRED_MED | FLAG_TOKENS).to_le_bytes());
1613        f[28..32].copy_from_slice(&BACKEND_ZSTD19.to_le_bytes());
1614        // zlib backend so we can hand-craft canonical payload bytes:
1615        f[28..32].copy_from_slice(&BACKEND_ZLIB.to_le_bytes());
1616        let res: Vec<u8> = vec![0u8; 16 * 16 * 3];
1617        let (wire, _) = crate::tokens::encode_plane(&res, 16, 16).expect("wire");
1618        // flip first tag byte to an unknown tag
1619        let mut wire_bad = wire.clone();
1620        wire_bad[0] = 7;
1621        let comp: Vec<u8>;
1622        {
1623            let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());
1624            std::io::Write::write_all(&mut e, &wire_bad).unwrap();
1625            comp = e.finish().unwrap();
1626        }
1627        let mut file = Vec::with_capacity(HEADER8 + comp.len());
1628        file.extend_from_slice(&f[..HEADER8]);
1629        let pl = comp.len() as u32;
1630        file[20..24].copy_from_slice(&pl.to_le_bytes());
1631        file.extend_from_slice(&comp);
1632        let r = std::panic::catch_unwind(|| decode_mnf8(&file));
1633        assert!(
1634            r.is_ok() && r.unwrap().is_err(),
1635            "corrupt wire: Err not panic"
1636        );
1637        // exact wire still decodes
1638        let comp2: Vec<u8>;
1639        {
1640            let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());
1641            std::io::Write::write_all(&mut e, &wire).unwrap();
1642            comp2 = e.finish().unwrap();
1643        }
1644        let mut file2 = Vec::with_capacity(HEADER8 + comp2.len());
1645        file2.extend_from_slice(&f[..HEADER8]);
1646        file2[20..24].copy_from_slice(&(comp2.len() as u32).to_le_bytes());
1647        file2.extend_from_slice(&comp2);
1648        let (_, out, _) = decode_mnf8(&file2).expect("valid token wire decodes");
1649        assert_eq!(out, pred_residual_invert(&res, 16, 16, PredKind::Med));
1650    }
1651}