Skip to main content

vyre_libs/visual/cell_grid/
mod.rs

1//! Character-cell grid expansion.
2//!
3//! One invocation derives one packed `u32` RGBA pixel from the cell that
4//! covers it. A terminal surface is a small grid of cells and a large field
5//! of pixels: 80x24 cells behind 800x456 pixels. Expanding the grid on the
6//! host means building one rectangle per cell every frame and handing the
7//! renderer tens of thousands of them; expanding it here means the host
8//! writes one `u32` per cell that changed and nothing else.
9//!
10//! Category A composition  -  pure IR over existing expressions, specializing
11//! the Tier 2.5 `packed_rgba_map` shape. No new IR variant, no target
12//! lowering.
13
14use vyre_foundation::ir::model::expr::GeneratorRef;
15use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
16
17const OP_ID: &str = "vyre-libs::visual::cell_grid";
18
19/// Pixel dimensions a cell grid expands to.
20///
21/// Kept as one value so a caller cannot pass the four numbers in the wrong
22/// order without saying which is which.
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub struct GridShape {
25    /// Cells across.
26    pub cols: u32,
27    /// Cells down.
28    pub rows: u32,
29    /// Pixels across one cell.
30    pub cell_width: u32,
31    /// Pixels down one cell.
32    pub cell_height: u32,
33}
34
35impl GridShape {
36    /// Pixels across the whole surface.
37    #[must_use]
38    pub const fn width(&self) -> u32 {
39        self.cols * self.cell_width
40    }
41
42    /// Pixels down the whole surface.
43    #[must_use]
44    pub const fn height(&self) -> u32 {
45        self.rows * self.cell_height
46    }
47
48    /// Cells in the grid.
49    #[must_use]
50    pub const fn cell_count(&self) -> u32 {
51        self.cols * self.rows
52    }
53
54    /// Pixels in the surface.
55    #[must_use]
56    pub const fn pixel_count(&self) -> u32 {
57        self.width() * self.height()
58    }
59
60    pub(super) fn validated(self) -> Self {
61        assert!(
62            self.cols > 0 && self.rows > 0,
63            "Fix: a cell grid needs at least one row and one column, got {}x{}",
64            self.cols,
65            self.rows
66        );
67        assert!(
68            self.cell_width > 0 && self.cell_height > 0,
69            "Fix: a cell needs a non-zero size, got {}x{} pixels",
70            self.cell_width,
71            self.cell_height
72        );
73        // Every product below is computed once, here, so an overflow is a
74        // named build-time failure rather than a wrapped count that silently
75        // sizes a buffer too small.
76        let width = self
77            .cols
78            .checked_mul(self.cell_width)
79            .expect("Fix: cols * cell_width overflows u32");
80        let height = self
81            .rows
82            .checked_mul(self.cell_height)
83            .expect("Fix: rows * cell_height overflows u32");
84        width
85            .checked_mul(height)
86            .expect("Fix: the surface pixel count overflows u32");
87        self.cols
88            .checked_mul(self.rows)
89            .expect("Fix: cols * rows overflows u32");
90        self
91    }
92}
93
94/// Bind `y`, `x`, `col`, `row` and `cell` for the pixel already bound as
95/// `idx`. Shared by every op that expands a cell grid, so the mapping cannot
96/// drift between them.
97///
98/// Every divisor is a build-time constant, so the Layer 1 strength-reduction
99/// pass turns each division into mulhi plus a shift. Writing the division is
100/// the correct thing to write.
101pub(super) fn cell_lookup_nodes(shape: GridShape) -> Vec<Node> {
102    let width = shape.width();
103    vec![
104        Node::let_bind("y", Expr::div(Expr::var("idx"), Expr::u32(width))),
105        // x = idx - y * width, not idx % width. The remainder lowers to the
106        // same division just performed, plus a multiply and a subtract, so
107        // reusing y keeps one division for the pair.
108        Node::let_bind(
109            "x",
110            Expr::sub(
111                Expr::var("idx"),
112                Expr::mul(Expr::var("y"), Expr::u32(width)),
113            ),
114        ),
115        Node::let_bind(
116            "col",
117            Expr::div(Expr::var("x"), Expr::u32(shape.cell_width)),
118        ),
119        Node::let_bind(
120            "row",
121            Expr::div(Expr::var("y"), Expr::u32(shape.cell_height)),
122        ),
123        Node::let_bind(
124            "cell",
125            Expr::add(
126                Expr::mul(Expr::var("row"), Expr::u32(shape.cols)),
127                Expr::var("col"),
128            ),
129        ),
130    ]
131}
132
133/// Build a Program that fills `output` with one packed RGBA pixel per pixel of
134/// the surface, taking each pixel's colour from the cell that covers it.
135///
136/// `cells` is `[u32; cols * rows]` in row-major order, one packed RGBA colour
137/// per cell. `output` is `[u32; width * height]`, also row-major.
138#[must_use]
139pub fn cell_grid_fill(cells: &str, output: &str, shape: GridShape) -> Program {
140    let shape = shape.validated();
141    let pixels = shape.pixel_count();
142    let width = shape.width();
143
144    Program::wrapped(
145        vec![
146            BufferDecl::storage(cells, 0, BufferAccess::ReadOnly, DataType::U32)
147                .with_count(shape.cell_count()),
148            BufferDecl::storage(output, 1, BufferAccess::ReadWrite, DataType::U32)
149                .with_count(pixels),
150        ],
151        super::PIXEL_WORKGROUP_SIZE,
152        vec![crate::region::wrap_anonymous(
153            OP_ID,
154            vec![crate::region::wrap_child(
155                vyre_primitives::visual::packed_rgba_map::OP_ID,
156                GeneratorRef {
157                    name: OP_ID.to_string(),
158                },
159                vec![
160                    Node::let_bind("idx", Expr::gid_x()),
161                    Node::if_then(
162                        Expr::lt(Expr::var("idx"), Expr::u32(pixels)),
163                        {
164                            let mut body = cell_lookup_nodes(shape);
165                            body.push(Node::let_bind(
166                                "colour",
167                                Expr::load(cells, Expr::var("cell")),
168                            ));
169                            body.push(Node::store(output, Expr::var("idx"), Expr::var("colour")));
170                            body
171                        },
172                    ),
173                ],
174            )],
175        )],
176    )
177}
178
179inventory::submit! {
180    vyre_foundation::operation::OperationRegistration {
181        semantic_version: 1,
182        signature: None,
183        tier: vyre_foundation::operation::OperationTier::Library,
184        laws: &[],
185        tolerance: vyre_foundation::operation::TolerancePolicy::EXACT,
186        id: OP_ID,
187        build: Some(|| {
188            cell_grid_fill(
189                "cells",
190                "out",
191                GridShape { cols: 2, rows: 2, cell_width: 2, cell_height: 2 },
192            )
193        }),
194        test_inputs: Some(|| {
195            // Four cells behind a 4x4 surface: red, green on the top row,
196            // blue, white on the bottom. Packing is little-endian RGBA, so
197            // bits [7:0] are red and [31:24] are alpha.
198            let cells = [0xFF00_00FFu32, 0xFF00_FF00, 0xFFFF_0000, 0xFFFF_FFFF];
199            vec![vec![
200                crate::visual::byte_helpers::u32_words_to_le_bytes(&cells),
201                vec![0u8; 16 * 4],
202            ]]
203        }),
204        expected_output: Some(|| {
205            // Each cell covers a 2x2 block, so every colour appears four
206            // times, in a quadrant rather than a run.
207            const R: u32 = 0xFF00_00FF;
208            const G: u32 = 0xFF00_FF00;
209            const B: u32 = 0xFFFF_0000;
210            const W: u32 = 0xFFFF_FFFF;
211            let expected = [
212                R, R, G, G,
213                R, R, G, G,
214                B, B, W, W,
215                B, B, W, W,
216            ];
217            vec![vec![crate::visual::byte_helpers::u32_words_to_le_bytes(&expected)]]
218        }),
219        category: Some("visual"),
220    }
221}