Skip to main content

long_screen/kernels/
framing.rs

1//! Presentation canvas synthesis (`src/core/framing.ts`): layout/coordinate mapping, background-extension
2//! statistics, and the two per-tile passes `buildFramedCanvas` drives (chrome/content copy, then source-tile
3//! evidence fold). Tile-cache traversal, the O(perimeter) candidate pre-check and KV writes stay in TS.
4
5use crate::kernels::geometry::{js_ceil, js_floor, Rect};
6use crate::kernels::region::Region;
7
8/// Presentation layout: native chrome (`pane`) plus a decorative background-extension band wrapped around the
9/// live content rect (`content`), matching `frameLayout`.
10#[derive(Clone, Copy)]
11pub struct Layout {
12    pub width: f64,
13    pub height: f64,
14    pub pane: Rect,
15    pub content: Rect,
16    pub dx: f64,
17    pub dy: f64,
18    pub seam_x: f64,
19}
20
21pub fn frame_layout(
22    source_width: f64,
23    source_height: f64,
24    pane: Rect,
25    bounds_width: f64,
26    bounds_height: f64,
27) -> Layout {
28    let content = Rect {
29        x: pane.x,
30        y: pane.y,
31        width: pane.width.max(js_ceil(bounds_width) as f64),
32        height: pane.height.max(js_ceil(bounds_height) as f64),
33    };
34    Layout {
35        width: source_width + content.width - pane.width,
36        height: source_height + content.height - pane.height,
37        pane,
38        content,
39        dx: content.width - pane.width,
40        dy: content.height - pane.height,
41        seam_x: (pane.x + pane.width / 2.0).floor(),
42    }
43}
44
45/// Native source coordinate for a context pixel: `None` is a decorative extension, `Some(None)` is
46/// reconstructed content, `Some(Some((x, y)))` is a native-chrome pixel copied from the reference frame.
47pub enum Coordinate {
48    Content,
49    Extension,
50    Source(f64, f64),
51}
52
53pub fn frame_coordinate(layout: &Layout, x: f64, y: f64) -> Coordinate {
54    let (p, c, dx, dy, seam_x) = (
55        layout.pane,
56        layout.content,
57        layout.dx,
58        layout.dy,
59        layout.seam_x,
60    );
61    if x >= c.x && x < c.x + c.width && y >= c.y && y < c.y + c.height {
62        return Coordinate::Content;
63    }
64    if y < p.y || y >= c.y + c.height {
65        let sy = if y < p.y { y } else { y - dy };
66        if x >= seam_x && x < seam_x + dx {
67            return Coordinate::Extension;
68        }
69        return Coordinate::Source(if x < seam_x { x } else { x - dx }, sy);
70    }
71    if y >= p.y + p.height {
72        return Coordinate::Extension;
73    }
74    Coordinate::Source(if x < p.x { x } else { x - dx }, y)
75}
76
77/// Most frequent value; ties keep the first value to reach the winning count (matches `Map` insertion order
78/// under JS's single-pass "first strictly greater count wins" scan). Empty input yields 0.
79fn mode(values: &[u32]) -> u32 {
80    let mut counts: std::collections::HashMap<u32, u32> = std::collections::HashMap::new();
81    let mut best = values.first().copied().unwrap_or(0);
82    let mut count = 0u32;
83    for &v in values {
84        let n = counts.entry(v).or_insert(0);
85        *n += 1;
86        if *n > count {
87            count = *n;
88            best = v;
89        }
90    }
91    best
92}
93
94/// Packs one RGBA pixel the way a `Uint32Array` view over the same little-endian bytes would.
95#[inline]
96fn px32(rgba: &[u8], i: usize) -> u32 {
97    u32::from_le_bytes([
98        rgba[i * 4],
99        rgba[i * 4 + 1],
100        rgba[i * 4 + 2],
101        rgba[i * 4 + 3],
102    ])
103}
104
105/// One sampled row/column per background axis, matching `backgrounds()`: the modal pixel sampled every
106/// `max(1, floor(pane.dim / 128))` columns/rows across the pane.
107pub fn backgrounds(
108    rgba: &[u8],
109    image_width: usize,
110    image_height: usize,
111    pane: Rect,
112) -> (Vec<u32>, Vec<u32>) {
113    let xs = (1usize).max((pane.width / 128.0).floor() as usize);
114    let ys = (1usize).max((pane.height / 128.0).floor() as usize);
115    let mut rows = vec![0u32; image_height];
116    let mut columns = vec![0u32; image_width];
117    let mut samples = Vec::new();
118    for (y, row) in rows.iter_mut().enumerate() {
119        samples.clear();
120        let mut x = pane.x as i64;
121        while (x as f64) < pane.x + pane.width {
122            samples.push(px32(rgba, y * image_width + x as usize));
123            x += xs as i64;
124        }
125        *row = mode(&samples);
126    }
127    for (x, column) in columns.iter_mut().enumerate() {
128        samples.clear();
129        let mut y = pane.y as i64;
130        while (y as f64) < pane.y + pane.height {
131            samples.push(px32(rgba, y as usize * image_width + x));
132            y += ys as i64;
133        }
134        *column = mode(&samples);
135    }
136    (rows, columns)
137}
138
139/// Output tile buffers `buildFramedCanvas` mutates. `frozen` is written by the evidence fold, not read.
140pub struct OutputTile<'a> {
141    pub size: usize,
142    pub pixels: &'a mut [u8],
143    pub coverage: &'a mut [u8],
144    pub provisional: &'a mut [u8],
145    pub quality: &'a mut [u8],
146    pub score: &'a mut [f32],
147    pub owner: &'a mut [u32],
148    pub conflicts: &'a mut [u8],
149    pub frozen: &'a mut [u8],
150}
151
152/// One resident source tile the evidence fold reads from.
153pub struct SourceTile<'a> {
154    pub sx: i64,
155    pub sy: i64,
156    pub pixels: &'a [u8],
157    pub coverage: &'a [u8],
158    pub provisional: &'a [u8],
159    pub quality: &'a [u8],
160    pub score: &'a [f32],
161    pub owner: &'a [u32],
162    pub conflicts: &'a [u8],
163    pub frozen: &'a [u8],
164}
165
166#[inline]
167fn bit(bits: &[u8], p: usize) -> bool {
168    bits[p >> 3] & (1 << (p & 7)) != 0
169}
170#[inline]
171fn set_bit(bits: &mut [u8], p: usize) {
172    bits[p >> 3] |= 1 << (p & 7);
173}
174
175fn intersect(a: Rect, b: Rect) -> Rect {
176    let x = a.x.max(b.x);
177    let y = a.y.max(b.y);
178    Rect {
179        x,
180        y,
181        width: 0f64.max((a.x + a.width).min(b.x + b.width) - x),
182        height: 0f64.max((a.y + a.height).min(b.y + b.height) - y),
183    }
184}
185
186/// Chrome/content synthesis (`buildFramedCanvas`'s pass 1) for one output tile: every pixel of `bounds`
187/// (already clipped to the framed canvas) either copies a native reference pixel, paints a background band,
188/// or is left untouched (reconstructed-content interior, filled later by `fold_evidence`).
189#[allow(clippy::too_many_arguments)]
190pub fn paint_tile(
191    out: &mut OutputTile<'_>,
192    layout: &Layout,
193    bounds: Rect,
194    source: &[u8],
195    source_width: usize,
196    source_height: usize,
197    bg_rows: &[u32],
198    bg_columns: &[u32],
199    ignore: &[Region],
200) {
201    let size = out.size;
202    let bw = bounds.width as i64;
203    let bh = bounds.height as i64;
204    for y in 0..bh {
205        for x in 0..bw {
206            let (ox, oy) = (bounds.x + x as f64, bounds.y + y as f64);
207            let at = (y as usize) * size + x as usize;
208            match frame_coordinate(layout, ox, oy) {
209                Coordinate::Content => continue,
210                Coordinate::Source(sx, sy) => {
211                    if ignore
212                        .iter()
213                        .any(|r| r.contains(sx, sy, source_width as f64, source_height as f64))
214                    {
215                        continue;
216                    }
217                    let src = sy as usize * source_width + sx as usize;
218                    out.pixels[at * 4..at * 4 + 4].copy_from_slice(&source[src * 4..src * 4 + 4]);
219                    if out.pixels[at * 4 + 3] != 0 {
220                        set_bit(out.coverage, at);
221                    }
222                }
223                Coordinate::Extension => {
224                    let v = if oy < layout.pane.y || oy >= layout.content.y + layout.content.height
225                    {
226                        bg_row(
227                            bg_rows,
228                            if oy < layout.pane.y {
229                                oy
230                            } else {
231                                oy - layout.dy
232                            },
233                        )
234                    } else {
235                        bg_column(
236                            bg_columns,
237                            if ox < layout.pane.x {
238                                ox
239                            } else {
240                                ox - layout.dx
241                            },
242                        )
243                    };
244                    out.pixels[at * 4..at * 4 + 4].copy_from_slice(&v.to_le_bytes());
245                }
246            }
247        }
248    }
249}
250
251/// `bg.rows[oy]`/`bg.columns[ox]` with JS's out-of-bounds-index-is-`undefined` semantics: `dx > pane.x` (a
252/// small chrome pane inside a much larger background extension) makes `ox - dx` negative, which JS reads as
253/// `undefined` and writes as 0.
254#[inline]
255fn bg_row(rows: &[u32], y: f64) -> u32 {
256    if y < 0.0 {
257        0
258    } else {
259        rows.get(y as usize).copied().unwrap_or(0)
260    }
261}
262#[inline]
263fn bg_column(columns: &[u32], x: f64) -> u32 {
264    if x < 0.0 {
265        0
266    } else {
267        columns.get(x as usize).copied().unwrap_or(0)
268    }
269}
270
271struct BlockAgg {
272    quality: u8,
273    score: f32,
274    score_set: bool,
275    owner_single: Option<u32>,
276    owner_conflict: bool,
277    conflicts: bool,
278    frozen: bool,
279}
280
281/// Evidence fold (`buildFramedCanvas`'s pass 2) for one output tile against up to four overlapping source
282/// tiles: copies covered source pixels into the tile (healing provisional bits, carrying coverage), then
283/// folds each touched 16×16 block's quality/score/owner/conflicts/frozen into the destination block.
284#[allow(clippy::too_many_arguments)]
285pub fn fold_evidence(
286    out: &mut OutputTile<'_>,
287    layout: &Layout,
288    source_bounds_x: f64,
289    source_bounds_y: f64,
290    bounds: Rect,
291    sources: &[SourceTile<'_>],
292) {
293    const QUALITY_BLOCK: i64 = 16;
294    let size = out.size as i64;
295    let overlap = intersect(bounds, layout.content);
296    if overlap.width <= 0.0 || overlap.height <= 0.0 {
297        return;
298    }
299    let wx = js_floor(source_bounds_x + overlap.x - layout.content.x) as i64;
300    let wy = js_floor(source_bounds_y + overlap.y - layout.content.y) as i64;
301    let mut evidence: std::collections::HashMap<i64, BlockAgg> = std::collections::HashMap::new();
302    let blocks_per_row = size / QUALITY_BLOCK;
303    for t in sources {
304        let part = intersect(
305            Rect {
306                x: wx as f64,
307                y: wy as f64,
308                width: overlap.width,
309                height: overlap.height,
310            },
311            Rect {
312                x: (t.sx * size) as f64,
313                y: (t.sy * size) as f64,
314                width: size as f64,
315                height: size as f64,
316            },
317        );
318        if part.width <= 0.0 || part.height <= 0.0 {
319            continue;
320        }
321        let (py, ph, px, pw) = (
322            part.y as i64,
323            part.height as i64,
324            part.x as i64,
325            part.width as i64,
326        );
327        for y in py..py + ph {
328            let dy = (overlap.y - bounds.y) as i64 + y - wy;
329            let dx = (overlap.x - bounds.x) as i64 + px - wx;
330            let from = (y - t.sy * size) * size + px - t.sx * size;
331            for x in 0..pw {
332                let source_pixel = (from + x) as usize;
333                let destination_pixel = (dy * size + dx + x) as usize;
334                out.pixels[destination_pixel * 4..destination_pixel * 4 + 4]
335                    .copy_from_slice(&t.pixels[source_pixel * 4..source_pixel * 4 + 4]);
336                if bit(t.coverage, source_pixel) {
337                    set_bit(out.coverage, destination_pixel);
338                    let sbx = (source_pixel as i64 % size) / QUALITY_BLOCK;
339                    let sby = (source_pixel as i64 / size) / QUALITY_BLOCK;
340                    let source_block = sby * blocks_per_row + sbx;
341                    let dbx = (destination_pixel as i64 % size) / QUALITY_BLOCK;
342                    let dby = (destination_pixel as i64 / size) / QUALITY_BLOCK;
343                    let destination_block = dby * blocks_per_row + dbx;
344                    let agg = evidence.entry(destination_block).or_insert(BlockAgg {
345                        quality: 255,
346                        score: 0.0,
347                        score_set: false,
348                        owner_single: None,
349                        owner_conflict: false,
350                        conflicts: false,
351                        frozen: false,
352                    });
353                    agg.quality = agg.quality.min(t.quality[source_block as usize]);
354                    let sscore = t.score[source_block as usize];
355                    agg.score = if agg.score_set {
356                        agg.score.min(sscore)
357                    } else {
358                        sscore
359                    };
360                    agg.score_set = true;
361                    let sowner = t.owner[source_block as usize];
362                    match agg.owner_single {
363                        None if !agg.owner_conflict => agg.owner_single = Some(sowner),
364                        Some(existing) if existing != sowner => {
365                            agg.owner_conflict = true;
366                            agg.owner_single = None;
367                        }
368                        _ => {}
369                    }
370                    agg.conflicts |= t.conflicts[source_block as usize] != 0;
371                    agg.frozen |= t.frozen[source_block as usize] != 0;
372                }
373                if bit(t.provisional, source_pixel) {
374                    set_bit(out.provisional, destination_pixel);
375                }
376            }
377        }
378    }
379    for (q, agg) in evidence {
380        out.quality[q as usize] = agg.quality;
381        out.score[q as usize] = if agg.score_set { agg.score } else { 0.0 };
382        out.conflicts[q as usize] = agg.conflicts as u8;
383        out.frozen[q as usize] = agg.frozen as u8;
384        // A single distinct non-zero owner across every contributing source block, else unowned (0) — the same
385        // rule as `owners.size === 1 && !owners.has(0) ? [...owners][0] : 0` (a lone owner of 0 maps to 0 either way).
386        out.owner[q as usize] = match agg.owner_single {
387            Some(o) if o != 0 && !agg.owner_conflict => o,
388            _ => 0,
389        };
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396
397    fn rect(x: f64, y: f64, w: f64, h: f64) -> Rect {
398        Rect {
399            x,
400            y,
401            width: w,
402            height: h,
403        }
404    }
405
406    #[test]
407    fn layout_matches_the_frozen_ts_geometry() {
408        let layout = frame_layout(12.0, 10.0, rect(2.0, 2.0, 8.0, 6.0), 14.0, 12.0);
409        assert_eq!((layout.width, layout.height), (18.0, 16.0));
410    }
411
412    #[test]
413    fn coordinate_classifies_content_extension_and_source() {
414        let layout = frame_layout(12.0, 10.0, rect(2.0, 2.0, 8.0, 6.0), 14.0, 12.0);
415        assert!(matches!(
416            frame_coordinate(&layout, 5.0, 5.0),
417            Coordinate::Content
418        ));
419        match frame_coordinate(&layout, 0.0, 0.0) {
420            Coordinate::Source(x, y) => assert_eq!((x, y), (0.0, 0.0)),
421            _ => panic!("expected source"),
422        }
423    }
424
425    #[test]
426    fn mode_breaks_ties_by_first_arrival() {
427        // [3, 1, 1, 3]: 1 reaches count 2 at index 2 (strictly beating 3's count of 1 so far); 3 only ties that
428        // count at index 3, so 1 keeps the lead — matches the frozen TS `mode()`'s single-pass "strictly greater" scan.
429        assert_eq!(mode(&[3, 1, 1, 3]), 1);
430        assert_eq!(mode(&[]), 0);
431    }
432}