Skip to main content

cortiq_engine/
vae3d.rs

1//! MiniMax-H3's video VAE decoder: a ViT3D, not a conv stack.
2//!
3//! 36 transformer blocks over the latent grid, each latent cell one
4//! token, and a single linear that expands every token into a
5//! 4×16×16×3 block of pixels. The encoder half is a 3-D causal CNN and
6//! is not packed — text-to-video never runs it.
7//!
8//! ## Tiling is not an optimization here
9//!
10//! The reference decodes in 256-pixel spatial tiles and 17-frame
11//! temporal clips ALWAYS — `tiling=True` is the constructor default and
12//! `decode_tiled` just forwards to `decode`. Because the decoder is
13//! global attention, a tile sees a different context than the whole
14//! frame would, so the tiling is part of the model's output, not a
15//! memory strategy layered on top of it. Both schedules are reproduced
16//! exactly: `split_tiles` down to the last overlap unit, and the
17//! clip/token-drop bookkeeping that makes a chunk emit 17 frames and
18//! carry 5 over.
19
20use crate::dit::Proj;
21use crate::pool::Pool;
22use cortiq_core::CmfModel;
23use std::sync::Arc;
24
25const IMAGENET_MEAN: [f32; 3] = [0.485, 0.456, 0.406];
26const IMAGENET_STD: [f32; 3] = [0.229, 0.224, 0.225];
27
28/// Pixel tile edge and the smallest overlap `split_tiles` will accept.
29const TILE: usize = 256;
30const TILE_OVERLAP_MIN: usize = 64;
31/// Temporal clip in frames, and the tokens dropped off the encoder's
32/// tail that the decoder must therefore re-manufacture.
33const CLIP_LENGTH: usize = 17;
34const TOKEN_DROP: usize = 3;
35
36struct Block {
37    norm1: Vec<f32>,
38    norm2: Vec<f32>,
39    scale1: Vec<f32>,
40    scale2: Vec<f32>,
41    qkv: Proj, // [3·dim, dim], PER HEAD interleaved
42    qkv_b: Vec<f32>,
43    out: Proj,
44    out_b: Vec<f32>,
45    w1: Proj, // [2·4·dim, dim]
46    w1_b: Vec<f32>,
47    w2: Proj, // [dim, 4·dim]
48    w2_b: Vec<f32>,
49}
50
51pub struct VideoVae {
52    post_quant: Proj,
53    post_quant_b: Vec<f32>,
54    x_embed: Proj,
55    x_embed_b: Vec<f32>,
56    registers: Vec<f32>, // [n_reg, dim]
57    blocks: Vec<Block>,
58    norm_out_w: Vec<f32>,
59    norm_out_b: Vec<f32>,
60    proj_out: Proj,
61    proj_out_b: Vec<f32>,
62    latents_mean: Vec<f32>,
63    latents_std: Vec<f32>,
64    pool: Option<Arc<Pool>>,
65    dim: usize,
66    heads: usize,
67    head_dim: usize,
68    z_channels: usize,
69    patch: usize,
70    patch_t: usize,
71    n_reg: usize,
72    rope_theta: f32,
73    rope_dim: usize,
74    eps: f64,
75}
76
77fn layer_norm(x: &[f32], w: &[f32], b: &[f32], eps: f64, dst: &mut [f32]) {
78    let n = x.len() as f64;
79    let mean = x.iter().map(|&v| v as f64).sum::<f64>() / n;
80    let var = x.iter().map(|&v| (v as f64 - mean).powi(2)).sum::<f64>() / n;
81    let inv = 1.0 / (var + eps).sqrt();
82    for (((d, &v), &g), &bb) in dst.iter_mut().zip(x).zip(w).zip(b) {
83        *d = ((v as f64 - mean) * inv) as f32 * g + bb;
84    }
85}
86
87/// RMSNorm with a weight, no bias.
88fn rms_norm_into(x: &[f32], w: &[f32], eps: f64, dst: &mut [f32]) {
89    let ss = x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / x.len() as f64;
90    let inv = 1.0 / (ss + eps).sqrt();
91    for ((d, &v), &g) in dst.iter_mut().zip(x).zip(w) {
92        *d = (v as f64 * inv) as f32 * g;
93    }
94}
95
96/// RMSNorm with NO affine — the decoder's q/k norms are weightless.
97fn rms_norm_plain(x: &mut [f32], eps: f64) {
98    let ss = x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / x.len() as f64;
99    let inv = 1.0 / (ss + eps).sqrt();
100    for v in x.iter_mut() {
101        *v = (*v as f64 * inv) as f32;
102    }
103}
104
105fn silu(v: f32) -> f32 {
106    v / (1.0 + (-v).exp())
107}
108
109struct SendPtr(*mut f32);
110unsafe impl Send for SendPtr {}
111unsafe impl Sync for SendPtr {}
112impl SendPtr {
113    /// SAFETY: caller guarantees disjoint `[off, off+len)` per worker.
114    #[allow(clippy::mut_from_ref)]
115    unsafe fn row(&self, off: usize, len: usize) -> &mut [f32] {
116        unsafe { std::slice::from_raw_parts_mut(self.0.add(off), len) }
117    }
118}
119
120fn softmax_inplace(row: &mut [f32]) {
121    let mx = row.iter().cloned().fold(f32::MIN, f32::max);
122    let mut den = 0f32;
123    for r in row.iter_mut() {
124        *r = (*r - mx).exp();
125        den += *r;
126    }
127    if den > 0.0 {
128        let inv = 1.0 / den;
129        for r in row.iter_mut() {
130            *r *= inv;
131        }
132    }
133}
134
135/// `(starts, lens, overlaps)` for one axis — the reference's schedule,
136/// which grows the overlaps rather than the tile count so every tile is
137/// exactly `TILE` wide.
138fn split_tiles(input_len: usize, ratio: usize) -> (Vec<usize>, Vec<usize>, Vec<usize>) {
139    if TILE >= input_len {
140        return (vec![0], vec![input_len], Vec::new());
141    }
142    let mut n = input_len.div_ceil(TILE);
143    let (mut overlaps, mut remaining);
144    loop {
145        overlaps = vec![TILE_OVERLAP_MIN; n - 1];
146        let total: usize = overlaps.iter().sum();
147        if TILE * n < total + input_len {
148            n += 1;
149            continue;
150        }
151        remaining = TILE * n - total - input_len;
152        break;
153    }
154    for i in 0..remaining / ratio {
155        overlaps[i % (n - 1)] += ratio;
156    }
157    let mut starts = vec![0usize];
158    for i in 0..n - 1 {
159        starts.push(starts[i] + TILE - overlaps[i]);
160    }
161    (starts, vec![TILE; n], overlaps)
162}
163
164impl VideoVae {
165    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<Self, String> {
166        let cfg: serde_json::Value = serde_json::from_slice(
167            model.tensor_bytes("vvae.config_json").map_err(|e| e.to_string())?,
168        )
169        .map_err(|e| format!("vvae.config_json: {e}"))?;
170        let u = |k: &str, d: usize| cfg[k].as_u64().map(|v| v as usize).unwrap_or(d);
171        let f32v = |n: &str| crate::dit::cmf_f32(model, n);
172        let n = u("num_layers", 36);
173        let mut blocks = Vec::with_capacity(n);
174        for l in 0..n {
175            let p = format!("vvae.blocks.{l}");
176            blocks.push(Block {
177                norm1: f32v(&format!("{p}.norm1"))?,
178                norm2: f32v(&format!("{p}.norm2"))?,
179                scale1: f32v(&format!("{p}.scale1"))?,
180                scale2: f32v(&format!("{p}.scale2"))?,
181                qkv: Proj::from_model(model, &format!("{p}.attn.to_qkv.weight"))?,
182                qkv_b: f32v(&format!("{p}.attn.to_qkv.bias"))?,
183                out: Proj::from_model(model, &format!("{p}.attn.to_out.weight"))?,
184                out_b: f32v(&format!("{p}.attn.to_out.bias"))?,
185                w1: Proj::from_model(model, &format!("{p}.ff.w1.weight"))?,
186                w1_b: f32v(&format!("{p}.ff.w1.bias"))?,
187                w2: Proj::from_model(model, &format!("{p}.ff.w2.weight"))?,
188                w2_b: f32v(&format!("{p}.ff.w2.bias"))?,
189            });
190        }
191        let dim = u("dim", 2048);
192        let heads = u("heads", 32);
193        Ok(Self {
194            post_quant: Proj::from_model(model, "vvae.post_quant_conv.weight")?,
195            post_quant_b: f32v("vvae.post_quant_conv.bias")?,
196            x_embed: Proj::from_model(model, "vvae.x_embedder.weight")?,
197            x_embed_b: f32v("vvae.x_embedder.bias")?,
198            registers: f32v("vvae.register_tokens")?,
199            blocks,
200            norm_out_w: f32v("vvae.norm_out.weight")?,
201            norm_out_b: f32v("vvae.norm_out.bias")?,
202            proj_out: Proj::from_model(model, "vvae.proj_out.weight")?,
203            proj_out_b: f32v("vvae.proj_out.bias")?,
204            latents_mean: f32v("vvae.latents_mean")?,
205            latents_std: f32v("vvae.latents_std")?,
206            pool: Pool::from_env(),
207            dim,
208            heads,
209            head_dim: dim / heads,
210            z_channels: u("z_channels", 24),
211            patch: u("patch_size", 16),
212            patch_t: u("patch_size_t", 4),
213            n_reg: u("num_register_tokens", 4),
214            rope_theta: cfg["rope_theta"].as_f64().unwrap_or(100.0) as f32,
215            rope_dim: ((dim / heads) as f64 * cfg["rope_dim_ratio"].as_f64().unwrap_or(0.75))
216                as usize,
217            eps: cfg["eps"].as_f64().unwrap_or(1e-5),
218        })
219    }
220
221    /// `arange(0.5, n)/n · 2 − 1` — the normalized cell centre of one
222    /// axis, so a tile's coordinates depend on the TILE's extent and
223    /// not on the frame's.
224    fn axis(n: usize) -> Vec<f32> {
225        (0..n)
226            .map(|i| 2.0 * ((i as f32 + 0.5) / n as f32) - 1.0)
227            .collect()
228    }
229
230    /// `[S, rope_dim/2]` angles: three axes × `rope_dim/6` frequencies,
231    /// scaled by 2π. Suffix tokens sit at the origin and get zeros.
232    fn rope_angles(&self, t: usize, h: usize, w: usize, suffix: usize) -> Vec<f32> {
233        let k = self.rope_dim / 6; // frequencies per axis
234        let inv: Vec<f32> = (0..k)
235            .map(|i| 1.0 / self.rope_theta.powf(i as f32 * 2.0 * 3.0 / self.rope_dim as f32))
236            .collect();
237        let (ta, ha, wa) = (Self::axis(t), Self::axis(h), Self::axis(w));
238        let tau = 2.0 * std::f32::consts::PI;
239        let mut out = Vec::with_capacity((t * h * w + suffix) * 3 * k);
240        for ti in 0..t {
241            for hi in 0..h {
242                for wi in 0..w {
243                    for &c in &[ta[ti], ha[hi], wa[wi]] {
244                        for &f in &inv {
245                            out.push(tau * c * f);
246                        }
247                    }
248                }
249            }
250        }
251        out.extend(std::iter::repeat_n(0.0, suffix * 3 * k));
252        out
253    }
254
255    fn attention(&self, qkv: &[f32], n: usize, attn: &mut [f32], angles: &[f32]) {
256        let (nh, hd, dim) = (self.heads, self.head_dim, self.dim);
257        let pairs = angles.len() / n; // = rope_dim / 2
258        let scale = 1.0 / (hd as f32).sqrt();
259        let pool = self.pool.as_deref();
260        let mut qh = vec![0f32; n * hd];
261        let mut kh = vec![0f32; n * hd];
262        let mut vt = vec![0f32; hd * n];
263        let mut scores = vec![0f32; n * n];
264        let mut oh = vec![0f32; n * hd];
265        for h in 0..nh {
266            for p in 0..n {
267                // to_qkv is viewed [.., heads, 3·hd] then chunked, so a
268                // head's q, k and v are adjacent — not three planes.
269                let base = p * 3 * dim + h * 3 * hd;
270                qh[p * hd..(p + 1) * hd].copy_from_slice(&qkv[base..base + hd]);
271                kh[p * hd..(p + 1) * hd].copy_from_slice(&qkv[base + hd..base + 2 * hd]);
272                for d in 0..hd {
273                    vt[d * n + p] = qkv[base + 2 * hd + d];
274                }
275            }
276            for (buf, _) in [(&mut qh, 0), (&mut kh, 1)] {
277                for p in 0..n {
278                    let x = &mut buf[p * hd..(p + 1) * hd];
279                    rms_norm_plain(x, self.eps);
280                    for j in 0..pairs {
281                        let (s, c) = angles[p * pairs + j].sin_cos();
282                        let (a, b) = (x[j], x[j + pairs]);
283                        x[j] = a * c - b * s;
284                        x[j + pairs] = a * s + b * c;
285                    }
286                }
287            }
288            for v in qh.iter_mut() {
289                *v *= scale;
290            }
291            crate::fcd_ops::gemm_nt(&qh, &kh, &mut scores, n, hd, n, pool);
292            let sp = SendPtr(scores.as_mut_ptr());
293            let soft = |lo: usize, hi: usize| {
294                for r in lo..hi {
295                    // SAFETY: workers own disjoint score rows.
296                    softmax_inplace(unsafe { sp.row(r * n, n) });
297                }
298            };
299            match pool {
300                Some(pl) => pl.run_rows(n, &soft),
301                None => soft(0, n),
302            }
303            crate::fcd_ops::gemm_nt(&scores, &vt, &mut oh, n, n, hd, pool);
304            for p in 0..n {
305                attn[p * dim + h * hd..p * dim + (h + 1) * hd]
306                    .copy_from_slice(&oh[p * hd..(p + 1) * hd]);
307            }
308        }
309    }
310
311    /// One tile-clip: latents `[z_channels, t, h, w]` → pixels
312    /// `[3, t·4, h·16, w·16]`, both channel-major.
313    fn decode_tile(&self, z: &[f32], t: usize, h: usize, w: usize) -> Vec<f32> {
314        let pool = self.pool.as_deref();
315        let dim = self.dim;
316        let np = t * h * w;
317        let suffix = 1 + self.n_reg;
318        let n = np + suffix;
319
320        // [C, T, H, W] → [T·H·W, C], then the 1×1×1 post-quant conv and
321        // the embedder, which are both plain linears at this point.
322        let zc = self.z_channels;
323        let mut rows = vec![0f32; np * zc];
324        for c in 0..zc {
325            for i in 0..np {
326                rows[i * zc + c] = z[c * np + i];
327            }
328        }
329        let mut pq = vec![0f32; np * zc];
330        self.post_quant.matmat(&rows, np, &mut pq, pool);
331        for r in pq.chunks_exact_mut(zc) {
332            for (v, &b) in r.iter_mut().zip(&self.post_quant_b) {
333                *v += b;
334            }
335        }
336        let mut x = vec![0f32; n * dim];
337        self.x_embed.matmat(&pq, np, &mut x[..np * dim], pool);
338        for r in x[..np * dim].chunks_exact_mut(dim) {
339            for (v, &b) in r.iter_mut().zip(&self.x_embed_b) {
340                *v += b;
341            }
342        }
343        // register tokens, then one all-zero token
344        x[np * dim..(np + self.n_reg) * dim].copy_from_slice(&self.registers);
345
346        let angles = self.rope_angles(t, h, w, suffix);
347        let mut xn = vec![0f32; n * dim];
348        let mut qkv = vec![0f32; n * 3 * dim];
349        let mut attn = vec![0f32; n * dim];
350        let mut proj = vec![0f32; n * dim];
351        let inner = 4 * dim;
352        for blk in &self.blocks {
353            for (o, src) in xn.chunks_exact_mut(dim).zip(x.chunks_exact(dim)) {
354                rms_norm_into(src, &blk.norm1, self.eps, o);
355            }
356            blk.qkv.matmat(&xn, n, &mut qkv, pool);
357            for r in qkv.chunks_exact_mut(3 * dim) {
358                for (v, &b) in r.iter_mut().zip(&blk.qkv_b) {
359                    *v += b;
360                }
361            }
362            self.attention(&qkv, n, &mut attn, &angles);
363            blk.out.matmat(&attn, n, &mut proj, pool);
364            for (p, r) in proj.chunks_exact_mut(dim).enumerate() {
365                for (i, v) in r.iter_mut().enumerate() {
366                    *v += blk.out_b[i];
367                    x[p * dim + i] += *v * blk.scale1[i];
368                }
369            }
370            for (o, src) in xn.chunks_exact_mut(dim).zip(x.chunks_exact(dim)) {
371                rms_norm_into(src, &blk.norm2, self.eps, o);
372            }
373            let mut gu = vec![0f32; n * 2 * inner];
374            blk.w1.matmat(&xn, n, &mut gu, pool);
375            let mut act = vec![0f32; n * inner];
376            for p in 0..n {
377                let r = &gu[p * 2 * inner..(p + 1) * 2 * inner];
378                for i in 0..inner {
379                    act[p * inner + i] =
380                        silu(r[i] + blk.w1_b[i]) * (r[inner + i] + blk.w1_b[inner + i]);
381                }
382            }
383            blk.w2.matmat(&act, n, &mut proj, pool);
384            for (p, r) in proj.chunks_exact_mut(dim).enumerate() {
385                for (i, v) in r.iter_mut().enumerate() {
386                    *v += blk.w2_b[i];
387                    x[p * dim + i] += *v * blk.scale2[i];
388                }
389            }
390        }
391
392        let (pt, ps) = (self.patch_t, self.patch);
393        let od = 3 * pt * ps * ps;
394        let mut head = vec![0f32; np * dim];
395        for (o, src) in head.chunks_exact_mut(dim).zip(x[..np * dim].chunks_exact(dim)) {
396            layer_norm(src, &self.norm_out_w, &self.norm_out_b, self.eps, o);
397        }
398        let mut out = vec![0f32; np * od];
399        self.proj_out.matmat(&head, np, &mut out, pool);
400        for r in out.chunks_exact_mut(od) {
401            for (v, &b) in r.iter_mut().zip(&self.proj_out_b) {
402                *v += b;
403            }
404        }
405
406        // [T,H,W, 3,pt,ps,ps] → [3, T·pt, H·ps, W·ps]
407        let (ot, oh, ow) = (t * pt, h * ps, w * ps);
408        let mut px = vec![0f32; 3 * ot * oh * ow];
409        for ti in 0..t {
410            for hi in 0..h {
411                for wi in 0..w {
412                    let src = &out[((ti * h + hi) * w + wi) * od..((ti * h + hi) * w + wi + 1) * od];
413                    for c in 0..3 {
414                        for a in 0..pt {
415                            for b in 0..ps {
416                                for d in 0..ps {
417                                    px[((c * ot + ti * pt + a) * oh + hi * ps + b) * ow
418                                        + wi * ps + d] =
419                                        src[((c * pt + a) * ps + b) * ps + d];
420                                }
421                            }
422                        }
423                    }
424                }
425            }
426        }
427        px
428    }
429
430    /// Spatially tiled decode of one temporal clip. `z` is
431    /// `[z_channels, t, zh, zw]`; the result is `[3, t·4, zh·16, zw·16]`.
432    fn decode_clip(&self, z: &[f32], t: usize, zh: usize, zw: usize) -> Vec<f32> {
433        let r = self.patch;
434        let (height, width) = (zh * r, zw * r);
435        let (ys, yl, yo) = split_tiles(height, r);
436        let (xs, xl, xo) = split_tiles(width, r);
437        let frames = t * self.patch_t;
438        let mut canvas = vec![0f32; 3 * frames * height * width];
439
440        // The reference blends each tile into its predecessor's tail and
441        // then trims the overlap off, so a pixel is written once.
442        let mut row_tails: Vec<Vec<f32>> = Vec::new();
443        let mut out_y = 0usize;
444        for (i, (&ip, &il)) in ys.iter().zip(&yl).enumerate() {
445            let (zi, zl) = (ip / r, il / r);
446            let mut new_tails: Vec<Vec<f32>> = Vec::new();
447            let mut left_tail: Option<Vec<f32>> = None;
448            let mut out_x = 0usize;
449            let mut row_h = 0usize;
450            for (j, (&jp, &jl)) in xs.iter().zip(&xl).enumerate() {
451                let (zj, zw_t) = (jp / r, jl / r);
452                let mut sub = vec![0f32; self.z_channels * t * zl * zw_t];
453                for c in 0..self.z_channels {
454                    for ti in 0..t {
455                        for hh in 0..zl {
456                            for ww in 0..zw_t {
457                                sub[((c * t + ti) * zl + hh) * zw_t + ww] =
458                                    z[((c * t + ti) * zh + zi + hh) * zw + zj + ww];
459                            }
460                        }
461                    }
462                }
463                let mut tile = self.decode_tile(&sub, t, zl, zw_t);
464                let (mut th, mut tw) = (zl * r, zw_t * r);
465                if i + 1 < ys.len() {
466                    new_tails.push(crop(&tile, frames, th, tw, th - yo[i], th, 0, tw));
467                }
468                let next_left = if j + 1 < xs.len() {
469                    Some(crop(&tile, frames, th, tw, 0, th, tw - xo[j], tw))
470                } else {
471                    None
472                };
473                if i > 0 {
474                    tile = blend(&row_tails[j], &tile, frames, th, tw, yo[i - 1], 2);
475                }
476                if j > 0 {
477                    let lt = left_tail.as_ref().unwrap();
478                    tile = blend(lt, &tile, frames, th, tw, xo[j - 1], 3);
479                }
480                left_tail = next_left;
481                if i + 1 < ys.len() {
482                    tile = crop(&tile, frames, th, tw, 0, th - yo[i], 0, tw);
483                    th -= yo[i];
484                }
485                if j + 1 < xs.len() {
486                    tile = crop(&tile, frames, th, tw, 0, th, 0, tw - xo[j]);
487                    tw -= xo[j];
488                }
489                for c in 0..3 {
490                    for f in 0..frames {
491                        for hh in 0..th {
492                            let dst = ((c * frames + f) * height + out_y + hh) * width + out_x;
493                            let src = ((c * frames + f) * th + hh) * tw;
494                            canvas[dst..dst + tw].copy_from_slice(&tile[src..src + tw]);
495                        }
496                    }
497                }
498                out_x += tw;
499                row_h = th;
500            }
501            row_tails = new_tails;
502            out_y += row_h;
503        }
504        canvas
505    }
506
507    /// Normalized latents `[z_channels, t_lat, zh, zw]` → RGB in [0, 1],
508    /// `[3, frames, zh·16, zw·16]`.
509    pub fn decode(&self, z: &[f32], t_lat: usize, zh: usize, zw: usize) -> (Vec<f32>, usize) {
510        let zc = self.z_channels;
511        let np = t_lat * zh * zw;
512        let mut zz = vec![0f32; zc * np];
513        for c in 0..zc {
514            let (m, s) = (self.latents_mean[c], self.latents_std[c]);
515            for i in 0..np {
516                zz[c * np + i] = z[c * np + i] * s + m;
517            }
518        }
519
520        let ratio_t = self.patch_t;
521        let chunk_tokens = CLIP_LENGTH.div_ceil(ratio_t); // 5
522        let token_overlap = (chunk_tokens - TOKEN_DROP % chunk_tokens) % chunk_tokens; // 2
523        let frame_pre_pad = (ratio_t - CLIP_LENGTH % ratio_t) % ratio_t; // 3
524        let frame_overlap = (token_overlap * ratio_t).saturating_sub(frame_pre_pad); // 5
525        let chunk_dec = chunk_tokens * ratio_t; // 20
526
527        // The encoder dropped TOKEN_DROP tokens off the tail, so the
528        // decoder plans against a longer pseudo-sequence and pads the
529        // real one out with a repeat of its last token.
530        let mut pseudo = t_lat + TOKEN_DROP;
531        let mut pad = 0usize;
532        if pseudo % chunk_tokens != 0 {
533            pad = chunk_tokens - pseudo % chunk_tokens;
534            pseudo += pad;
535        }
536        let mut chunks = pseudo / chunk_tokens - usize::from(TOKEN_DROP > 0);
537        if chunks < 1 {
538            pad += chunk_tokens;
539            chunks += 1;
540        }
541        let t_pad = t_lat + pad;
542        if pad > 0 {
543            let mut grown = vec![0f32; zc * t_pad * zh * zw];
544            for c in 0..zc {
545                for ti in 0..t_pad {
546                    let src = ti.min(t_lat - 1);
547                    let a = (c * t_lat + src) * zh * zw;
548                    let b = (c * t_pad + ti) * zh * zw;
549                    grown[b..b + zh * zw].copy_from_slice(&zz[a..a + zh * zw]);
550                }
551            }
552            zz = grown;
553        }
554
555        let (h, w) = (zh * self.patch, zw * self.patch);
556        let mut out: Vec<f32> = Vec::new();
557        let mut carry: Option<(Vec<f32>, usize)> = None;
558        for i in 0..chunks {
559            let a = i * chunk_tokens;
560            let b = (a + chunk_tokens + token_overlap).min(t_pad);
561            let n = b.saturating_sub(a.min(t_pad));
562            if n == 0 {
563                continue;
564            }
565            let mut sub = vec![0f32; zc * n * zh * zw];
566            for c in 0..zc {
567                let src = (c * t_pad + a) * zh * zw;
568                let dst = c * n * zh * zw;
569                sub[dst..dst + n * zh * zw].copy_from_slice(&zz[src..src + n * zh * zw]);
570            }
571            let dec = self.decode_clip(&sub, n, zh, zw);
572            let dec_frames = n * ratio_t;
573            for j in 0..2 {
574                let fa = j * chunk_dec;
575                let fb = (fa + chunk_dec).min(dec_frames);
576                if fb <= fa + frame_pre_pad {
577                    continue;
578                }
579                let mut part = frames_of(&dec, dec_frames, h, w, fa + frame_pre_pad, fb);
580                let mut pn = fb - fa - frame_pre_pad;
581                if j == 0 {
582                    if let Some((tail, tn)) = carry.take() {
583                        part = blend_frames(&tail, tn, &part, pn, h, w, frame_overlap);
584                        pn = part.len() / (3 * h * w);
585                    }
586                    append_frames(&mut out, &part, pn, h, w);
587                } else {
588                    carry = Some((part, pn));
589                }
590            }
591            if i + 1 == chunks {
592                if let Some((tail, tn)) = carry.take() {
593                    append_frames(&mut out, &tail, tn, h, w);
594                }
595            }
596        }
597
598        // Undo the ImageNet pixel normalization and clamp; the reference
599        // then maps to [-1, 1] and every caller maps straight back, so
600        // stop at [0, 1].
601        let frames = out.len() / (3 * h * w);
602        let want = t_lat * ratio_t - pad_frames(t_lat, pad, chunk_tokens, ratio_t);
603        for c in 0..3 {
604            let base = c * frames * h * w;
605            for v in out[base..base + frames * h * w].iter_mut() {
606                *v = (*v * IMAGENET_STD[c] + IMAGENET_MEAN[c]).clamp(0.0, 1.0);
607            }
608        }
609        let keep = want.min(frames);
610        if keep < frames {
611            let mut trimmed = vec![0f32; 3 * keep * h * w];
612            for c in 0..3 {
613                let src = c * frames * h * w;
614                let dst = c * keep * h * w;
615                trimmed[dst..dst + keep * h * w].copy_from_slice(&out[src..src + keep * h * w]);
616            }
617            out = trimmed;
618        }
619        (out, keep)
620    }
621
622    pub fn spatial_ratio(&self) -> usize {
623        self.patch
624    }
625    pub fn temporal_ratio(&self) -> usize {
626        self.patch_t
627    }
628}
629
630/// Frames the padding tokens manufactured, which the reference trims.
631fn pad_frames(t_lat: usize, pad: usize, chunk_tokens: usize, ratio_t: usize) -> usize {
632    if pad == 0 {
633        return 0;
634    }
635    let intra = CLIP_LENGTH % ratio_t;
636    if intra == 0 {
637        return pad * ratio_t;
638    }
639    (0..pad)
640        .map(|k| {
641            if (t_lat + k) % chunk_tokens == 0 {
642                intra
643            } else {
644                ratio_t
645            }
646        })
647        .sum()
648}
649
650/// `[3, f, h, w]` sub-rectangle.
651#[allow(clippy::too_many_arguments)]
652fn crop(x: &[f32], f: usize, h: usize, w: usize, y0: usize, y1: usize, x0: usize, x1: usize) -> Vec<f32> {
653    let (nh, nw) = (y1 - y0, x1 - x0);
654    let mut out = vec![0f32; 3 * f * nh * nw];
655    for c in 0..3 {
656        for fi in 0..f {
657            for yy in 0..nh {
658                let s = ((c * f + fi) * h + y0 + yy) * w + x0;
659                let d = ((c * f + fi) * nh + yy) * nw;
660                out[d..d + nw].copy_from_slice(&x[s..s + nw]);
661            }
662        }
663    }
664    out
665}
666
667/// Linear cross-fade of `a`'s tail into `b`'s head along `dim` (2 = y,
668/// 3 = x); the result is `b` with its first `extent` rows/columns
669/// replaced by the blend.
670#[allow(clippy::too_many_arguments)]
671fn blend(a: &[f32], b: &[f32], f: usize, h: usize, w: usize, extent: usize, dim: usize) -> Vec<f32> {
672    let ah = a.len() / (3 * f * w);
673    let aw = a.len() / (3 * f * h);
674    let mut out = b.to_vec();
675    let e = if dim == 2 {
676        extent.min(ah).min(h)
677    } else {
678        extent.min(aw).min(w)
679    };
680    for c in 0..3 {
681        for fi in 0..f {
682            for k in 0..e {
683                let wb = k as f32 / e as f32;
684                let wa = 1.0 - wb;
685                if dim == 2 {
686                    let sa = ((c * f + fi) * ah + ah - e + k) * w;
687                    let sb = ((c * f + fi) * h + k) * w;
688                    for x in 0..w {
689                        out[sb + x] = a[sa + x] * wa + b[sb + x] * wb;
690                    }
691                } else {
692                    for y in 0..h {
693                        let sa = ((c * f + fi) * h + y) * aw + aw - e + k;
694                        let sb = ((c * f + fi) * h + y) * w + k;
695                        out[sb] = a[sa] * wa + b[sb] * wb;
696                    }
697                }
698            }
699        }
700    }
701    out
702}
703
704/// Frames `[a, b)` of a `[3, f, h, w]` clip.
705fn frames_of(x: &[f32], f: usize, h: usize, w: usize, a: usize, b: usize) -> Vec<f32> {
706    let n = b - a;
707    let mut out = vec![0f32; 3 * n * h * w];
708    for c in 0..3 {
709        let s = (c * f + a) * h * w;
710        let d = c * n * h * w;
711        out[d..d + n * h * w].copy_from_slice(&x[s..s + n * h * w]);
712    }
713    out
714}
715
716/// Cross-fade `tail` into the head of `part` along the frame axis.
717#[allow(clippy::too_many_arguments)]
718fn blend_frames(
719    tail: &[f32],
720    tn: usize,
721    part: &[f32],
722    pn: usize,
723    h: usize,
724    w: usize,
725    extent: usize,
726) -> Vec<f32> {
727    let e = extent.min(tn).min(pn);
728    let mut out = part.to_vec();
729    for c in 0..3 {
730        for k in 0..e {
731            let wb = k as f32 / e as f32;
732            let wa = 1.0 - wb;
733            let s = ((c * tn) + tn - e + k) * h * w;
734            let d = ((c * pn) + k) * h * w;
735            for i in 0..h * w {
736                out[d + i] = tail[s + i] * wa + part[d + i] * wb;
737            }
738        }
739    }
740    out
741}
742
743/// Append a `[3, n, h, w]` clip to a growing `[3, ?, h, w]` buffer.
744fn append_frames(out: &mut Vec<f32>, part: &[f32], n: usize, h: usize, w: usize) {
745    let old = out.len() / (3 * h * w);
746    let total = old + n;
747    let mut grown = vec![0f32; 3 * total * h * w];
748    for c in 0..3 {
749        if old > 0 {
750            let s = c * old * h * w;
751            let d = c * total * h * w;
752            grown[d..d + old * h * w].copy_from_slice(&out[s..s + old * h * w]);
753        }
754        let s = c * n * h * w;
755        let d = (c * total + old) * h * w;
756        grown[d..d + n * h * w].copy_from_slice(&part[s..s + n * h * w]);
757    }
758    *out = grown;
759}
760
761#[cfg(test)]
762mod tests {
763    use super::*;
764
765    #[test]
766    fn tile_schedule_matches_the_reference() {
767        // 288 tall: two 256 tiles, the overlap grown to swallow the slack.
768        let (s, l, o) = split_tiles(288, 16);
769        assert_eq!(s, vec![0, 32]);
770        assert_eq!(l, vec![256, 256]);
771        assert_eq!(o, vec![224]);
772        // 512 wide does not fit in two tiles at the minimum overlap.
773        let (s, l, o) = split_tiles(512, 16);
774        assert_eq!(s, vec![0, 128, 256]);
775        assert_eq!(l, vec![256; 3]);
776        assert_eq!(o, vec![128, 128]);
777        // Anything at or under one tile is one tile.
778        assert_eq!(split_tiles(256, 16).0, vec![0]);
779        assert_eq!(split_tiles(128, 16).1, vec![128]);
780    }
781
782    #[test]
783    fn temporal_constants_come_out_as_the_reference_computes_them() {
784        let ratio_t = 4usize;
785        let chunk = CLIP_LENGTH.div_ceil(ratio_t);
786        assert_eq!(chunk, 5);
787        assert_eq!((chunk - TOKEN_DROP % chunk) % chunk, 2);
788        assert_eq!((ratio_t - CLIP_LENGTH % ratio_t) % ratio_t, 3);
789    }
790}
791
792// ── the encoder, for keyframes only ─────────────────────────────────
793//
794// `fl2va` conditions on a first and/or last frame, and a frame is ONE
795// frame. That collapses the whole 3-D causal encoder to a 2-D one:
796// causal padding fills the front with zeros, and the reference's own
797// `autopad="causal_zero"` therefore trims the kernel to
798// `weight[:, :, -T:]` — at T = 1, the last temporal tap and nothing
799// else. So the packer stores that tap alone (a third of the bytes) and
800// this runs plain 2-D convolutions over it. Encoding real video would
801// need the other two taps back; keyframes never reach them.
802
803/// A 2-D convolution with the reference's padding policy: reflect on
804/// H and W, applied by hand because the checkpoint's convolutions
805/// carry no padding of their own.
806struct EncConv {
807    w: Vec<f32>, // [out, in, kh, kw]
808    b: Vec<f32>,
809    out_ch: usize,
810    in_ch: usize,
811    k: usize,
812    stride: usize,
813    pad: usize,
814}
815
816impl EncConv {
817    fn load(model: &Arc<CmfModel>, name: &str, stride: usize, pad: usize) -> Result<Self, String> {
818        let e = model
819            .tensor(&format!("{name}.weight"))
820            .ok_or_else(|| format!("missing {name}.weight"))?;
821        // [out, in, kh, kw] — the packer already dropped the temporal axis.
822        let (out_ch, in_ch, k) = (e.shape[0], e.shape[1], e.shape[2]);
823        Ok(Self {
824            w: crate::dit::cmf_f32(model, &format!("{name}.weight"))?,
825            b: crate::dit::cmf_f32(model, &format!("{name}.bias"))?,
826            out_ch,
827            in_ch,
828            k,
829            stride,
830            pad,
831        })
832    }
833
834    /// `x` is `[in_ch, h, w]`; reflect-padded by `self.pad` on each side.
835    fn apply(&self, x: &[f32], h: usize, w: usize, pool: Option<&Pool>) -> (Vec<f32>, usize, usize) {
836        let (ph, pw) = (h + 2 * self.pad, w + 2 * self.pad);
837        let oh = (ph - self.k) / self.stride + 1;
838        let ow = (pw - self.k) / self.stride + 1;
839        // Reflect padding: index |p| mirrored about the edges, which for
840        // pad 1 on a plane of at least 2 is just the neighbour row.
841        let refl = |i: isize, n: usize| -> usize {
842            let n = n as isize;
843            let mut i = i;
844            while i < 0 || i >= n {
845                if i < 0 {
846                    i = -i;
847                }
848                if i >= n {
849                    i = 2 * (n - 1) - i;
850                }
851            }
852            i as usize
853        };
854        let mut out = vec![0f32; self.out_ch * oh * ow];
855        let ptr = SendPtr(out.as_mut_ptr());
856        let work = |lo: usize, hi: usize| {
857            for o in lo..hi {
858                // SAFETY: workers own disjoint output channels.
859                let dst = unsafe { ptr.row(o * oh * ow, oh * ow) };
860                dst.fill(self.b[o]);
861                for i in 0..self.in_ch {
862                    let ker = &self.w[(o * self.in_ch + i) * self.k * self.k
863                        ..(o * self.in_ch + i + 1) * self.k * self.k];
864                    let src = &x[i * h * w..(i + 1) * h * w];
865                    for oy in 0..oh {
866                        for ox in 0..ow {
867                            let mut acc = 0f32;
868                            for ky in 0..self.k {
869                                let sy = (oy * self.stride + ky) as isize - self.pad as isize;
870                                let sy = refl(sy, h);
871                                for kx in 0..self.k {
872                                    let sx = (ox * self.stride + kx) as isize - self.pad as isize;
873                                    acc += ker[ky * self.k + kx] * src[sy * w + refl(sx, w)];
874                                }
875                            }
876                            dst[oy * ow + ox] += acc;
877                        }
878                    }
879                }
880            }
881        };
882        match pool {
883            Some(p) => p.run_rows(self.out_ch, &work),
884            None => work(0, self.out_ch),
885        }
886        (out, oh, ow)
887    }
888}
889
890/// GroupNorm(32) with affine parameters, over `[ch, h, w]`.
891struct GroupNorm {
892    w: Vec<f32>,
893    b: Vec<f32>,
894}
895
896impl GroupNorm {
897    fn load(model: &Arc<CmfModel>, name: &str) -> Result<Self, String> {
898        Ok(Self {
899            w: crate::dit::cmf_f32(model, &format!("{name}.weight"))?,
900            b: crate::dit::cmf_f32(model, &format!("{name}.bias"))?,
901        })
902    }
903
904    fn apply(&self, x: &mut [f32], ch: usize, hw: usize) {
905        let groups = 32;
906        let per = ch / groups;
907        for g in 0..groups {
908            let seg = &mut x[g * per * hw..(g + 1) * per * hw];
909            let n = seg.len() as f64;
910            let mean = seg.iter().map(|&v| v as f64).sum::<f64>() / n;
911            let var = seg.iter().map(|&v| (v as f64 - mean).powi(2)).sum::<f64>() / n;
912            let inv = 1.0 / (var + 1e-6).sqrt();
913            for (i, v) in seg.iter_mut().enumerate() {
914                let c = g * per + i / hw;
915                *v = ((*v as f64 - mean) * inv) as f32 * self.w[c] + self.b[c];
916            }
917        }
918    }
919}
920
921struct ResBlock {
922    norm1: GroupNorm,
923    norm2: GroupNorm,
924    conv1: EncConv,
925    conv2: EncConv,
926    shortcut: Option<EncConv>,
927}
928
929/// The encoder half: `[3, h, w]` in [-1, 1] → normalized latents
930/// `[24, h/16, w/16]`.
931pub struct VideoVaeEncoder {
932    conv_in: EncConv,
933    levels: Vec<(Vec<ResBlock>, Option<EncConv>)>,
934    norm_out: GroupNorm,
935    conv_out: EncConv,
936    quant: Vec<f32>, // [48, 48] 1x1x1
937    quant_b: Vec<f32>,
938    latents_mean: Vec<f32>,
939    latents_std: Vec<f32>,
940    pool: Option<Arc<Pool>>,
941    z_channels: usize,
942    ratio: usize,
943}
944
945impl VideoVaeEncoder {
946    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<Self, String> {
947        let cfg: serde_json::Value = serde_json::from_slice(
948            model
949                .tensor_bytes("vvae.config_json")
950                .map_err(|e| e.to_string())?,
951        )
952        .map_err(|e| format!("vvae.config_json: {e}"))?;
953        let space_down: Vec<usize> = cfg["space_down"]
954            .as_array()
955            .map(|a| a.iter().map(|v| v.as_u64().unwrap_or(1) as usize).collect())
956            .unwrap_or_else(|| vec![2, 2, 2, 2, 1, 1]);
957        let n_res = cfg["num_res_blocks"].as_u64().unwrap_or(2) as usize;
958        let mut levels = Vec::new();
959        for (i, &sd) in space_down.iter().enumerate() {
960            let mut blocks = Vec::new();
961            for j in 0..n_res {
962                let p = format!("vvae.enc.down.{i}.block.{j}");
963                let shortcut = model
964                    .tensor(&format!("{p}.nin_shortcut.weight"))
965                    .map(|_| EncConv::load(model, &format!("{p}.nin_shortcut"), 1, 0))
966                    .transpose()?;
967                blocks.push(ResBlock {
968                    norm1: GroupNorm::load(model, &format!("{p}.norm1"))?,
969                    norm2: GroupNorm::load(model, &format!("{p}.norm2"))?,
970                    conv1: EncConv::load(model, &format!("{p}.conv1"), 1, 1)?,
971                    conv2: EncConv::load(model, &format!("{p}.conv2"), 1, 1)?,
972                    shortcut,
973                });
974            }
975            // The downsample's own convolution pads nothing: the caller
976            // reflect-pads one row and column on the far side instead.
977            let down = if sd > 1 {
978                Some(EncConv::load(
979                    model,
980                    &format!("vvae.enc.down.{i}.downsample.conv"),
981                    sd,
982                    0,
983                )?)
984            } else {
985                None
986            };
987            levels.push((blocks, down));
988        }
989        Ok(Self {
990            conv_in: EncConv::load(model, "vvae.enc.conv_in", 1, 1)?,
991            levels,
992            norm_out: GroupNorm::load(model, "vvae.enc.norm_out")?,
993            conv_out: EncConv::load(model, "vvae.enc.conv_out", 1, 1)?,
994            quant: crate::dit::cmf_f32(model, "vvae.quant_conv.weight")?,
995            quant_b: crate::dit::cmf_f32(model, "vvae.quant_conv.bias")?,
996            latents_mean: crate::dit::cmf_f32(model, "vvae.latents_mean")?,
997            latents_std: crate::dit::cmf_f32(model, "vvae.latents_std")?,
998            pool: Pool::from_env(),
999            z_channels: cfg["z_channels"].as_u64().unwrap_or(24) as usize,
1000            ratio: cfg["patch_size"].as_u64().unwrap_or(16) as usize,
1001        })
1002    }
1003
1004    /// One tile, unpadded: `[3, h, w]` → moments `[2·z, h/16, w/16]`.
1005    fn encode_tile(&self, x: &[f32], h: usize, w: usize) -> (Vec<f32>, usize, usize) {
1006        let pool = self.pool.as_deref();
1007        let (mut cur, mut ch, mut cw) = self.conv_in.apply(x, h, w, pool);
1008        let mut c = self.conv_in.out_ch;
1009        for (blocks, down) in &self.levels {
1010            for b in blocks {
1011                let mut hh = cur.clone();
1012                self.norm_out_like(&b.norm1, &mut hh, c, ch * cw);
1013                for v in hh.iter_mut() {
1014                    *v = silu(*v);
1015                }
1016                let (mut t, th, tw) = b.conv1.apply(&hh, ch, cw, pool);
1017                let oc = b.conv1.out_ch;
1018                self.norm_out_like(&b.norm2, &mut t, oc, th * tw);
1019                for v in t.iter_mut() {
1020                    *v = silu(*v);
1021                }
1022                let (t2, th2, tw2) = b.conv2.apply(&t, th, tw, pool);
1023                let skip = match &b.shortcut {
1024                    Some(s) => s.apply(&cur, ch, cw, pool).0,
1025                    None => cur.clone(),
1026                };
1027                cur = t2.iter().zip(&skip).map(|(&a, &b)| a + b).collect();
1028                (ch, cw, c) = (th2, tw2, oc);
1029            }
1030            if let Some(d) = down {
1031                // reflect one row and column on the far side, as the
1032                // reference does before its unpadded stride-2 kernel
1033                let (padded, ph, pw) = reflect_pad_far(&cur, c, ch, cw);
1034                let (t, th, tw) = d.apply(&padded, ph, pw, pool);
1035                cur = t;
1036                (ch, cw, c) = (th, tw, d.out_ch);
1037            }
1038        }
1039        self.norm_out_like(&self.norm_out, &mut cur, c, ch * cw);
1040        for v in cur.iter_mut() {
1041            *v = silu(*v);
1042        }
1043        let (moments, mh, mw) = self.conv_out.apply(&cur, ch, cw, pool);
1044        // quant_conv is 1x1x1: a per-position linear.
1045        let n = 2 * self.z_channels;
1046        let mut out = vec![0f32; n * mh * mw];
1047        for o in 0..n {
1048            for p in 0..mh * mw {
1049                let mut acc = self.quant_b[o];
1050                for i in 0..n {
1051                    acc += self.quant[o * n + i] * moments[i * mh * mw + p];
1052                }
1053                out[o * mh * mw + p] = acc;
1054            }
1055        }
1056        (out, mh, mw)
1057    }
1058
1059    fn norm_out_like(&self, g: &GroupNorm, x: &mut [f32], ch: usize, hw: usize) {
1060        g.apply(x, ch, hw);
1061    }
1062
1063    /// A single frame in [-1, 1], `[3, h, w]` → normalized latents
1064    /// `[z, h/16, w/16]`, spatially tiled exactly as the reference does.
1065    pub fn encode_frame(&self, rgb: &[f32], h: usize, w: usize) -> (Vec<f32>, usize, usize) {
1066        // [-1, 1] → [0, 1] → ImageNet-normalized, the reference's order.
1067        let mut x = vec![0f32; 3 * h * w];
1068        for c in 0..3 {
1069            for p in 0..h * w {
1070                let v = (rgb[c * h * w + p] + 1.0) * 0.5;
1071                x[c * h * w + p] = (v - IMAGENET_MEAN[c]) / IMAGENET_STD[c];
1072            }
1073        }
1074        let r = self.ratio;
1075        let (ys, yl, yo) = split_tiles(h, r);
1076        let (xs, xl, xo) = split_tiles(w, r);
1077        let (zh, zw) = (h / r, w / r);
1078        let zc = 2 * self.z_channels;
1079        let mut rows: Vec<Vec<Vec<f32>>> = Vec::new();
1080        let mut dims: Vec<Vec<(usize, usize)>> = Vec::new();
1081        for (&ip, &il) in ys.iter().zip(&yl) {
1082            let mut row = Vec::new();
1083            let mut rd = Vec::new();
1084            for (&jp, &jl) in xs.iter().zip(&xl) {
1085                let mut sub = vec![0f32; 3 * il * jl];
1086                for c in 0..3 {
1087                    for yy in 0..il {
1088                        let s = (c * h + ip + yy) * w + jp;
1089                        let d = (c * il + yy) * jl;
1090                        sub[d..d + jl].copy_from_slice(&x[s..s + jl]);
1091                    }
1092                }
1093                let (t, th, tw) = self.encode_tile(&sub, il, jl);
1094                row.push(t);
1095                rd.push((th, tw));
1096            }
1097            rows.push(row);
1098            dims.push(rd);
1099        }
1100        // Latent-space blend then trim, mirroring `tiled_encode`.
1101        let mut canvas = vec![0f32; zc * zh * zw];
1102        let mut out_y = 0usize;
1103        for i in 0..rows.len() {
1104            let mut out_x = 0usize;
1105            let mut row_h = 0usize;
1106            for j in 0..rows[i].len() {
1107                let (th, tw) = dims[i][j];
1108                let mut tile = rows[i][j].clone();
1109                let (mut ch_, mut cw_) = (th, tw);
1110                if i > 0 {
1111                    tile = blend_plane(&rows[i - 1][j], &tile, zc, dims[i - 1][j], (ch_, cw_), yo[i - 1] / r, 0);
1112                }
1113                if j > 0 {
1114                    tile = blend_plane(&rows[i][j - 1], &tile, zc, dims[i][j - 1], (ch_, cw_), xo[j - 1] / r, 1);
1115                }
1116                if i + 1 < rows.len() {
1117                    tile = crop_plane(&tile, zc, ch_, cw_, 0, ch_ - yo[i] / r, 0, cw_);
1118                    ch_ -= yo[i] / r;
1119                }
1120                if j + 1 < rows[i].len() {
1121                    tile = crop_plane(&tile, zc, ch_, cw_, 0, ch_, 0, cw_ - xo[j] / r);
1122                    cw_ -= xo[j] / r;
1123                }
1124                for c in 0..zc {
1125                    for yy in 0..ch_ {
1126                        let d = (c * zh + out_y + yy) * zw + out_x;
1127                        let s = (c * ch_ + yy) * cw_;
1128                        canvas[d..d + cw_].copy_from_slice(&tile[s..s + cw_]);
1129                    }
1130                }
1131                out_x += cw_;
1132                row_h = ch_;
1133            }
1134            out_y += row_h;
1135        }
1136        // The posterior MEAN is the first z channels; no sampling.
1137        let mut z = vec![0f32; self.z_channels * zh * zw];
1138        for c in 0..self.z_channels {
1139            let (m, s) = (self.latents_mean[c], self.latents_std[c]);
1140            for p in 0..zh * zw {
1141                z[c * zh * zw + p] = (canvas[c * zh * zw + p] - m) / s;
1142            }
1143        }
1144        (z, zh, zw)
1145    }
1146}
1147
1148/// Reflect one row and one column onto the far edge — `F.pad(x, (0,1,0,1))`.
1149fn reflect_pad_far(x: &[f32], c: usize, h: usize, w: usize) -> (Vec<f32>, usize, usize) {
1150    let (ph, pw) = (h + 1, w + 1);
1151    let mut out = vec![0f32; c * ph * pw];
1152    for ci in 0..c {
1153        for y in 0..ph {
1154            let sy = if y < h { y } else { h - 2 };
1155            for x2 in 0..pw {
1156                let sx = if x2 < w { x2 } else { w - 2 };
1157                out[(ci * ph + y) * pw + x2] = x[(ci * h + sy) * w + sx];
1158            }
1159        }
1160    }
1161    (out, ph, pw)
1162}
1163
1164/// Cross-fade `a`'s tail into `b`'s head over `extent`, `dim` 0 = y, 1 = x.
1165fn blend_plane(
1166    a: &[f32],
1167    b: &[f32],
1168    c: usize,
1169    ad: (usize, usize),
1170    bd: (usize, usize),
1171    extent: usize,
1172    dim: usize,
1173) -> Vec<f32> {
1174    let (ah, aw) = ad;
1175    let (bh, bw) = bd;
1176    let mut out = b.to_vec();
1177    let e = if dim == 0 { extent.min(ah).min(bh) } else { extent.min(aw).min(bw) };
1178    if e == 0 {
1179        return out;
1180    }
1181    for ci in 0..c {
1182        for k in 0..e {
1183            let wb = k as f32 / e as f32;
1184            let wa = 1.0 - wb;
1185            if dim == 0 {
1186                for x in 0..bw.min(aw) {
1187                    let sa = (ci * ah + ah - e + k) * aw + x;
1188                    let sb = (ci * bh + k) * bw + x;
1189                    out[sb] = a[sa] * wa + b[sb] * wb;
1190                }
1191            } else {
1192                for y in 0..bh.min(ah) {
1193                    let sa = (ci * ah + y) * aw + aw - e + k;
1194                    let sb = (ci * bh + y) * bw + k;
1195                    out[sb] = a[sa] * wa + b[sb] * wb;
1196                }
1197            }
1198        }
1199    }
1200    out
1201}
1202
1203/// `[c, h, w]` sub-rectangle.
1204#[allow(clippy::too_many_arguments)]
1205fn crop_plane(x: &[f32], c: usize, h: usize, w: usize, y0: usize, y1: usize, x0: usize, x1: usize) -> Vec<f32> {
1206    let (nh, nw) = (y1 - y0, x1 - x0);
1207    let mut out = vec![0f32; c * nh * nw];
1208    for ci in 0..c {
1209        for y in 0..nh {
1210            let s = (ci * h + y0 + y) * w + x0;
1211            let d = (ci * nh + y) * nw;
1212            out[d..d + nw].copy_from_slice(&x[s..s + nw]);
1213        }
1214    }
1215    out
1216}