| 1 | //! Software renderer: compose the grid into an ARGB8888 buffer. |
| 2 | //! |
| 3 | //! The target is a `wl_shm` buffer in `Argb8888`, which on little-endian is |
| 4 | //! `[B, G, R, A]` per pixel. Rendering is two passes per frame - backgrounds |
| 5 | //! then glyphs - so a wide glyph that overflows its cell is not clipped by the |
| 6 | //! neighbouring cell's background fill. |
| 7 | |
| 8 | use crate::font::{CellMetrics, Fonts, GlyphData, Style}; |
| 9 | use crate::grid::{Color, Flags, Grid}; |
| 10 | |
| 11 | /// Foreground/background used for `Color::Default`. |
| 12 | const DEFAULT_FG: Rgb = Rgb(0xc5, 0xc8, 0xc6); |
| 13 | const DEFAULT_BG: Rgb = Rgb(0x18, 0x18, 0x18); |
| 14 | |
| 15 | #[derive(Clone, Copy)] |
| 16 | struct Rgb(u8, u8, u8); |
| 17 | |
| 18 | /// A mutable view over a BGRA pixel buffer. |
| 19 | struct Canvas<'a> { |
| 20 | pixels: &'a mut [u8], |
| 21 | width: usize, |
| 22 | height: usize, |
| 23 | } |
| 24 | |
| 25 | impl Canvas<'_> { |
| 26 | fn index(&self, x: i32, y: i32) -> Option<usize> { |
| 27 | if x < 0 || y < 0 || x as usize >= self.width || y as usize >= self.height { |
| 28 | return None; |
| 29 | } |
| 30 | Some((y as usize * self.width + x as usize) * 4) |
| 31 | } |
| 32 | |
| 33 | fn fill_rect(&mut self, x0: i32, y0: i32, w: u32, h: u32, c: Rgb) { |
| 34 | for dy in 0..h as i32 { |
| 35 | for dx in 0..w as i32 { |
| 36 | if let Some(i) = self.index(x0 + dx, y0 + dy) { |
| 37 | self.pixels[i] = c.2; |
| 38 | self.pixels[i + 1] = c.1; |
| 39 | self.pixels[i + 2] = c.0; |
| 40 | self.pixels[i + 3] = 0xff; |
| 41 | } |
| 42 | } |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | /// Alpha-blend `fg` over the existing pixel with coverage `a`. |
| 47 | fn blend(&mut self, x: i32, y: i32, fg: Rgb, a: u8) { |
| 48 | let Some(i) = self.index(x, y) else { return }; |
| 49 | let (a, inv) = (u32::from(a), u32::from(255 - a)); |
| 50 | let mix = |src: u8, dst: u8| ((u32::from(src) * a + u32::from(dst) * inv) / 255) as u8; |
| 51 | self.pixels[i] = mix(fg.2, self.pixels[i]); |
| 52 | self.pixels[i + 1] = mix(fg.1, self.pixels[i + 1]); |
| 53 | self.pixels[i + 2] = mix(fg.0, self.pixels[i + 2]); |
| 54 | self.pixels[i + 3] = 0xff; |
| 55 | } |
| 56 | |
| 57 | /// Composite one pre-multiplied BGRA source pixel over the destination. |
| 58 | fn over(&mut self, x: i32, y: i32, src: &[u8]) { |
| 59 | let Some(i) = self.index(x, y) else { return }; |
| 60 | let inv = u32::from(255 - src[3]); |
| 61 | let comp = |s: u8, dst: u8| (u32::from(s) + u32::from(dst) * inv / 255).min(255) as u8; |
| 62 | self.pixels[i] = comp(src[0], self.pixels[i]); |
| 63 | self.pixels[i + 1] = comp(src[1], self.pixels[i + 1]); |
| 64 | self.pixels[i + 2] = comp(src[2], self.pixels[i + 2]); |
| 65 | self.pixels[i + 3] = 0xff; |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | #[derive(Debug)] |
| 70 | pub struct Renderer { |
| 71 | fonts: Fonts, |
| 72 | } |
| 73 | |
| 74 | impl Renderer { |
| 75 | pub fn new(fonts: Fonts) -> Self { |
| 76 | Self { fonts } |
| 77 | } |
| 78 | |
| 79 | pub fn metrics(&self) -> CellMetrics { |
| 80 | self.fonts.metrics() |
| 81 | } |
| 82 | |
| 83 | /// Compose `grid` into `pixels` (BGRA, `width`×`height` px). The cursor cell |
| 84 | /// is drawn reversed. |
| 85 | pub fn render(&mut self, grid: &Grid, pixels: &mut [u8], width: usize, height: usize) { |
| 86 | let mut canvas = Canvas { |
| 87 | pixels, |
| 88 | width, |
| 89 | height, |
| 90 | }; |
| 91 | canvas.fill_rect(0, 0, width as u32, height as u32, DEFAULT_BG); |
| 92 | |
| 93 | let m = self.fonts.metrics(); |
| 94 | let cursor = grid.cursor(); |
| 95 | |
| 96 | for y in 0..grid.rows() { |
| 97 | for x in 0..grid.cols() { |
| 98 | let (_, bg) = cell_colors(grid.cell(x, y), (x, y) == cursor); |
| 99 | let (px, py) = (x as i32 * m.width as i32, y as i32 * m.height as i32); |
| 100 | canvas.fill_rect(px, py, m.width, m.height, bg); |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | for y in 0..grid.rows() { |
| 105 | for x in 0..grid.cols() { |
| 106 | let cell = grid.cell(x, y); |
| 107 | if cell.flags.contains(Flags::WIDE_CONT) || cell.c == ' ' { |
| 108 | continue; |
| 109 | } |
| 110 | let (fg, _) = cell_colors(cell, (x, y) == cursor); |
| 111 | let style = Style { |
| 112 | bold: cell.flags.contains(Flags::BOLD), |
| 113 | italic: cell.flags.contains(Flags::ITALIC), |
| 114 | }; |
| 115 | let origin_x = x as i32 * m.width as i32; |
| 116 | let baseline = y as i32 * m.height as i32 + m.ascent as i32; |
| 117 | self.draw_glyph(&mut canvas, cell.c, style, origin_x, baseline, fg); |
| 118 | } |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | fn draw_glyph( |
| 123 | &mut self, |
| 124 | canvas: &mut Canvas, |
| 125 | c: char, |
| 126 | style: Style, |
| 127 | origin_x: i32, |
| 128 | baseline: i32, |
| 129 | fg: Rgb, |
| 130 | ) { |
| 131 | let glyph = match self.fonts.glyph(c, style) { |
| 132 | Ok(glyph) => glyph, |
| 133 | Err(err) => { |
| 134 | tracing::debug!("glyph {c:?}: {err}"); |
| 135 | return; |
| 136 | } |
| 137 | }; |
| 138 | let (left, top, w, h) = ( |
| 139 | glyph.left, |
| 140 | glyph.top, |
| 141 | glyph.width as i32, |
| 142 | glyph.height as i32, |
| 143 | ); |
| 144 | match &glyph.data { |
| 145 | GlyphData::Mask(mask) => { |
| 146 | for gy in 0..h { |
| 147 | for gx in 0..w { |
| 148 | let a = mask[(gy * w + gx) as usize]; |
| 149 | if a != 0 { |
| 150 | canvas.blend(origin_x + left + gx, baseline - top + gy, fg, a); |
| 151 | } |
| 152 | } |
| 153 | } |
| 154 | } |
| 155 | GlyphData::Color(bgra) => { |
| 156 | for gy in 0..h { |
| 157 | for gx in 0..w { |
| 158 | let i = ((gy * w + gx) * 4) as usize; |
| 159 | canvas.over(origin_x + left + gx, baseline - top + gy, &bgra[i..i + 4]); |
| 160 | } |
| 161 | } |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | /// Resolve a cell's (foreground, background) RGB, applying reverse video, |
| 168 | /// bold-as-bright for the foreground, and hidden. |
| 169 | fn cell_colors(cell: &crate::grid::Cell, cursor: bool) -> (Rgb, Rgb) { |
| 170 | let bold = cell.flags.contains(Flags::BOLD); |
| 171 | let mut fg = resolve(cell.fg, DEFAULT_FG, bold); |
| 172 | let mut bg = resolve(cell.bg, DEFAULT_BG, false); |
| 173 | if cell.flags.contains(Flags::REVERSE) ^ cursor { |
| 174 | std::mem::swap(&mut fg, &mut bg); |
| 175 | } |
| 176 | if cell.flags.contains(Flags::HIDDEN) { |
| 177 | fg = bg; |
| 178 | } |
| 179 | (fg, bg) |
| 180 | } |
| 181 | |
| 182 | fn resolve(color: Color, default: Rgb, bold: bool) -> Rgb { |
| 183 | match color { |
| 184 | Color::Default => default, |
| 185 | Color::Indexed(i) => ansi256(if bold && i < 8 { i + 8 } else { i }), |
| 186 | Color::Rgb(r, g, b) => Rgb(r, g, b), |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | /// The xterm 256-colour palette: 16 base, a 6×6×6 cube, then 24 greys. |
| 191 | fn ansi256(i: u8) -> Rgb { |
| 192 | const BASE: [Rgb; 16] = [ |
| 193 | Rgb(0x00, 0x00, 0x00), |
| 194 | Rgb(0xcd, 0x00, 0x00), |
| 195 | Rgb(0x00, 0xcd, 0x00), |
| 196 | Rgb(0xcd, 0xcd, 0x00), |
| 197 | Rgb(0x00, 0x00, 0xee), |
| 198 | Rgb(0xcd, 0x00, 0xcd), |
| 199 | Rgb(0x00, 0xcd, 0xcd), |
| 200 | Rgb(0xe5, 0xe5, 0xe5), |
| 201 | Rgb(0x7f, 0x7f, 0x7f), |
| 202 | Rgb(0xff, 0x00, 0x00), |
| 203 | Rgb(0x00, 0xff, 0x00), |
| 204 | Rgb(0xff, 0xff, 0x00), |
| 205 | Rgb(0x5c, 0x5c, 0xff), |
| 206 | Rgb(0xff, 0x00, 0xff), |
| 207 | Rgb(0x00, 0xff, 0xff), |
| 208 | Rgb(0xff, 0xff, 0xff), |
| 209 | ]; |
| 210 | match i { |
| 211 | 0..=15 => BASE[i as usize], |
| 212 | 16..=231 => { |
| 213 | let i = i - 16; |
| 214 | Rgb(cube(i / 36), cube((i / 6) % 6), cube(i % 6)) |
| 215 | } |
| 216 | _ => { |
| 217 | let v = 8 + 10 * (i - 232); |
| 218 | Rgb(v, v, v) |
| 219 | } |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | fn cube(step: u8) -> u8 { |
| 224 | if step == 0 { 0 } else { 55 + 40 * step } |
| 225 | } |
| 226 | |
| 227 | #[cfg(test)] |
| 228 | mod tests { |
| 229 | use super::*; |
| 230 | |
| 231 | #[test] |
| 232 | fn palette_cube_and_grey() { |
| 233 | assert_eq!(ansi256(16).0, 0); // cube origin is black |
| 234 | let white = ansi256(231); |
| 235 | assert_eq!((white.0, white.1, white.2), (255, 255, 255)); |
| 236 | assert_eq!(ansi256(232).0, 8); // first grey step |
| 237 | } |
| 238 | } |