Skip to main content

cram_codec/
img.rs

1//! Overfitted image codec (Phase 2, Track D): Cool-chic 5 style decoder in
2//! exact 32-bit integer arithmetic, so the CPU reference and the GPU kernels
3//! ([`crate::imggpu`]) decode the same bytes to the same pixels.
4//!
5//! The encoder side is the Cool-chic 5 reference (PyTorch, per-image
6//! overfitting); `tools/coolchic/export_cc.py` dumps a trained image as a
7//! [`Ccx`] file (integer latents + dequantized nets). [`encode`] turns that
8//! into a `TCI1` stream:
9//!
10//! - **Nets** (ARM, IFCEs, upsampling, synthesis): each tensor as its
11//!   quantization exponent `s` plus integers `k = v·2^s` (zigzag varints),
12//!   then fixed point with a per-layer weight precision `fw` chosen by the
13//!   encoder so that no accumulation leaves `i32` on this image (checked in
14//!   `i64`; the decoder re-checks).
15//! - **Latents**: per grid (coarse → fine), Laplace probabilities from the
16//!   integer ARM (spatial causal context + IFCE inter-feature context),
17//!   quantized to `NFB` μ-fraction × `NLB` log-scale buckets, coded with
18//!   16-bit-word rANS. Rows are spread over `lanes` independent rANS streams
19//!   and decoded in a **wavefront** (`t = x + s·y`, `s` from the context
20//!   template), so every lane advances in lockstep; optional column tiles
21//!   (`tile_w`) cut contexts at tile borders for more parallel blocks.
22//! - **Synthesis**: learned separable upsampling pyramid, then the synthesis
23//!   MLP / 3×3 residual convs, all `QA`-bit fixed point; pixels are
24//!   `clamp(⌊255·v⌉, 0, 255)`.
25
26use crate::bitio::{unzigzag, zigzag, BitReader, BitWriter, ByteReader, ByteWriter, DecodeError};
27use std::collections::HashMap;
28
29/// Stream magic.
30pub const MAGIC: &[u8; 4] = b"TCI1";
31/// Fractional bits of ARM activations (latents enter as `v << QA_ARM`).
32pub const QA_ARM: u32 = 8;
33/// Fractional bits of upsampling / synthesis activations.
34pub const QA: u32 = 12;
35/// μ-fraction buckets.
36pub const NFB: usize = 256 >> FB_SHIFT;
37/// μ-fraction bucket width `2^FB_SHIFT / 256`.
38pub const FB_SHIFT: u32 = 3;
39/// Log-scale bucket width `2^LB_SHIFT / 256`.
40pub const LB_SHIFT: u32 = 3;
41/// Log-scale buckets over `ln b ∈ [−5, 5]`.
42pub const NLB: usize = (2560 >> LB_SHIFT) + 1;
43/// Residual alphabet `r ∈ [−R, R]` plus two escapes.
44pub const R: i32 = 31;
45/// Symbols per table.
46pub const NSYM: usize = (2 * R + 3) as usize;
47/// rANS probability precision.
48pub const PB: u32 = 15;
49const PM: u32 = 1 << PB;
50const RL: u32 = 1 << 16;
51
52// ---------------------------------------------------------------- CCX input
53
54/// A model exported by `export_cc.py`: named f64 arrays with shapes.
55pub struct Ccx {
56    /// name → (shape, data)
57    pub t: HashMap<String, (Vec<usize>, Vec<f64>)>,
58}
59
60impl Ccx {
61    /// Parse the `CCX1` container.
62    pub fn parse(b: &[u8]) -> Result<Self, DecodeError> {
63        let mut r = ByteReader::new(b);
64        if r.take(4)? != b"CCX1" {
65            return Err(DecodeError::Invalid("not CCX1"));
66        }
67        let le32 = |r: &mut ByteReader| -> Result<u32, DecodeError> {
68            Ok(u32::from_le_bytes(r.take(4)?.try_into().unwrap()))
69        };
70        let n = le32(&mut r)?;
71        let mut t = HashMap::new();
72        for _ in 0..n {
73            let nl = u16::from_le_bytes(r.take(2)?.try_into().unwrap()) as usize;
74            let name = String::from_utf8_lossy(r.take(nl)?).into_owned();
75            let nd = r.u8()? as usize;
76            let shape: Vec<usize> = (0..nd)
77                .map(|_| le32(&mut r).map(|v| v as usize))
78                .collect::<Result<_, _>>()?;
79            let cnt: usize = shape.iter().product();
80            let data = r
81                .take(cnt * 8)?
82                .as_chunks::<8>()
83                .0
84                .iter()
85                .map(|c| f64::from_le_bytes(*c))
86                .collect();
87            t.insert(name, (shape, data));
88        }
89        Ok(Self { t })
90    }
91    fn get(&self, k: &str) -> &(Vec<usize>, Vec<f64>) {
92        self.t.get(k).unwrap_or_else(|| panic!("ccx: missing {k}"))
93    }
94    fn has(&self, k: &str) -> bool {
95        self.t.contains_key(k)
96    }
97    fn ints(&self, k: &str) -> Vec<i64> {
98        self.get(k).1.iter().map(|&v| v as i64).collect()
99    }
100}
101
102// ------------------------------------------------------------ fixed point nets
103
104/// A quantized tensor: values `k · 2^{−s}`.
105#[derive(Clone, Debug, Default)]
106pub struct QT {
107    /// Exponent: value = k / 2^s.
108    pub s: u32,
109    /// Integer mantissas.
110    pub k: Vec<i32>,
111}
112
113impl QT {
114    fn from_f64(v: &[f64]) -> Self {
115        let s = (0..=24u32)
116            .find(|&s| v.iter().all(|&x| (x * (1u64 << s) as f64).fract() == 0.0))
117            .unwrap_or(24);
118        Self {
119            s,
120            k: v.iter()
121                .map(|&x| (x * (1u64 << s) as f64).round() as i32)
122                .collect(),
123        }
124    }
125    /// Fixed point with `f` fractional bits (round half away from zero when `f < s`).
126    pub fn fixed(&self, f: u32) -> Vec<i32> {
127        self.k
128            .iter()
129            .map(|&k| {
130                if f >= self.s {
131                    k << (f - self.s)
132                } else {
133                    let d = self.s - f;
134                    let h = 1i32 << (d - 1);
135                    if k >= 0 {
136                        (k + h) >> d
137                    } else {
138                        -((-k + h) >> d)
139                    }
140                }
141            })
142            .collect()
143    }
144    fn write(&self, w: &mut BitWriter) {
145        eg_put(w, self.s as u64, 0);
146        // Exp-Golomb order chosen per tensor
147        let zz: Vec<u64> = self.k.iter().map(|&k| zigzag(k as i64)).collect();
148        let cost = |o: u32| zz.iter().map(|&z| eg_len(z, o)).sum::<u64>();
149        let o = (0..12).min_by_key(|&o| cost(o)).unwrap();
150        eg_put(w, o as u64, 0);
151        for &z in &zz {
152            eg_put(w, z, o);
153        }
154    }
155    /// Read `n` values (the count comes from the layer shape).
156    fn read(r: &mut BitReader, n: usize) -> Result<Self, DecodeError> {
157        let s = eg_get(r, 0)? as u32;
158        if s > 24 {
159            return Err(DecodeError::Invalid("qt exponent"));
160        }
161        let o = eg_get(r, 0)? as u32;
162        let k = (0..n)
163            .map(|_| eg_get(r, o).map(|z| unzigzag(z) as i32))
164            .collect::<Result<_, _>>()?;
165        Ok(Self { s, k })
166    }
167}
168
169/// Weight precision cap; precisions are sent as `FWMAX − fw`.
170const FWMAX: u32 = 16;
171
172fn put_fw(w: &mut BitWriter, fw: u32) {
173    eg_put(w, (FWMAX - fw) as u64, 0);
174}
175
176fn get_fw(r: &mut BitReader) -> Result<u32, DecodeError> {
177    let d = eg_get(r, 0)?;
178    if d > FWMAX as u64 {
179        return Err(DecodeError::Invalid("weight precision"));
180    }
181    Ok(FWMAX - d as u32)
182}
183
184fn eg_len(v: u64, o: u32) -> u64 {
185    let x = (v >> o) + 1;
186    (2 * (63 - x.leading_zeros()) + 1 + o) as u64
187}
188
189/// Exp-Golomb of order `o`.
190fn eg_put(w: &mut BitWriter, v: u64, o: u32) {
191    let x = (v >> o) + 1;
192    let nb = 63 - x.leading_zeros();
193    w.put(0, nb);
194    w.put(1, 1);
195    w.put(x & ((1 << nb) - 1), nb);
196    w.put(v & ((1 << o) - 1), o);
197}
198
199fn eg_get(r: &mut BitReader, o: u32) -> Result<u64, DecodeError> {
200    let mut nb = 0;
201    while r.get(1)? == 0 {
202        nb += 1;
203        if nb > 40 {
204            return Err(DecodeError::Invalid("exp-golomb"));
205        }
206    }
207    let x = (1u64 << nb) | r.get(nb)?;
208    Ok(((x - 1) << o) | r.get(o)?)
209}
210
211/// One linear layer `out × inp` (`w[o·inp + i]`), fixed point.
212#[derive(Clone, Debug)]
213pub struct Lin {
214    /// Outputs.
215    pub out: usize,
216    /// Inputs.
217    pub inp: usize,
218    /// Source weights / biases.
219    pub qw: QT,
220    /// Biases.
221    pub qb: QT,
222    /// Weight fractional bits.
223    pub fw: u32,
224    /// Weights `Q(fw)`.
225    pub w: Vec<i32>,
226    /// Biases `Q(qa + fw)`.
227    pub b: Vec<i32>,
228}
229
230impl Lin {
231    fn new(out: usize, inp: usize, qw: QT, qb: QT) -> Self {
232        assert_eq!(qw.k.len(), out * inp);
233        assert_eq!(qb.k.len(), out);
234        Self {
235            out,
236            inp,
237            qw,
238            qb,
239            fw: 0,
240            w: vec![],
241            b: vec![],
242        }
243    }
244    fn set_fw(&mut self, fw: u32, qa: u32) {
245        self.fw = fw;
246        self.w = self.qw.fixed(fw);
247        self.b = self.qb.fixed(fw + qa);
248    }
249}
250
251/// Overflow guard: every accumulation is computed in `i64` and must fit `i32`
252/// (so the GPU's wrapping `i32` gives the same bits).
253#[derive(Default, Debug, Clone, Copy)]
254pub struct Guard {
255    /// Largest |accumulator| seen.
256    pub max: i64,
257}
258
259impl Guard {
260    #[inline]
261    fn see(&mut self, v: i64) -> i64 {
262        let a = v.abs();
263        if a > self.max {
264            self.max = a;
265        }
266        v
267    }
268    /// True when every value fitted `i32` (with one bit of headroom).
269    pub fn ok(&self) -> bool {
270        self.max < (1 << 30)
271    }
272}
273
274#[inline]
275fn rshift(acc: i64, f: u32) -> i64 {
276    if f == 0 {
277        acc
278    } else {
279        (acc + (1i64 << (f - 1))) >> f
280    }
281}
282
283/// ARM / IFCE MLP: hidden ReLU layers (square hidden layers residual inside
284/// the ReLU when `residual`), a linear output layer, and an optional linear
285/// stabiliser branch over the input added to the output.
286#[derive(Clone, Debug)]
287pub struct Mlp {
288    /// Layers in order.
289    pub layers: Vec<Lin>,
290    /// Stabiliser branch (input → output).
291    pub stab: Option<Lin>,
292    /// Square hidden layers carry a residual connection.
293    pub residual: bool,
294}
295
296impl Mlp {
297    fn from_ccx(c: &Ccx, p: &str, residual: bool) -> Self {
298        let mut layers = vec![];
299        for i in 0.. {
300            let k = format!("{p}.L{i}.w");
301            if !c.has(&k) {
302                break;
303            }
304            let (sh, w) = c.get(&k);
305            layers.push(Lin::new(
306                sh[0],
307                sh[1],
308                QT::from_f64(w),
309                QT::from_f64(&c.get(&format!("{p}.L{i}.b")).1),
310            ));
311        }
312        let stab = c.has(&format!("{p}.stab.w")).then(|| {
313            let (sh, w) = c.get(&format!("{p}.stab.w"));
314            Lin::new(
315                sh[0],
316                sh[1],
317                QT::from_f64(w),
318                QT::from_f64(&c.get(&format!("{p}.stab.b")).1),
319            )
320        });
321        Self {
322            layers,
323            stab,
324            residual,
325        }
326    }
327    fn set_fw(&mut self, fw: &[u32]) {
328        for (l, &f) in self.layers.iter_mut().zip(fw) {
329            l.set_fw(f, QA_ARM);
330        }
331        if let Some(s) = &mut self.stab {
332            s.set_fw(fw[self.layers.len()], QA_ARM);
333        }
334    }
335    /// Number of inputs.
336    pub fn nin(&self) -> usize {
337        self.layers[0].inp
338    }
339    /// Number of outputs.
340    pub fn nout(&self) -> usize {
341        self.layers.last().unwrap().out
342    }
343    /// Integer forward (`Q8` in, `Q8` out).
344    pub fn run(&self, x: &[i32], out: &mut [i32], g: &mut Guard) {
345        let mut a = [0i64; 64];
346        let mut b = [0i64; 64];
347        for i in 0..x.len() {
348            a[i] = x[i] as i64;
349        }
350        let nl = self.layers.len();
351        for (li, l) in self.layers.iter().enumerate() {
352            for o in 0..l.out {
353                let mut acc = l.b[o] as i64;
354                for i in 0..l.inp {
355                    acc = g.see(acc + l.w[o * l.inp + i] as i64 * a[i]);
356                }
357                let mut v = g.see(rshift(acc, l.fw));
358                if li + 1 < nl {
359                    if self.residual && l.inp == l.out {
360                        v = g.see(v + a[o]);
361                    }
362                    v = v.max(0);
363                }
364                b[o] = v;
365            }
366            std::mem::swap(&mut a, &mut b);
367        }
368        if let Some(s) = &self.stab {
369            for o in 0..s.out {
370                let mut acc = s.b[o] as i64;
371                for i in 0..s.inp {
372                    acc = g.see(acc + s.w[o * s.inp + i] as i64 * x[i] as i64);
373                }
374                a[o] = g.see(a[o] + rshift(acc, s.fw));
375            }
376        }
377        for o in 0..out.len() {
378            out[o] = a[o] as i32;
379        }
380    }
381    fn write(&self, w: &mut BitWriter) {
382        eg_put(w, self.layers.len() as u64, 0);
383        eg_put(
384            w,
385            (self.stab.is_some() as u8 | (self.residual as u8) << 1) as u64,
386            0,
387        );
388        // input width once; each layer's input is the previous output, the
389        // stabiliser's is the MLP input
390        eg_put(w, self.nin() as u64, 0);
391        for l in self.layers.iter().chain(self.stab.iter()) {
392            eg_put(w, l.out as u64, 0);
393            put_fw(w, l.fw);
394            l.qw.write(w);
395            l.qb.write(w);
396        }
397    }
398    fn read(r: &mut BitReader) -> Result<Self, DecodeError> {
399        let nl = eg_get(r, 0)? as usize;
400        let fl = eg_get(r, 0)? as u8;
401        let mut ls: Vec<Lin> = vec![];
402        let nin = eg_get(r, 0)? as usize;
403        for li in 0..nl + (fl & 1) as usize {
404            let out = eg_get(r, 0)? as usize;
405            let inp = if li == 0 || li == nl {
406                nin
407            } else {
408                ls[li - 1].out
409            };
410            let fw = get_fw(r)?;
411            if out > 64 || inp > 64 {
412                return Err(DecodeError::Invalid("mlp dims"));
413            }
414            let (qw, qb) = (QT::read(r, out * inp)?, QT::read(r, out)?);
415            let mut l = Lin::new(out, inp, qw, qb);
416            l.set_fw(fw, QA_ARM);
417            ls.push(l);
418        }
419        let stab = if fl & 1 == 1 { ls.pop() } else { None };
420        Ok(Self {
421            layers: ls,
422            stab,
423            residual: fl & 2 != 0,
424        })
425    }
426}
427
428/// A synthesis convolution (`k×k`, replicate padding).
429#[derive(Clone, Debug)]
430pub struct Conv {
431    /// Weights `[out][inp][k][k]` + biases, fixed point.
432    pub lin: Lin,
433    /// Kernel size (1 or 3).
434    pub k: usize,
435    /// `y += x`.
436    pub residual: bool,
437    /// ReLU after.
438    pub relu: bool,
439}
440
441impl Conv {
442    fn from_ccx(c: &Ccx, p: &str) -> Self {
443        let (sh, w) = c.get(&format!("{p}.w"));
444        let meta = &c.get(&format!("{p}.meta")).1;
445        let lin = Lin::new(
446            sh[0],
447            sh[1] * sh[2] * sh[3],
448            QT::from_f64(w),
449            QT::from_f64(&c.get(&format!("{p}.b")).1),
450        );
451        Self {
452            lin,
453            k: sh[2],
454            residual: meta[0] != 0.0,
455            relu: meta[1] != 0.0,
456        }
457    }
458    /// Input channels.
459    pub fn cin(&self) -> usize {
460        self.lin.inp / (self.k * self.k)
461    }
462    fn write(&self, w: &mut BitWriter) {
463        eg_put(w, self.lin.out as u64, 0);
464        eg_put(w, self.cin() as u64, 0);
465        eg_put(w, self.k as u64, 0);
466        eg_put(w, (self.residual as u8 | (self.relu as u8) << 1) as u64, 0);
467        put_fw(w, self.lin.fw);
468        self.lin.qw.write(w);
469        self.lin.qb.write(w);
470    }
471    fn read(r: &mut BitReader) -> Result<Self, DecodeError> {
472        let out = eg_get(r, 0)? as usize;
473        let cin = eg_get(r, 0)? as usize;
474        let k = eg_get(r, 0)? as usize;
475        let fl = eg_get(r, 0)? as u8;
476        let fw = get_fw(r)?;
477        if out > 128 || cin > 128 || !(k == 1 || k == 3) {
478            return Err(DecodeError::Invalid("conv dims"));
479        }
480        let (qw, qb) = (QT::read(r, out * cin * k * k)?, QT::read(r, out)?);
481        let mut lin = Lin::new(out, cin * k * k, qw, qb);
482        lin.set_fw(fw, QA);
483        Ok(Self {
484            lin,
485            k,
486            residual: fl & 1 != 0,
487            relu: fl & 2 != 0,
488        })
489    }
490}
491
492/// A symmetric 1-D upsampling / pre-concatenation filter.
493#[derive(Clone, Debug)]
494pub struct Filt {
495    /// Source taps.
496    pub q: QT,
497    /// Fractional bits.
498    pub fw: u32,
499    /// Taps `Q(fw)`.
500    pub w: Vec<i32>,
501}
502
503impl Filt {
504    fn new(q: QT, fw: u32) -> Self {
505        let w = q.fixed(fw);
506        Self { q, fw, w }
507    }
508}
509
510// ------------------------------------------------------------------- model
511
512/// Everything the decoder needs except the latent payload.
513#[derive(Clone, Debug)]
514pub struct Model {
515    /// Image height, width.
516    pub h: usize,
517    /// Width.
518    pub w: usize,
519    /// Grid sizes `(h, w)`, finest first.
520    pub sizes: Vec<(usize, usize)>,
521    /// Hyperlatent grids (not fed to the synthesis).
522    pub hyper: Vec<bool>,
523    /// Context template: `(dy, dx)` offsets (dy ≤ 0).
524    pub ctx: Vec<(i32, i32)>,
525    /// IFCE output features (appended to the ARM input).
526    pub nifce: usize,
527    /// Per grid: index into `ifces`, or `None` (zero inter-feature context).
528    pub ifce_of: Vec<Option<usize>>,
529    /// Entropy model.
530    pub arm: Mlp,
531    /// Inter-feature context extractors.
532    pub ifces: Vec<Mlp>,
533    /// Upsampling (transposed, even) filters.
534    pub tconv: Vec<Filt>,
535    /// Pre-concatenation (odd, residual) filters.
536    pub pconv: Vec<Filt>,
537    /// Synthesis main branch.
538    pub syn: Vec<Conv>,
539    /// Synthesis stabiliser (1×1 over the input).
540    pub stab: Option<Conv>,
541    /// Output transform (1×1).
542    pub outt: Conv,
543    /// Wavefront slope.
544    pub slope: usize,
545    /// rANS lanes per tile.
546    pub lanes: usize,
547    /// Tile width (0 = whole grid).
548    pub tile_w: usize,
549    /// Tile height (0 = whole grid).
550    pub tile_h: usize,
551}
552
553impl Model {
554    /// Build from an export, with the given lane / tile layout. Weight
555    /// precisions are placeholders until [`encode`] fits them.
556    pub fn from_ccx(c: &Ccx, lanes: usize, tile_w: usize, tile_h: usize) -> Self {
557        let isz = c.ints("img_size");
558        let sz = c.ints("sizes");
559        let sizes: Vec<(usize, usize)> = sz
560            .chunks(2)
561            .map(|p| (p[0] as usize, p[1] as usize))
562            .collect();
563        let hyper = c.get("is_hyper").1.iter().map(|&v| v != 0.0).collect();
564        let ms = c.ints("mask_size")[0] as i32;
565        let ctx = c
566            .ints("ctx_index")
567            .iter()
568            .map(|&k| ((k as i32) / ms - ms / 2, (k as i32) % ms - ms / 2))
569            .collect::<Vec<_>>();
570        let nifce = c.ints("n_ifce_out")[0] as usize;
571        let n = sizes.len();
572        let (ifce_of, ifces) = if c.has("ifce.index_to_arm") {
573            let ia = c.ints("ifce.index_to_arm");
574            let nf = c.ints("ifce.input_features");
575            let of = (0..n)
576                .map(|i| (nf[i] > 0 && ia[i] >= 0).then_some(ia[i] as usize))
577                .collect();
578            let na = ia.iter().copied().max().unwrap_or(-1) + 1;
579            (
580                of,
581                (0..na)
582                    .map(|j| Mlp::from_ccx(c, &format!("ifce.A{j}"), false))
583                    .collect(),
584            )
585        } else {
586            (vec![None; n], vec![])
587        };
588        let filts = |p: &str| {
589            (0..)
590                .map_while(|i| {
591                    c.has(&format!("{p}.{i}"))
592                        .then(|| Filt::new(QT::from_f64(&c.get(&format!("{p}.{i}")).1), 12))
593                })
594                .collect::<Vec<_>>()
595        };
596        let syn = (0..)
597            .map_while(|i| {
598                c.has(&format!("syn.L{i}.w"))
599                    .then(|| Conv::from_ccx(c, &format!("syn.L{i}")))
600            })
601            .collect();
602        let stab = c.has("syn.stab.w").then(|| Conv::from_ccx(c, "syn.stab"));
603        let mut m = Self {
604            h: isz[0] as usize,
605            w: isz[1] as usize,
606            sizes,
607            hyper,
608            slope: 0,
609            ctx,
610            nifce,
611            ifce_of,
612            arm: Mlp::from_ccx(c, "arm", true),
613            ifces,
614            tconv: filts("ups.tconv"),
615            pconv: filts("ups.conv"),
616            syn,
617            stab,
618            outt: Conv::from_ccx(c, "syn.out"),
619            lanes,
620            tile_w,
621            tile_h,
622        };
623        m.slope = slope_of(&m.ctx);
624        // NB: the export already carries Cool-chic's −4 log-scale shift in the
625        // ARM's last bias (`arm_to_fixed_point_param` applies it in place
626        // during the reference decode that `export_cc.py` captures).
627        m
628    }
629}
630
631/// Cool-chic's context priority order (`get_priority_order`, 9×9 causal mask)
632/// as `(dy, dx)`.
633pub const CTX_ORDER: [(i32, i32); 40] = {
634    const PRIO: [u8; 40] = [
635        38, 35, 30, 25, 23, 31, 36, 37, 39, 33, 28, 21, 20, 6, 15, 22, 29, 34, 32, 18, 12, 10, 5,
636        9, 14, 19, 27, 24, 13, 8, 2, 1, 3, 11, 17, 26, 16, 7, 4, 0,
637    ];
638    let mut out = [(0i32, 0i32); 40];
639    let mut k = 0;
640    while k < 40 {
641        out[PRIO[k] as usize] = ((k / 9) as i32 - 4, (k % 9) as i32 - 4);
642        k += 1;
643    }
644    out
645};
646
647/// Smallest `s` with `t(y, x) = x + s·y` strictly after every context.
648pub fn slope_of(ctx: &[(i32, i32)]) -> usize {
649    let mut s = 1;
650    for &(dy, dx) in ctx {
651        if dy < 0 {
652            s = s.max((dx.max(0) / -dy + 1) as usize);
653        }
654    }
655    s
656}
657
658// ------------------------------------------------------------ prob tables
659
660/// Laplace CDF tables `[fb][lb][sym+1]` (cumulative, `Q(PB)`), built with
661/// plain IEEE f64 ops on the host and shared by both decoders.
662pub fn tables() -> &'static Vec<u32> {
663    static T: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
664    T.get_or_init(|| {
665        let mut out = Vec::with_capacity(NFB * NLB * (NSYM + 1));
666        for fb in 0..NFB {
667            let fbw = (1 << FB_SHIFT) as f64;
668            let mu = (fb as f64 * fbw - 128.0 + fbw / 2.0) / 256.0;
669            for lb in 0..NLB {
670                let b = ((lb << LB_SHIFT) as f64 / 256.0 - 5.0).exp();
671                let cdf = |x: f64| {
672                    let d = x - mu;
673                    if d < 0.0 {
674                        0.5 * (d / b).exp()
675                    } else {
676                        1.0 - 0.5 * (-d / b).exp()
677                    }
678                };
679                let mut p = vec![0f64; NSYM];
680                p[0] = cdf(-R as f64 - 0.5);
681                for r in -R..=R {
682                    p[(r + R + 1) as usize] = cdf(r as f64 + 0.5) - cdf(r as f64 - 0.5);
683                }
684                p[NSYM - 1] = 1.0 - cdf(R as f64 + 0.5);
685                let budget = (PM - NSYM as u32) as f64;
686                let mut f: Vec<u32> = p.iter().map(|&q| 1 + (q * budget).floor() as u32).collect();
687                let tot: u32 = f.iter().sum();
688                let mut best = 0;
689                for i in 1..NSYM {
690                    if f[i] > f[best] {
691                        best = i;
692                    }
693                }
694                f[best] += PM - tot;
695                let mut c = 0;
696                out.push(0);
697                for &x in &f {
698                    c += x;
699                    out.push(c);
700                }
701            }
702        }
703        out
704    })
705}
706
707/// `(base, table offset)` from the ARM output (μ, ln b in `Q8`).
708#[inline]
709pub fn bucket(mu: i32, ls: i32) -> (i32, usize) {
710    let mu = mu.clamp(-64 * 256, 64 * 256 - 1);
711    let base = (mu + 128) >> 8;
712    let fb = ((mu - (base << 8) + 128) >> FB_SHIFT) as usize;
713    let lb = ((ls + 1280 + (1 << (LB_SHIFT - 1))) >> LB_SHIFT).clamp(0, NLB as i32 - 1) as usize;
714    (base, (fb * NLB + lb) * (NSYM + 1))
715}
716
717// ------------------------------------------------------------------- rANS
718
719struct RansEnc {
720    x: u32,
721    words: Vec<u16>,
722}
723
724impl RansEnc {
725    fn new() -> Self {
726        Self {
727            x: RL,
728            words: vec![],
729        }
730    }
731    fn put(&mut self, start: u32, freq: u32) {
732        let xmax = ((RL >> PB) << 16) * freq;
733        if self.x >= xmax {
734            self.words.push(self.x as u16);
735            self.x >>= 16;
736        }
737        self.x = ((self.x / freq) << PB) + self.x % freq + start;
738    }
739    /// Words in decode order: final state (2 words), then payload.
740    fn finish(mut self) -> Vec<u16> {
741        self.words.push(self.x as u16);
742        self.words.push((self.x >> 16) as u16);
743        self.words.reverse();
744        self.words
745    }
746}
747
748/// Decoder side of one lane.
749#[derive(Clone, Copy)]
750pub struct RansDec {
751    /// State.
752    pub x: u32,
753    /// Next word.
754    pub pos: usize,
755}
756
757impl RansDec {
758    /// Start at `words[pos..]`.
759    pub fn new(words: &[u16], pos: usize) -> Self {
760        let x = (words[pos] as u32) << 16 | words[pos + 1] as u32;
761        Self { x, pos: pos + 2 }
762    }
763    #[inline]
764    fn peek(&self) -> u32 {
765        self.x & (PM - 1)
766    }
767    #[inline]
768    fn adv(&mut self, words: &[u16], start: u32, freq: u32) {
769        self.x = freq * (self.x >> PB) + self.peek() - start;
770        if self.x < RL {
771            self.x = self.x << 16 | *words.get(self.pos).unwrap_or(&0) as u32;
772            self.pos += 1;
773        }
774    }
775}
776
777/// Decode one latent value with the table at `off` (`cum[0..=NSYM]`).
778#[inline]
779pub fn dec_sym(d: &mut RansDec, words: &[u16], tab: &[u32], base: i32, off: usize) -> i32 {
780    let cum = &tab[off..off + NSYM + 1];
781    let v = d.peek();
782    let (mut lo, mut hi) = (0usize, NSYM);
783    while hi - lo > 1 {
784        let mid = (lo + hi) / 2;
785        if cum[mid] <= v {
786            lo = mid;
787        } else {
788            hi = mid;
789        }
790    }
791    d.adv(words, cum[lo], cum[lo + 1] - cum[lo]);
792    if lo == 0 || lo == NSYM - 1 {
793        {
794            let mut e = 0u32;
795            for _ in 0..2 {
796                let b = d.peek() >> (PB - 8);
797                d.adv(words, b << (PB - 8), 1 << (PB - 8));
798                e = e << 8 | b;
799            }
800            let e = e as i32;
801            if lo == 0 {
802                base - R - 1 - e
803            } else {
804                base + R + 1 + e
805            }
806        }
807    } else {
808        base + lo as i32 - R - 1
809    }
810}
811
812/// Encode-side symbol list for one value (in decode order).
813fn enc_syms(v: i32, base: i32, off: usize, out: &mut Vec<(u32, u32)>) {
814    let tab = tables();
815    let cum = &tab[off..off + NSYM + 1];
816    let r = v - base;
817    let (s, esc) = if r < -R {
818        (0, Some(-R - 1 - r))
819    } else if r > R {
820        (NSYM - 1, Some(r - R - 1))
821    } else {
822        ((r + R + 1) as usize, None)
823    };
824    out.push((cum[s], cum[s + 1] - cum[s]));
825    if let Some(e) = esc {
826        assert!((0..65536).contains(&e), "latent escape out of range");
827        for b in [(e >> 8) as u32, (e & 255) as u32] {
828            out.push((b << (PB - 8), 1 << (PB - 8)));
829        }
830    }
831}
832
833// --------------------------------------------------------------- schedule
834
835/// Wavefront layout of one grid: tiles of `th × tw`, `lanes` lanes per tile.
836#[derive(Clone, Copy, Debug)]
837pub struct Layout {
838    /// Grid height, width.
839    pub h: usize,
840    /// Width.
841    pub w: usize,
842    /// Tile height, width.
843    pub th: usize,
844    /// Tile width.
845    pub tw: usize,
846    /// Tile columns.
847    pub ntx: usize,
848    /// Tiles.
849    pub nt: usize,
850    /// Lanes per tile (≤ th); tile `j` uses global lanes `j·lanes ..`.
851    pub lanes: usize,
852    /// Slope.
853    pub s: usize,
854    /// Wavefront steps per tile.
855    pub steps: usize,
856}
857
858impl Layout {
859    /// Layout for an `h × w` grid.
860    pub fn new(h: usize, w: usize, m: &Model) -> Self {
861        let tw = if m.tile_w == 0 { w } else { m.tile_w.min(w) };
862        let th = if m.tile_h == 0 { h } else { m.tile_h.min(h) };
863        let ntx = w.div_ceil(tw);
864        let nt = ntx * h.div_ceil(th);
865        assert!(nt <= m.lanes, "more tiles than lanes");
866        let lanes = (m.lanes / nt).min(th).max(1);
867        Self {
868            h,
869            w,
870            th,
871            tw,
872            ntx,
873            nt,
874            lanes,
875            s: m.slope,
876            steps: tw + m.slope * (th - 1),
877        }
878    }
879    /// Tile origin and extent `(y0, x0, th, tw)`.
880    #[inline]
881    pub fn tile(&self, tile: usize) -> (usize, usize, usize, usize) {
882        let (y0, x0) = ((tile / self.ntx) * self.th, (tile % self.ntx) * self.tw);
883        (y0, x0, self.th.min(self.h - y0), self.tw.min(self.w - x0))
884    }
885    /// Pixels of lane `l` active at step `t`, as global `(y, x)`, in decode order.
886    #[inline]
887    pub fn active(&self, l: usize, t: usize, tile: usize, mut f: impl FnMut(usize, usize)) {
888        let (y0, x0, th, tw) = self.tile(tile);
889        let mut y = l;
890        while y < th {
891            let sy = self.s * y;
892            if sy > t {
893                break;
894            }
895            let x = t - sy;
896            if x < tw {
897                f(y0 + y, x0 + x);
898            }
899            y += self.lanes;
900        }
901    }
902}
903
904// ----------------------------------------------------------- latent model
905
906/// Gather the ARM input for global `(y, x)` of a grid (values so far in
907/// `lat`, context cut at the tile border) into `buf` (`Q8`).
908#[inline]
909#[allow(clippy::too_many_arguments)]
910pub fn arm_input(
911    m: &Model,
912    lay: &Layout,
913    lat: &[i32],
914    ifc: &[i32],
915    y: usize,
916    x: usize,
917    tile: usize,
918    buf: &mut [i32],
919) {
920    let (y0, x0, th, tw) = lay.tile(tile);
921    for (i, &(dy, dx)) in m.ctx.iter().enumerate() {
922        let (yy, xx) = (y as i32 + dy, x as i32 + dx);
923        buf[i] =
924            if yy >= y0 as i32 && yy < (y0 + th) as i32 && xx >= x0 as i32 && xx < (x0 + tw) as i32
925            {
926                lat[yy as usize * lay.w + xx as usize] << QA_ARM
927            } else {
928                0
929            };
930    }
931    let nc = m.ctx.len();
932    for j in 0..m.nifce {
933        buf[nc + j] = if ifc.is_empty() {
934            0
935        } else {
936            ifc[(y * lay.w + x) * m.nifce + j]
937        };
938    }
939}
940
941/// Inter-feature context of grid `g` (`[h·w][nifce]`, `Q8`) from the
942/// already-decoded coarser grids, or empty when the grid has no IFCE.
943pub fn ifce_ctx(m: &Model, g: usize, grids: &[Vec<i32>], gd: &mut Guard) -> Vec<i32> {
944    let Some(a) = m.ifce_of[g] else { return vec![] };
945    let mlp = &m.ifces[a];
946    let n = m.sizes.len();
947    // nearest-upsampled stack of grids g+1..n−1 at the resolution of grid g+1
948    // (channel order: g+1, g+2, …), as Cool-chic's `fixed_upsampling`
949    let (h1, w1) = m.sizes[g + 1];
950    let mut stack: Vec<Vec<i32>> = vec![];
951    let (mut ch, mut cw) = m.sizes[n - 1];
952    let mut cur: Vec<Vec<i32>> = vec![grids[n - 1].clone()];
953    for k in (g + 1..n - 1).rev() {
954        let (th, tw) = m.sizes[k];
955        if (th, tw) != (ch, cw) {
956            cur = cur
957                .iter()
958                .map(|c| {
959                    (0..th * tw)
960                        .map(|i| c[(i / tw / 2).min(ch - 1) * cw + (i % tw / 2).min(cw - 1)])
961                        .collect()
962                })
963                .collect();
964            (ch, cw) = (th, tw);
965        }
966        let mut nc = vec![grids[k].clone()];
967        nc.extend(cur);
968        cur = nc;
969    }
970    stack.extend(cur);
971    assert_eq!((ch, cw), (h1, w1));
972    assert_eq!(stack.len(), mlp.nin());
973    let (h, w) = m.sizes[g];
974    let mut low = vec![0i32; h1 * w1 * m.nifce];
975    let mut inb = vec![0i32; mlp.nin()];
976    for p in 0..h1 * w1 {
977        for c in 0..stack.len() {
978            inb[c] = stack[c][p] << QA_ARM;
979        }
980        mlp.run(&inb, &mut low[p * m.nifce..(p + 1) * m.nifce], gd);
981    }
982    let mut out = vec![0i32; h * w * m.nifce];
983    for y in 0..h {
984        for x in 0..w {
985            let s = ((y / 2).min(h1 - 1) * w1 + (x / 2).min(w1 - 1)) * m.nifce;
986            out[(y * w + x) * m.nifce..][..m.nifce].copy_from_slice(&low[s..s + m.nifce]);
987        }
988    }
989    out
990}
991
992// ---------------------------------------------------------------- stream
993
994/// Parsed stream.
995pub struct Parsed {
996    /// Model.
997    pub m: Model,
998    /// All lanes' rANS words, concatenated.
999    pub words: Vec<u16>,
1000    /// Start of each lane's words.
1001    pub starts: Vec<u32>,
1002}
1003
1004fn write_model(m: &Model, w: &mut BitWriter) {
1005    eg_put(w, m.h as u64, 0);
1006    eg_put(w, m.w as u64, 0);
1007    eg_put(w, m.sizes.len() as u64, 0);
1008    for (i, &(h, ww)) in m.sizes.iter().enumerate() {
1009        // usually (⌈H/2^d⌉, ⌈W/2^d⌉): send d; else d = 31 and the size
1010        match (0..31).find(|&d| (h, ww) == (m.h.div_ceil(1 << d), m.w.div_ceil(1 << d))) {
1011            Some(d) => eg_put(w, d as u64, 0),
1012            None => {
1013                eg_put(w, 31, 0);
1014                eg_put(w, h as u64, 0);
1015                eg_put(w, ww as u64, 0);
1016            }
1017        }
1018        eg_put(
1019            w,
1020            (m.hyper[i] as u8 | (m.ifce_of[i].map_or(0, |a| a as u8 + 1) << 1)) as u64,
1021            0,
1022        );
1023    }
1024    // context: Cool-chic's priority order prefix (flag 1), or explicit
1025    let canon = m.ctx.len() <= CTX_ORDER.len() && m.ctx.iter().zip(CTX_ORDER).all(|(a, b)| *a == b);
1026    eg_put(w, (m.ctx.len() << 1 | canon as usize) as u64, 0);
1027    if !canon {
1028        for &(dy, dx) in &m.ctx {
1029            eg_put(w, (((-dy) as u8) << 4 | (dx + 8) as u8) as u64, 0);
1030        }
1031    }
1032    eg_put(w, m.nifce as u64, 0);
1033    eg_put(w, m.lanes as u64, 0);
1034    eg_put(w, m.tile_w as u64, 0);
1035    eg_put(w, m.tile_h as u64, 0);
1036    m.arm.write(w);
1037    eg_put(w, m.ifces.len() as u64, 0);
1038    for f in &m.ifces {
1039        f.write(w);
1040    }
1041    for fs in [&m.tconv, &m.pconv] {
1042        eg_put(w, fs.len() as u64, 0);
1043        for f in fs {
1044            // symmetric taps: send the first ⌈k/2⌉
1045            let k = f.q.k.len();
1046            assert!(
1047                (0..k).all(|i| f.q.k[i] == f.q.k[k - 1 - i]),
1048                "upsampling filter not symmetric"
1049            );
1050            eg_put(w, k as u64, 0);
1051            put_fw(w, f.fw);
1052            QT {
1053                s: f.q.s,
1054                k: f.q.k[..k.div_ceil(2)].to_vec(),
1055            }
1056            .write(w);
1057        }
1058    }
1059    eg_put(w, m.syn.len() as u64, 0);
1060    for c in &m.syn {
1061        c.write(w);
1062    }
1063    eg_put(w, (m.stab.is_some() as u8) as u64, 0);
1064    if let Some(s) = &m.stab {
1065        s.write(w);
1066    }
1067    m.outt.write(w);
1068}
1069
1070fn read_model(r: &mut BitReader) -> Result<Model, DecodeError> {
1071    let bad = |s| DecodeError::Invalid(s);
1072    let h = eg_get(r, 0)? as usize;
1073    let w = eg_get(r, 0)? as usize;
1074    let n = eg_get(r, 0)? as usize;
1075    if h == 0 || w == 0 || h * w > 1 << 26 || n == 0 || n > 16 {
1076        return Err(bad("dims"));
1077    }
1078    let (mut sizes, mut hyper, mut ifce_of) = (vec![], vec![], vec![]);
1079    for _ in 0..n {
1080        let d = eg_get(r, 0)?;
1081        let (a, b) = match d {
1082            0..=30 => (h.div_ceil(1 << d), w.div_ceil(1 << d)),
1083            31 => (eg_get(r, 0)? as usize, eg_get(r, 0)? as usize),
1084            _ => return Err(bad("grid size")),
1085        };
1086        if a == 0 || b == 0 || a > h || b > w {
1087            return Err(bad("grid size"));
1088        }
1089        sizes.push((a, b));
1090        let f = eg_get(r, 0)? as u8;
1091        hyper.push(f & 1 != 0);
1092        ifce_of.push((f >> 1 != 0).then(|| (f >> 1) as usize - 1));
1093    }
1094    let nc = eg_get(r, 0)? as usize;
1095    let canon = nc & 1 == 1;
1096    let nc = nc >> 1;
1097    if nc > CTX_ORDER.len() {
1098        return Err(bad("ctx"));
1099    }
1100    let ctx = if canon {
1101        CTX_ORDER[..nc].to_vec()
1102    } else {
1103        (0..nc)
1104            .map(|_| eg_get(r, 0).map(|b| (-((b >> 4) as i32), (b & 15) as i32 - 8)))
1105            .collect::<Result<Vec<_>, _>>()?
1106    };
1107    let nifce = eg_get(r, 0)? as usize;
1108    let lanes = eg_get(r, 0)? as usize;
1109    let tile_w = eg_get(r, 0)? as usize;
1110    let tile_h = eg_get(r, 0)? as usize;
1111    if nifce > 16 || lanes == 0 || lanes > 1024 {
1112        return Err(bad("lanes"));
1113    }
1114    let arm = Mlp::read(r)?;
1115    let ni = eg_get(r, 0)? as usize;
1116    let ifces = (0..ni.min(16))
1117        .map(|_| Mlp::read(r))
1118        .collect::<Result<Vec<_>, _>>()?;
1119    let mut fl = vec![];
1120    for _ in 0..2 {
1121        let k = eg_get(r, 0)? as usize;
1122        let mut v = vec![];
1123        for _ in 0..k.min(32) {
1124            let k = eg_get(r, 0)? as usize;
1125            let fw = get_fw(r)?;
1126            if !(2..=16).contains(&k) {
1127                return Err(bad("filter"));
1128            }
1129            let mut q = QT::read(r, k.div_ceil(2))?;
1130            for i in (0..k / 2).rev() {
1131                let v = q.k[i];
1132                q.k.push(v);
1133            }
1134            v.push(Filt::new(q, fw));
1135        }
1136        fl.push(v);
1137    }
1138    let ns = eg_get(r, 0)? as usize;
1139    let syn = (0..ns.min(16))
1140        .map(|_| Conv::read(r))
1141        .collect::<Result<Vec<_>, _>>()?;
1142    let stab = if (eg_get(r, 0)? as u8) != 0 {
1143        Some(Conv::read(r)?)
1144    } else {
1145        None
1146    };
1147    let outt = Conv::read(r)?;
1148    let pconv = fl.pop().unwrap();
1149    let tconv = fl.pop().unwrap();
1150    let slope = slope_of(&ctx);
1151    let m = Model {
1152        h,
1153        w,
1154        sizes,
1155        hyper,
1156        ctx,
1157        nifce,
1158        ifce_of,
1159        arm,
1160        ifces,
1161        tconv,
1162        pconv,
1163        syn,
1164        stab,
1165        outt,
1166        slope,
1167        lanes,
1168        tile_w,
1169        tile_h,
1170    };
1171    if m.arm.nin() != m.ctx.len() + m.nifce
1172        || m.arm.nout() != 2
1173        || m.ifce_of.iter().flatten().any(|&a| a >= m.ifces.len())
1174    {
1175        return Err(bad("model shape"));
1176    }
1177    Ok(m)
1178}
1179
1180/// Parse a `TCI1` stream.
1181pub fn parse(bytes: &[u8]) -> Result<Parsed, DecodeError> {
1182    let mut r = ByteReader::new(bytes);
1183    if r.take(4)? != MAGIC {
1184        return Err(DecodeError::Invalid("not TCI1"));
1185    }
1186    let mb = r.block()?;
1187    let m = read_model(&mut BitReader::new(mb))?;
1188    let mut starts = vec![];
1189    let mut acc = 0u32;
1190    for _ in 0..m.lanes {
1191        starts.push(acc);
1192        acc = acc
1193            .checked_add(r.varint()? as u32)
1194            .ok_or(DecodeError::Invalid("lanes"))?;
1195    }
1196    let raw = r.take(acc as usize * 2)?;
1197    let words = raw
1198        .as_chunks::<2>()
1199        .0
1200        .iter()
1201        .map(|c| u16::from_le_bytes(*c))
1202        .collect();
1203    for g in 0..m.sizes.len() {
1204        let lay = Layout::new(m.sizes[g].0, m.sizes[g].1, &m);
1205        if lay.nt > m.lanes {
1206            return Err(DecodeError::Invalid("tiles > lanes"));
1207        }
1208    }
1209    Ok(Parsed { m, words, starts })
1210}
1211
1212// ------------------------------------------------------------- decoding
1213
1214/// Decode the latent grids (finest first) on the CPU. Lane `l`'s rANS
1215/// state runs on across grids (coarse → fine).
1216pub fn decode_latents(p: &Parsed, gd: &mut Guard) -> Vec<Vec<i32>> {
1217    let m = &p.m;
1218    let n = m.sizes.len();
1219    let tab = tables();
1220    let mut grids: Vec<Vec<i32>> = vec![vec![]; n];
1221    let mut inb = vec![0i32; m.arm.nin()];
1222    let mut o = [0i32; 2];
1223    let mut decs: Vec<RansDec> = p
1224        .starts
1225        .iter()
1226        .map(|&s| RansDec::new(&p.words, s as usize))
1227        .collect();
1228    for g in (0..n).rev() {
1229        let (h, w) = m.sizes[g];
1230        let lay = Layout::new(h, w, m);
1231        let ifc = ifce_ctx(m, g, &grids, gd);
1232        let mut lat = vec![0i32; h * w];
1233        for tile in 0..lay.nt {
1234            for t in 0..lay.steps {
1235                for l in 0..lay.lanes {
1236                    let d = &mut decs[tile * lay.lanes + l];
1237                    lay.active(l, t, tile, |y, x| {
1238                        arm_input(m, &lay, &lat, &ifc, y, x, tile, &mut inb);
1239                        m.arm.run(&inb, &mut o, gd);
1240                        let (base, off) = bucket(o[0], o[1]);
1241                        lat[y * w + x] = dec_sym(d, &p.words, tab, base, off);
1242                    });
1243                }
1244            }
1245        }
1246        grids[g] = lat;
1247    }
1248    grids
1249}
1250
1251/// Symmetric 1-D filter taps → index helpers for the 2× transposed conv.
1252/// `out[m] = Σ_j xpad[j]·w[m + C − 2j]`, `xpad[j] = x[clamp(j − P0)]`.
1253fn tconv_1d(
1254    x: &[i64],
1255    n: usize,
1256    stride: usize,
1257    f: &Filt,
1258    out: &mut [i64],
1259    ostride: usize,
1260    gd: &mut Guard,
1261) {
1262    let k = f.w.len();
1263    let p0 = (k / 2) as i64;
1264    let c = 2 * p0 - 1 + p0;
1265    for m in 0..2 * n {
1266        let mut acc = 0i64;
1267        // j with 0 ≤ m + c − 2j < k
1268        let jlo = ((m as i64 + c - k as i64 + 1) + 1).div_euclid(2).max(0);
1269        let jhi = (m as i64 + c).div_euclid(2);
1270        for j in jlo..=jhi {
1271            let t = m as i64 + c - 2 * j;
1272            if t < 0 || t >= k as i64 {
1273                continue;
1274            }
1275            let src = (j - p0).clamp(0, n as i64 - 1) as usize;
1276            acc = gd.see(acc + f.w[t as usize] as i64 * x[src * stride]);
1277        }
1278        out[m * ostride] = gd.see(rshift(acc, f.fw));
1279    }
1280}
1281
1282/// `2×` learned upsampling of one channel (`h × w` → `2h × 2w`, `Q(QA)`).
1283fn tconv2d(x: &[i64], h: usize, w: usize, f: &Filt, gd: &mut Guard) -> Vec<i64> {
1284    let mut t = vec![0i64; h * 2 * w];
1285    for y in 0..h {
1286        tconv_1d(&x[y * w..], w, 1, f, &mut t[y * 2 * w..], 1, gd);
1287    }
1288    let mut o = vec![0i64; 4 * h * w];
1289    for xx in 0..2 * w {
1290        tconv_1d(&t[xx..], h, 2 * w, f, &mut o[xx..], 2 * w, gd);
1291    }
1292    o
1293}
1294
1295/// Residual separable symmetric odd filter (zero padding): `y = w⊗w ∗ x + x`.
1296fn pconv2d(x: &[i64], h: usize, w: usize, f: &Filt, gd: &mut Guard) -> Vec<i64> {
1297    let k = f.w.len();
1298    let p = (k / 2) as i64;
1299    let mut t = vec![0i64; h * w];
1300    for y in 0..h {
1301        for xx in 0..w {
1302            let mut acc = 0i64;
1303            for i in 0..k {
1304                let sx = xx as i64 + i as i64 - p;
1305                if sx >= 0 && sx < w as i64 {
1306                    acc = gd.see(acc + f.w[i] as i64 * x[y * w + sx as usize]);
1307                }
1308            }
1309            t[y * w + xx] = gd.see(rshift(acc, f.fw));
1310        }
1311    }
1312    let mut o = vec![0i64; h * w];
1313    for y in 0..h {
1314        for xx in 0..w {
1315            let mut acc = 0i64;
1316            for i in 0..k {
1317                let sy = y as i64 + i as i64 - p;
1318                if sy >= 0 && sy < h as i64 {
1319                    acc = gd.see(acc + f.w[i] as i64 * t[sy as usize * w + xx]);
1320                }
1321            }
1322            o[y * w + xx] = gd.see(rshift(acc, f.fw) + x[y * w + xx]);
1323        }
1324    }
1325    o
1326}
1327
1328/// Upsampling pyramid → dense synthesis input `[C][h·w]` (`Q(QA)`).
1329pub fn upsample(m: &Model, grids: &[Vec<i32>], gd: &mut Guard) -> Vec<Vec<i64>> {
1330    let idx: Vec<usize> = (0..m.sizes.len()).filter(|&i| !m.hyper[i]).collect();
1331    let mut it = idx.iter().rev();
1332    let &g0 = it.next().unwrap();
1333    let (mut ch, mut cw) = m.sizes[g0];
1334    let mut up: Vec<Vec<i64>> = vec![grids[g0].iter().map(|&v| (v as i64) << QA).collect()];
1335    for (k, &g) in it.enumerate() {
1336        let (th, tw) = m.sizes[g];
1337        let f = &m.tconv[k % m.tconv.len()];
1338        let ups: Vec<Vec<i64>> = up
1339            .iter()
1340            .map(|c| {
1341                let full = tconv2d(c, ch, cw, f, gd);
1342                (0..th * tw)
1343                    .map(|i| full[(i / tw) * 2 * cw + i % tw])
1344                    .collect()
1345            })
1346            .collect();
1347        let tgt: Vec<i64> = grids[g].iter().map(|&v| (v as i64) << QA).collect();
1348        let hi = pconv2d(&tgt, th, tw, &m.pconv[k % m.pconv.len()], gd);
1349        up = std::iter::once(hi).chain(ups).collect();
1350        (ch, cw) = (th, tw);
1351    }
1352    assert_eq!((ch, cw), (m.h, m.w));
1353    up
1354}
1355
1356#[allow(clippy::too_many_arguments)]
1357fn conv_px(
1358    c: &Conv,
1359    inp: &[Vec<i64>],
1360    h: usize,
1361    w: usize,
1362    y: usize,
1363    x: usize,
1364    o: usize,
1365    gd: &mut Guard,
1366) -> i64 {
1367    let l = &c.lin;
1368    let cin = c.cin();
1369    let k = c.k;
1370    let p = (k / 2) as i64;
1371    let mut acc = l.b[o] as i64;
1372    for ci in 0..cin {
1373        for dy in 0..k {
1374            let sy = (y as i64 + dy as i64 - p).clamp(0, h as i64 - 1) as usize;
1375            for dx in 0..k {
1376                let sx = (x as i64 + dx as i64 - p).clamp(0, w as i64 - 1) as usize;
1377                acc = gd.see(
1378                    acc + l.w[((o * cin + ci) * k + dy) * k + dx] as i64 * inp[ci][sy * w + sx],
1379                );
1380            }
1381        }
1382    }
1383    let mut v = gd.see(rshift(acc, l.fw));
1384    if c.residual {
1385        v = gd.see(v + inp[o][y * w + x]);
1386    }
1387    if c.relu {
1388        v = v.max(0);
1389    }
1390    v
1391}
1392
1393fn conv_all(c: &Conv, inp: &[Vec<i64>], h: usize, w: usize, gd: &mut Guard) -> Vec<Vec<i64>> {
1394    (0..c.lin.out)
1395        .map(|o| {
1396            (0..h * w)
1397                .map(|i| conv_px(c, inp, h, w, i / w, i % w, o, gd))
1398                .collect()
1399        })
1400        .collect()
1401}
1402
1403/// Synthesis → interleaved RGB8.
1404pub fn synthesize(m: &Model, dense: &[Vec<i64>], gd: &mut Guard) -> Vec<u8> {
1405    let (h, w) = (m.h, m.w);
1406    let mut x = dense.to_vec();
1407    for c in &m.syn {
1408        x = conv_all(c, &x, h, w, gd);
1409    }
1410    if let Some(s) = &m.stab {
1411        let sb = conv_all(s, &dense[..s.cin()], h, w, gd);
1412        for (a, b) in x.iter_mut().zip(sb) {
1413            for (p, q) in a.iter_mut().zip(b) {
1414                *p = gd.see(*p + q);
1415            }
1416        }
1417    }
1418    let y = conv_all(&m.outt, &x, h, w, gd);
1419    let mut out = vec![0u8; h * w * 3];
1420    for i in 0..h * w {
1421        for c in 0..3 {
1422            out[i * 3 + c] = rshift(gd.see(y[c][i] * 255), QA).clamp(0, 255) as u8;
1423        }
1424    }
1425    out
1426}
1427
1428/// Full CPU decode: stream → RGB8 (`h·w·3`). Errors if any accumulation
1429/// leaves `i32` (the GPU would wrap).
1430pub fn decode(bytes: &[u8]) -> Result<(usize, usize, Vec<u8>), DecodeError> {
1431    let p = parse(bytes)?;
1432    let mut gd = Guard::default();
1433    let grids = decode_latents(&p, &mut gd);
1434    let dense = upsample(&p.m, &grids, &mut gd);
1435    let rgb = synthesize(&p.m, &dense, &mut gd);
1436    if !gd.ok() {
1437        return Err(DecodeError::Invalid("i32 overflow"));
1438    }
1439    Ok((p.m.h, p.m.w, rgb))
1440}
1441
1442// ------------------------------------------------------------- encoding
1443
1444/// Encoder statistics.
1445#[derive(Debug, Clone, Default)]
1446pub struct Stats {
1447    /// Total stream bytes.
1448    pub bytes: usize,
1449    /// Model (header + nets) bytes.
1450    pub model_bytes: usize,
1451    /// Latent payload bytes (lane offsets + words).
1452    pub latent_bytes: usize,
1453    /// Ideal latent bits per grid under the quantized tables.
1454    pub grid_bits: Vec<f64>,
1455    /// Ideal latent bits under the quantized tables (no lane overhead).
1456    pub ideal_bits: f64,
1457}
1458
1459/// Encode an export into a `TCI1` stream with the given lanes / tile width.
1460pub fn encode(c: &Ccx, lanes: usize, tile_w: usize, tile_h: usize) -> (Vec<u8>, Stats) {
1461    let mut m = Model::from_ccx(c, lanes, tile_w, tile_h);
1462    let n = m.sizes.len();
1463    let grids: Vec<Vec<i32>> = (0..n)
1464        .map(|g| {
1465            c.ints(&format!("latent.{g}"))
1466                .iter()
1467                .map(|&v| v as i32)
1468                .collect()
1469        })
1470        .collect();
1471    for g in 0..n {
1472        assert_eq!(grids[g].len(), m.sizes[g].0 * m.sizes[g].1);
1473    }
1474    // simple, exact approach: set caps, then shrink until the full decode
1475    // passes the guard (encoder runs the decoder anyway)
1476    let fit_all = |m: &mut Model, cap: u32| {
1477        let ws = vec![cap; m.arm.layers.len() + 1];
1478        m.arm.set_fw(&ws);
1479        for f in &mut m.ifces {
1480            let ws = vec![cap; f.layers.len() + 1];
1481            f.set_fw(&ws);
1482        }
1483        for f in m.tconv.iter_mut().chain(m.pconv.iter_mut()) {
1484            *f = Filt::new(f.q.clone(), cap.min(14));
1485        }
1486        for cv in m
1487            .syn
1488            .iter_mut()
1489            .chain(m.stab.iter_mut())
1490            .chain(std::iter::once(&mut m.outt))
1491        {
1492            cv.lin.set_fw(cap.min(14), QA);
1493        }
1494    };
1495    let mut cap = 16;
1496    loop {
1497        fit_all(&mut m, cap);
1498        let mut gd = Guard::default();
1499        // check every stage on the true latents
1500        let mut ok = true;
1501        let mut inb = vec![0i32; m.arm.nin()];
1502        let mut o = [0i32; 2];
1503        for g in (0..n).rev() {
1504            let (h, w) = m.sizes[g];
1505            let lay = Layout::new(h, w, &m);
1506            let ifc = ifce_ctx(&m, g, &grids, &mut gd);
1507            for tile in 0..lay.nt {
1508                let (y0, x0, th, tw) = lay.tile(tile);
1509                for y in y0..y0 + th {
1510                    for x in x0..x0 + tw {
1511                        arm_input(&m, &lay, &grids[g], &ifc, y, x, tile, &mut inb);
1512                        m.arm.run(&inb, &mut o, &mut gd);
1513                    }
1514                }
1515            }
1516        }
1517        if gd.ok() {
1518            let dense = upsample(&m, &grids, &mut gd);
1519            let _ = synthesize(&m, &dense, &mut gd);
1520        }
1521        ok &= gd.ok();
1522        if ok || cap <= 6 {
1523            break;
1524        }
1525        cap -= 1;
1526    }
1527    // entropy-code the latents: per lane, symbols in decode order
1528    let mut w = ByteWriter::new();
1529    w.bytes(MAGIC);
1530    let mut bw = BitWriter::new();
1531    write_model(&m, &mut bw);
1532    w.block(&bw.finish());
1533    let model_bytes = w.buf.len();
1534    let mut st = Stats {
1535        model_bytes,
1536        grid_bits: vec![0.0; n],
1537        ..Default::default()
1538    };
1539    let mut gd = Guard::default();
1540    let mut inb = vec![0i32; m.arm.nin()];
1541    let mut o = [0i32; 2];
1542    let mut syms: Vec<Vec<(u32, u32)>> = vec![vec![]; m.lanes];
1543    for g in (0..n).rev() {
1544        let (h, ww) = m.sizes[g];
1545        let lay = Layout::new(h, ww, &m);
1546        let ifc = ifce_ctx(&m, g, &grids, &mut gd);
1547        let lat = &grids[g];
1548        for tile in 0..lay.nt {
1549            for t in 0..lay.steps {
1550                for l in 0..lay.lanes {
1551                    let sl = &mut syms[tile * lay.lanes + l];
1552                    lay.active(l, t, tile, |y, x| {
1553                        arm_input(&m, &lay, lat, &ifc, y, x, tile, &mut inb);
1554                        m.arm.run(&inb, &mut o, &mut gd);
1555                        let (base, off) = bucket(o[0], o[1]);
1556                        let v = lat[y * ww + x];
1557                        let k0 = sl.len();
1558                        enc_syms(v, base, off, sl);
1559                        for &(_, f) in &sl[k0..] {
1560                            st.grid_bits[g] += PB as f64 - (f as f64).log2();
1561                        }
1562                    });
1563                }
1564            }
1565        }
1566    }
1567    st.ideal_bits = st.grid_bits.iter().sum();
1568    let lanes: Vec<Vec<u16>> = syms
1569        .iter()
1570        .map(|s| {
1571            let mut e = RansEnc::new();
1572            for &(a, f) in s.iter().rev() {
1573                e.put(a, f);
1574            }
1575            e.finish()
1576        })
1577        .collect();
1578    for l in &lanes {
1579        w.varint(l.len() as u64);
1580    }
1581    for l in &lanes {
1582        for v in l {
1583            w.bytes(&v.to_le_bytes());
1584        }
1585    }
1586    st.bytes = w.buf.len();
1587    st.latent_bytes = st.bytes - model_bytes;
1588    (w.buf, st)
1589}
1590
1591/// PSNR (dB) of two RGB8 buffers.
1592pub fn psnr(a: &[u8], b: &[u8]) -> f64 {
1593    let mse: f64 = a
1594        .iter()
1595        .zip(b)
1596        .map(|(&x, &y)| (x as f64 - y as f64).powi(2))
1597        .sum::<f64>()
1598        / a.len() as f64;
1599    10.0 * (255.0f64 * 255.0 / mse).log10()
1600}
1601
1602// ----------------------------------------------------- integer graphs (GPU)
1603
1604use tang_expr::int::{IId, IType, IntGraph};
1605
1606fn g_layer(g: &mut IntGraph, l: &Lin, inp: &[IId], residual: bool, relu: bool) -> Vec<IId> {
1607    let zero = g.i32(0);
1608    (0..l.out)
1609        .map(|o| {
1610            let mut acc = g.i32(l.b[o]);
1611            for i in 0..l.inp {
1612                let w = l.w[o * l.inp + i];
1613                if w != 0 {
1614                    let wc = g.i32(w);
1615                    let t = g.mul(wc, inp[i]);
1616                    acc = g.add(acc, t);
1617                }
1618            }
1619            let mut v = g_rshift(g, acc, l.fw);
1620            if residual {
1621                v = g.add(v, inp[o]);
1622            }
1623            if relu {
1624                v = g.max(v, zero);
1625            }
1626            v
1627        })
1628        .collect()
1629}
1630
1631fn g_rshift(g: &mut IntGraph, v: IId, f: u32) -> IId {
1632    if f == 0 {
1633        return v;
1634    }
1635    let h = g.i32(1 << (f - 1));
1636    let s = g.u32(f);
1637    let t = g.add(v, h);
1638    g.shr(t, s)
1639}
1640
1641impl Mlp {
1642    /// [`Mlp::run`] as an integer graph: params `0..nin`, outputs `0..nout`.
1643    pub fn graph(&self) -> (IntGraph, Vec<IId>) {
1644        let mut g = IntGraph::new();
1645        let x: Vec<IId> = (0..self.nin())
1646            .map(|i| g.param(i as u16, IType::I32))
1647            .collect();
1648        let mut a = x.clone();
1649        let nl = self.layers.len();
1650        for (li, l) in self.layers.iter().enumerate() {
1651            let hidden = li + 1 < nl;
1652            a = g_layer(
1653                &mut g,
1654                l,
1655                &a,
1656                hidden && self.residual && l.inp == l.out,
1657                hidden,
1658            );
1659        }
1660        if let Some(s) = &self.stab {
1661            let sb = g_layer(&mut g, s, &x, false, false);
1662            a = a.iter().zip(sb).map(|(&p, q)| g.add(p, q)).collect();
1663        }
1664        (g, a)
1665    }
1666}
1667
1668/// A synthesis segment: one (possibly 3×3) conv followed by 1×1 convs.
1669/// Params: the first conv's taps `[cin][k][k]` (replicate-padded gather),
1670/// then (if it is residual) its `cin` centre values; when `last`, then the
1671/// `stab` input channels. Outputs: the segment's channels, or (when `last`)
1672/// the three 8-bit pixel values.
1673pub fn segment_graph(m: &Model, convs: &[&Conv], last: bool) -> (IntGraph, Vec<IId>) {
1674    let mut g = IntGraph::new();
1675    let c0 = convs[0];
1676    let nt = c0.lin.inp;
1677    let mut np = 0u16;
1678    let mut par = |g: &mut IntGraph| {
1679        let p = g.param(np, IType::I32);
1680        np += 1;
1681        p
1682    };
1683    let taps: Vec<IId> = (0..nt).map(|_| par(&mut g)).collect();
1684    let centre: Vec<IId> = if c0.residual {
1685        (0..c0.cin()).map(|_| par(&mut g)).collect()
1686    } else {
1687        vec![]
1688    };
1689    let mut a = {
1690        let zero = g.i32(0);
1691        let l = &c0.lin;
1692        (0..l.out)
1693            .map(|o| {
1694                let mut acc = g.i32(l.b[o]);
1695                for i in 0..l.inp {
1696                    let w = l.w[o * l.inp + i];
1697                    if w != 0 {
1698                        let wc = g.i32(w);
1699                        let t = g.mul(wc, taps[i]);
1700                        acc = g.add(acc, t);
1701                    }
1702                }
1703                let mut v = g_rshift(&mut g, acc, l.fw);
1704                if c0.residual {
1705                    v = g.add(v, centre[o]);
1706                }
1707                if c0.relu {
1708                    v = g.max(v, zero);
1709                }
1710                v
1711            })
1712            .collect::<Vec<_>>()
1713    };
1714    for c in &convs[1..] {
1715        assert_eq!(c.k, 1);
1716        a = g_layer(&mut g, &c.lin, &a, c.residual, c.relu);
1717    }
1718    if last {
1719        if let Some(s) = &m.stab {
1720            let si: Vec<IId> = (0..s.cin()).map(|_| par(&mut g)).collect();
1721            let sb = g_layer(&mut g, &s.lin, &si, false, false);
1722            a = a.iter().zip(sb).map(|(&p, q)| g.add(p, q)).collect();
1723        }
1724        a = g_layer(&mut g, &m.outt.lin, &a, m.outt.residual, m.outt.relu);
1725        let (k255, lo, hi) = (g.i32(255), g.i32(0), g.i32(255));
1726        a = a
1727            .iter()
1728            .map(|&v| {
1729                let t = g.mul(v, k255);
1730                let t = g_rshift(&mut g, t, QA);
1731                g.clamp(t, lo, hi)
1732            })
1733            .collect();
1734    }
1735    (g, a)
1736}
1737
1738/// Synthesis split into segments (each starts at a 3×3 conv or the input).
1739pub fn segments(m: &Model) -> Vec<Vec<&Conv>> {
1740    let mut segs: Vec<Vec<&Conv>> = vec![];
1741    for c in &m.syn {
1742        if c.k != 1 || segs.is_empty() {
1743            segs.push(vec![c]);
1744        } else {
1745            segs.last_mut().unwrap().push(c);
1746        }
1747    }
1748    segs
1749}
1750
1751#[cfg(test)]
1752pub(crate) mod tests {
1753    use super::*;
1754    use tang_expr::int::Program;
1755
1756    struct Rng(u64);
1757    impl Rng {
1758        fn next(&mut self) -> u64 {
1759            self.0 ^= self.0 << 13;
1760            self.0 ^= self.0 >> 7;
1761            self.0 ^= self.0 << 17;
1762            self.0
1763        }
1764        /// Quantized value k/64 with |k| ≤ a.
1765        fn q(&mut self, a: i64) -> f64 {
1766            ((self.next() % (2 * a as u64 + 1)) as i64 - a) as f64 / 64.0
1767        }
1768    }
1769
1770    fn put(t: &mut HashMap<String, (Vec<usize>, Vec<f64>)>, k: &str, sh: &[usize], v: Vec<f64>) {
1771        assert_eq!(sh.iter().product::<usize>(), v.len());
1772        t.insert(k.into(), (sh.to_vec(), v));
1773    }
1774
1775    /// A small Cool-chic-shaped model with random weights and latents.
1776    pub(crate) fn synth(seed: u64) -> Ccx {
1777        let mut r = Rng(seed);
1778        let mut t = HashMap::new();
1779        let sizes = [(16usize, 24usize), (8, 12), (4, 6), (4, 6)];
1780        put(&mut t, "img_size", &[2], vec![16.0, 24.0]);
1781        put(
1782            &mut t,
1783            "sizes",
1784            &[4, 2],
1785            sizes
1786                .iter()
1787                .flat_map(|&(a, b)| [a as f64, b as f64])
1788                .collect(),
1789        );
1790        put(&mut t, "is_hyper", &[4], vec![0.0, 0.0, 0.0, 1.0]);
1791        let ctx = [
1792            (0, -1),
1793            (-1, 0),
1794            (-1, -1),
1795            (-1, 1),
1796            (0, -2),
1797            (-2, 0),
1798            (-1, 2),
1799        ];
1800        put(
1801            &mut t,
1802            "ctx_index",
1803            &[ctx.len()],
1804            ctx.iter()
1805                .map(|&(dy, dx): &(i32, i32)| ((dy + 4) * 9 + dx + 4) as f64)
1806                .collect(),
1807        );
1808        put(&mut t, "mask_size", &[1], vec![9.0]);
1809        put(&mut t, "n_ifce_out", &[1], vec![2.0]);
1810        put(
1811            &mut t,
1812            "ifce.input_features",
1813            &[4],
1814            vec![3.0, 0.0, 0.0, 0.0],
1815        );
1816        put(
1817            &mut t,
1818            "ifce.index_to_arm",
1819            &[4],
1820            vec![0.0, -1.0, -1.0, -1.0],
1821        );
1822        let lin = |t: &mut HashMap<_, _>, r: &mut Rng, p: &str, o: usize, i: usize, a: i64| {
1823            put(
1824                t,
1825                &format!("{p}.w"),
1826                &[o, i],
1827                (0..o * i).map(|_| r.q(a)).collect(),
1828            );
1829            put(t, &format!("{p}.b"), &[o], (0..o).map(|_| r.q(a)).collect());
1830        };
1831        lin(&mut t, &mut r, "ifce.A0.L0", 2, 3, 40);
1832        let nin = ctx.len() + 2;
1833        lin(&mut t, &mut r, "arm.L0", nin, nin, 20);
1834        lin(&mut t, &mut r, "arm.L1", nin, nin, 20);
1835        lin(&mut t, &mut r, "arm.L2", 2, nin, 30);
1836        lin(&mut t, &mut r, "arm.stab", 2, nin, 10);
1837        for i in 0..2 {
1838            let a = [0.0351562, 0.1054687, -0.2617187, -0.8789063]
1839                .map(|v: f64| -((v * 64.0).round() / 64.0));
1840            put(
1841                &mut t,
1842                &format!("ups.tconv.{i}"),
1843                &[8],
1844                [a[0], a[1], a[2], a[3], a[3], a[2], a[1], a[0]].to_vec(),
1845            );
1846            let h: Vec<f64> = (0..4).map(|k| if k == 3 { 0.25 } else { r.q(4) }).collect();
1847            put(
1848                &mut t,
1849                &format!("ups.conv.{i}"),
1850                &[7],
1851                vec![h[0], h[1], h[2], h[3], h[2], h[1], h[0]],
1852            );
1853        }
1854        let conv = |t: &mut HashMap<_, _>,
1855                    r: &mut Rng,
1856                    p: &str,
1857                    o: usize,
1858                    i: usize,
1859                    k: usize,
1860                    res: bool,
1861                    relu: bool| {
1862            put(
1863                t,
1864                &format!("{p}.w"),
1865                &[o, i, k, k],
1866                (0..o * i * k * k).map(|_| r.q(30)).collect(),
1867            );
1868            put(
1869                t,
1870                &format!("{p}.b"),
1871                &[o],
1872                (0..o).map(|_| r.q(30)).collect(),
1873            );
1874            put(
1875                t,
1876                &format!("{p}.meta"),
1877                &[2],
1878                vec![res as u8 as f64, relu as u8 as f64],
1879            );
1880        };
1881        conv(&mut t, &mut r, "syn.L0", 8, 3, 1, false, true);
1882        conv(&mut t, &mut r, "syn.L1", 3, 8, 1, false, false);
1883        conv(&mut t, &mut r, "syn.L2", 3, 3, 3, true, true);
1884        conv(&mut t, &mut r, "syn.stab", 3, 3, 1, false, false);
1885        put(
1886            &mut t,
1887            "syn.out.w",
1888            &[3, 3, 1, 1],
1889            vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
1890        );
1891        put(&mut t, "syn.out.b", &[3], vec![0.5; 3]);
1892        put(&mut t, "syn.out.meta", &[2], vec![0.0, 0.0]);
1893        for (g, &(h, w)) in sizes.iter().enumerate() {
1894            // mostly small, a few escapes
1895            let v = (0..h * w)
1896                .map(|_| match r.next() % 40 {
1897                    0 => 40.0,
1898                    1 => -37.0,
1899                    k => (k % 5) as f64 - 2.0,
1900                })
1901                .collect();
1902            put(&mut t, &format!("latent.{g}"), &[h, w], v);
1903        }
1904        Ccx { t }
1905    }
1906
1907    #[test]
1908    fn tables_are_normalized() {
1909        let t = tables();
1910        for c in t.chunks(NSYM + 1) {
1911            assert_eq!(c[0], 0);
1912            assert_eq!(c[NSYM], PM);
1913            assert!(c.windows(2).all(|w| w[1] > w[0]));
1914        }
1915    }
1916
1917    #[test]
1918    fn roundtrip_layouts() {
1919        let c = synth(7);
1920        let n = 4;
1921        let truth: Vec<Vec<i32>> = (0..n)
1922            .map(|g| {
1923                c.ints(&format!("latent.{g}"))
1924                    .iter()
1925                    .map(|&v| v as i32)
1926                    .collect()
1927            })
1928            .collect();
1929        let mut first: Option<Vec<u8>> = None;
1930        for (lanes, tw, th) in [
1931            (1, 0, 0),
1932            (4, 0, 0),
1933            (16, 0, 0),
1934            (16, 8, 0),
1935            (16, 0, 4),
1936            (32, 8, 8),
1937        ] {
1938            let (bytes, st) = encode(&c, lanes, tw, th);
1939            assert_eq!(st.bytes, bytes.len());
1940            let p = parse(&bytes).unwrap();
1941            let mut gd = Guard::default();
1942            assert_eq!(
1943                decode_latents(&p, &mut gd),
1944                truth,
1945                "lanes {lanes} tile {tw}x{th}"
1946            );
1947            let (h, w, rgb) = decode(&bytes).unwrap();
1948            assert_eq!((h, w), (16, 24));
1949            // the image depends on the latents only, not on the layout
1950            match &first {
1951                None => first = Some(rgb),
1952                Some(f) => assert_eq!(f, &rgb),
1953            }
1954        }
1955    }
1956
1957    #[test]
1958    fn graphs_match_native() {
1959        let c = synth(11);
1960        let (bytes, _) = encode(&c, 4, 0, 0);
1961        let p = parse(&bytes).unwrap();
1962        let m = &p.m;
1963        let mut r = Rng(3);
1964        for mlp in std::iter::once(&m.arm).chain(&m.ifces) {
1965            let (g, o) = mlp.graph();
1966            let mut prog = Program::new(&g, &o);
1967            for _ in 0..200 {
1968                let x: Vec<i32> = (0..mlp.nin())
1969                    .map(|_| (r.next() % 2049) as i32 - 1024)
1970                    .collect();
1971                let mut want = vec![0i32; mlp.nout()];
1972                mlp.run(&x, &mut want, &mut Guard::default());
1973                let xin: Vec<u32> = x.iter().map(|&v| v as u32).collect();
1974                let mut got = vec![0u32; mlp.nout()];
1975                prog.run(&xin, &[], &mut got);
1976                assert_eq!(got.iter().map(|&v| v as i32).collect::<Vec<_>>(), want);
1977            }
1978        }
1979        // the last synthesis segment (taps + centre + stab inputs) against conv_px
1980        let segs = segments(m);
1981        assert!(!segs.is_empty());
1982    }
1983
1984    #[test]
1985    fn rejects_garbage() {
1986        assert!(parse(b"TCI1").is_err());
1987        let (mut bytes, _) = encode(&synth(5), 4, 0, 0);
1988        bytes.truncate(bytes.len() / 2);
1989        assert!(parse(&bytes).is_err());
1990    }
1991}