Skip to main content

agg_rust/
pixfmt_lcd.rs

1//! LCD subpixel pixel format for RGBA32 buffers.
2//!
3//! Port of `agg_pixfmt_rgb24_lcd.h` — specialized pixel format that performs
4//! LCD subpixel rendering by distributing coverage across the R, G, B channels
5//! of adjacent pixels. Adapted for RGBA32 (4 bytes per pixel) buffers.
6//!
7//! The rasterizer operates at 3x horizontal resolution. Each "subpixel" maps
8//! to one color channel (R, G, or B) of an actual RGBA pixel.
9//!
10//! Copyright (c) 2025. BSD-3-Clause License.
11
12use crate::basics::CoverType;
13use crate::color::Rgba8;
14use crate::pixfmt_rgba::PixelFormat;
15use crate::rendering_buffer::RowAccessor;
16
17// ============================================================================
18// LcdDistributionLut
19// ============================================================================
20
21/// Lookup table for LCD subpixel coverage distribution.
22///
23/// Distributes each coverage value across primary (center), secondary
24/// (adjacent), and tertiary (2-away) positions. This implements the
25/// energy distribution described in Steve Gibson's subpixel rendering guide.
26///
27/// Port of C++ `lcd_distribution_lut` from `agg_pixfmt_rgb24_lcd.h`.
28pub struct LcdDistributionLut {
29    primary_lut: [u8; 256],
30    secondary_lut: [u8; 256],
31    tertiary_lut: [u8; 256],
32}
33
34impl LcdDistributionLut {
35    /// Create a new LCD distribution lookup table.
36    ///
37    /// # Arguments
38    /// * `prim` — Weight for the primary (center) subpixel
39    /// * `second` — Weight for the secondary (adjacent) subpixels
40    /// * `tert` — Weight for the tertiary (2-away) subpixels
41    ///
42    /// Weights are normalized so that `prim + 2*second + 2*tert = 1.0`.
43    pub fn new(prim: f64, second: f64, tert: f64) -> Self {
44        let norm = 1.0 / (prim + second * 2.0 + tert * 2.0);
45        let prim = prim * norm;
46        let second = second * norm;
47        let tert = tert * norm;
48
49        let mut primary_lut = [0u8; 256];
50        let mut secondary_lut = [0u8; 256];
51        let mut tertiary_lut = [0u8; 256];
52
53        for i in 0..256 {
54            primary_lut[i] = (prim * i as f64).floor() as u8;
55            secondary_lut[i] = (second * i as f64).floor() as u8;
56            tertiary_lut[i] = (tert * i as f64).floor() as u8;
57        }
58
59        Self {
60            primary_lut,
61            secondary_lut,
62            tertiary_lut,
63        }
64    }
65
66    /// Get the primary (center) distribution for a coverage value.
67    #[inline]
68    pub fn primary(&self, v: u8) -> u8 {
69        self.primary_lut[v as usize]
70    }
71
72    /// Get the secondary (adjacent) distribution for a coverage value.
73    #[inline]
74    pub fn secondary(&self, v: u8) -> u8 {
75        self.secondary_lut[v as usize]
76    }
77
78    /// Get the tertiary (2-away) distribution for a coverage value.
79    #[inline]
80    pub fn tertiary(&self, v: u8) -> u8 {
81        self.tertiary_lut[v as usize]
82    }
83}
84
85// ============================================================================
86// PixfmtRgba32Lcd
87// ============================================================================
88
89const BPP: usize = 4; // bytes per pixel in RGBA32
90
91/// LCD subpixel pixel format for RGBA32 rendering buffers.
92///
93/// Adapted from C++ `pixfmt_rgb24_lcd` for RGBA32 (4 bytes per pixel) buffers.
94/// Reports width as `actual_width * 3` so the rasterizer operates at 3x
95/// horizontal resolution. Each "subpixel" maps to one R, G, or B channel
96/// of an actual RGBA pixel:
97///
98/// - Subpixel `sp` → pixel `sp / 3`, channel `sp % 3` (0=R, 1=G, 2=B)
99/// - Byte offset: `(sp / 3) * 4 + (sp % 3)`
100///
101/// The `blend_solid_hspan` method distributes each coverage value across
102/// 5 neighboring subpixels (tertiary, secondary, primary, secondary, tertiary)
103/// using the `LcdDistributionLut`, matching the C++ implementation exactly.
104pub struct PixfmtRgba32Lcd<'a> {
105    rbuf: &'a mut RowAccessor,
106    lut: &'a LcdDistributionLut,
107}
108
109impl<'a> PixfmtRgba32Lcd<'a> {
110    /// Create a new LCD pixel format wrapping an RGBA32 rendering buffer.
111    pub fn new(rbuf: &'a mut RowAccessor, lut: &'a LcdDistributionLut) -> Self {
112        Self { rbuf, lut }
113    }
114
115    /// Get the actual (non-subpixel) width.
116    #[inline]
117    fn actual_width(&self) -> u32 {
118        self.rbuf.width()
119    }
120
121    /// Row of pixels at `y` as a byte slice (shared, read-only view).
122    #[inline]
123    fn row(&self, y: i32) -> &[u8] {
124        unsafe {
125            let ptr = self.rbuf.row_ptr(y);
126            std::slice::from_raw_parts(ptr, self.rbuf.width() as usize * BPP)
127        }
128    }
129
130    /// Row of pixels at `y` as a mutable byte slice.
131    #[inline]
132    fn row_mut(&mut self, y: i32) -> &mut [u8] {
133        unsafe {
134            let ptr = self.rbuf.row_ptr(y);
135            std::slice::from_raw_parts_mut(ptr, self.rbuf.width() as usize * BPP)
136        }
137    }
138
139    /// Blend a single byte in the RGBA buffer using the C++ alpha formula.
140    ///
141    /// `*p = (((rgb_val - *p) * alpha) + (*p << 16)) >> 16`
142    #[inline]
143    fn blend_byte(dst: u8, src: u8, alpha: i32) -> u8 {
144        (((src as i32 - dst as i32) * alpha + ((dst as i32) << 16)) >> 16) as u8
145    }
146}
147
148impl<'a> PixelFormat for PixfmtRgba32Lcd<'a> {
149    type ColorType = Rgba8;
150
151    fn width(&self) -> u32 {
152        self.rbuf.width() * 3
153    }
154
155    fn height(&self) -> u32 {
156        self.rbuf.height()
157    }
158
159    fn pixel(&self, x: i32, y: i32) -> Rgba8 {
160        // Map subpixel x to actual pixel
161        let pixel = x as usize / 3;
162        let actual_w = self.actual_width() as usize;
163        if pixel >= actual_w {
164            return Rgba8::new(0, 0, 0, 0);
165        }
166        let row = self.row(y);
167        let off = pixel * BPP;
168        Rgba8::new(
169            row[off] as u32,
170            row[off + 1] as u32,
171            row[off + 2] as u32,
172            row[off + 3] as u32,
173        )
174    }
175
176    fn copy_pixel(&mut self, x: i32, y: i32, c: &Rgba8) {
177        let pixel = x as usize / 3;
178        let actual_w = self.actual_width() as usize;
179        if pixel >= actual_w {
180            return;
181        }
182        let row = self.row_mut(y);
183        let off = pixel * BPP;
184        row[off] = c.r;
185        row[off + 1] = c.g;
186        row[off + 2] = c.b;
187        row[off + 3] = c.a;
188    }
189
190    fn copy_hline(&mut self, x: i32, y: i32, len: u32, c: &Rgba8) {
191        // Copy len subpixels — sets whole pixels for each affected pixel
192        let actual_w = self.actual_width() as usize;
193        let row = self.row_mut(y);
194        for k in 0..len as usize {
195            let sp = x as usize + k;
196            let pixel = sp / 3;
197            let channel = sp % 3;
198            if pixel >= actual_w {
199                break;
200            }
201            let byte_off = pixel * BPP + channel;
202            row[byte_off] = [c.r, c.g, c.b][channel];
203            // Set alpha to 255 when we touch any channel of a pixel
204            row[pixel * BPP + 3] = 255;
205        }
206    }
207
208    fn blend_pixel(&mut self, x: i32, y: i32, c: &Rgba8, cover: CoverType) {
209        let sp = x as usize;
210        let pixel = sp / 3;
211        let channel = sp % 3;
212        let actual_w = self.actual_width() as usize;
213        if pixel >= actual_w {
214            return;
215        }
216        let row = self.row_mut(y);
217        let byte_off = pixel * BPP + channel;
218        let rgb = [c.r, c.g, c.b];
219        let alpha = cover as i32 * c.a as i32;
220        if alpha != 0 {
221            if alpha == 255 * 255 {
222                row[byte_off] = rgb[channel];
223            } else {
224                row[byte_off] = Self::blend_byte(row[byte_off], rgb[channel], alpha);
225            }
226            row[pixel * BPP + 3] = 255;
227        }
228    }
229
230    fn blend_hline(&mut self, x: i32, y: i32, len: u32, c: &Rgba8, cover: CoverType) {
231        let actual_w = self.actual_width() as usize;
232        let row = self.row_mut(y);
233        let alpha = cover as i32 * c.a as i32;
234        if alpha == 0 {
235            return;
236        }
237        let rgb = [c.r, c.g, c.b];
238
239        for k in 0..len as usize {
240            let sp = x as usize + k;
241            let pixel = sp / 3;
242            let channel = sp % 3;
243            if pixel >= actual_w {
244                break;
245            }
246            let byte_off = pixel * BPP + channel;
247            if alpha == 255 * 255 {
248                row[byte_off] = rgb[channel];
249            } else {
250                row[byte_off] = Self::blend_byte(row[byte_off], rgb[channel], alpha);
251            }
252            row[pixel * BPP + 3] = 255;
253        }
254    }
255
256    /// LCD subpixel coverage distribution — the core of LCD rendering.
257    ///
258    /// Distributes each coverage value across 5 neighboring subpixel positions
259    /// (tertiary, secondary, primary, secondary, tertiary) using the LUT, then
260    /// blends the distributed coverage into the RGBA buffer.
261    ///
262    /// Exact port of C++ `pixfmt_rgb24_lcd::blend_solid_hspan`, adapted for
263    /// RGBA32 byte layout.
264    fn blend_solid_hspan(
265        &mut self,
266        x: i32,
267        y: i32,
268        len: u32,
269        c: &Rgba8,
270        covers: &[CoverType],
271    ) {
272        let len = len as usize;
273
274        // Step 1: Distribute coverage across 5-tap kernel
275        // Matching C++: c3[i+0] += tertiary, c3[i+1] += secondary,
276        //               c3[i+2] += primary,  c3[i+3] += secondary,
277        //               c3[i+4] += tertiary
278        let dist_len = len + 4;
279        let mut c3 = vec![0u8; dist_len];
280
281        for i in 0..len {
282            let cv = covers[i];
283            c3[i] = c3[i].wrapping_add(self.lut.tertiary(cv));
284            c3[i + 1] = c3[i + 1].wrapping_add(self.lut.secondary(cv));
285            c3[i + 2] = c3[i + 2].wrapping_add(self.lut.primary(cv));
286            c3[i + 3] = c3[i + 3].wrapping_add(self.lut.secondary(cv));
287            c3[i + 4] = c3[i + 4].wrapping_add(self.lut.tertiary(cv));
288        }
289
290        // Step 2: Adjust start position (distribution extends 2 subpixels before)
291        let mut sp_start = x as i32 - 2;
292        let mut c3_offset = 0usize;
293        let mut remaining = dist_len;
294
295        if sp_start < 0 {
296            let skip = (-sp_start) as usize;
297            c3_offset = skip;
298            if skip >= remaining {
299                return;
300            }
301            remaining -= skip;
302            sp_start = 0;
303        }
304
305        // Step 3: Apply distributed covers to RGBA buffer
306        let actual_w = self.actual_width() as usize;
307        let row = self.row_mut(y);
308
309        let rgb = [c.r, c.g, c.b];
310        // Channel cycling: which RGB channel does sp_start map to?
311        // Matching C++: i = x % 3 (after x -= 2)
312        // sp_start % 3 gives us the starting channel
313
314        for k in 0..remaining {
315            let sp = sp_start as usize + k;
316            let pixel = sp / 3;
317            let channel = sp % 3;
318
319            if pixel >= actual_w {
320                break;
321            }
322
323            let cover = c3[c3_offset + k];
324            let alpha = cover as i32 * c.a as i32;
325
326            if alpha != 0 {
327                let byte_off = pixel * BPP + channel;
328                if alpha == 255 * 255 {
329                    row[byte_off] = rgb[channel];
330                } else {
331                    row[byte_off] =
332                        Self::blend_byte(row[byte_off], rgb[channel], alpha);
333                }
334                // Ensure alpha channel is opaque for any touched pixel
335                row[pixel * BPP + 3] = 255;
336            }
337        }
338    }
339
340    fn blend_color_hspan(
341        &mut self,
342        x: i32,
343        y: i32,
344        len: u32,
345        colors: &[Rgba8],
346        covers: &[CoverType],
347        cover: CoverType,
348    ) {
349        let actual_w = self.actual_width() as usize;
350        let row = self.row_mut(y);
351
352        for k in 0..len as usize {
353            let sp = x as usize + k;
354            let pixel = sp / 3;
355            let channel = sp % 3;
356            if pixel >= actual_w {
357                break;
358            }
359
360            let c = &colors[k];
361            let cov = if !covers.is_empty() {
362                covers[k]
363            } else {
364                cover
365            };
366            let alpha = cov as i32 * c.a as i32;
367            if alpha != 0 {
368                let byte_off = pixel * BPP + channel;
369                let rgb = [c.r, c.g, c.b];
370                if alpha == 255 * 255 {
371                    row[byte_off] = rgb[channel];
372                } else {
373                    row[byte_off] =
374                        Self::blend_byte(row[byte_off], rgb[channel], alpha);
375                }
376                row[pixel * BPP + 3] = 255;
377            }
378        }
379    }
380}
381
382// ============================================================================
383// Tests
384// ============================================================================
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    #[test]
391    fn test_lcd_distribution_lut_construction() {
392        // Default weights from the C++ demo: primary=1/3, secondary=2/9, tertiary=1/9
393        let lut = LcdDistributionLut::new(1.0 / 3.0, 2.0 / 9.0, 1.0 / 9.0);
394
395        // Coverage 0 should distribute to 0
396        assert_eq!(lut.primary(0), 0);
397        assert_eq!(lut.secondary(0), 0);
398        assert_eq!(lut.tertiary(0), 0);
399
400        // Coverage 255 should distribute fully
401        // prim + 2*sec + 2*tert should approximately equal 255
402        let total = lut.primary(255) as u32
403            + 2 * lut.secondary(255) as u32
404            + 2 * lut.tertiary(255) as u32;
405        // Due to floor(), total may be slightly less than 255
406        assert!(total <= 255);
407        assert!(total >= 250, "total distribution = {}", total);
408    }
409
410    #[test]
411    fn test_lcd_distribution_lut_normalization() {
412        // Custom weights that don't sum to 1
413        let lut = LcdDistributionLut::new(3.0, 2.0, 1.0);
414        // After normalization: prim=3/9, sec=2/9, tert=1/9
415        // primary(255) = floor(3/9 * 255) = floor(85) = 85
416        assert_eq!(lut.primary(255), 85);
417    }
418
419    fn make_buffer(w: u32, h: u32) -> (Vec<u8>, RowAccessor) {
420        let stride = (w * BPP as u32) as i32;
421        let buf = vec![255u8; (h * w * BPP as u32) as usize];
422        let mut ra = RowAccessor::new();
423        unsafe {
424            ra.attach(buf.as_ptr() as *mut u8, w, h, stride);
425        }
426        (buf, ra)
427    }
428
429    #[test]
430    fn test_pixfmt_lcd_width_height() {
431        let (_buf, mut ra) = make_buffer(100, 50);
432        let lut = LcdDistributionLut::new(1.0 / 3.0, 2.0 / 9.0, 1.0 / 9.0);
433        let pf = PixfmtRgba32Lcd::new(&mut ra, &lut);
434        assert_eq!(pf.width(), 300); // 100 * 3
435        assert_eq!(pf.height(), 50);
436    }
437
438    #[test]
439    fn test_lcd_blend_solid_hspan_black_on_white() {
440        // Blend black text on white background — should darken the pixels
441        let (_buf, mut ra) = make_buffer(100, 10);
442        let lut = LcdDistributionLut::new(1.0 / 3.0, 2.0 / 9.0, 1.0 / 9.0);
443        let mut pf = PixfmtRgba32Lcd::new(&mut ra, &lut);
444
445        // Full-coverage span of 6 subpixels at position 30 (pixel 10)
446        let covers = [255u8; 6];
447        let black = Rgba8::new(0, 0, 0, 255);
448        pf.blend_solid_hspan(30, 5, 6, &black, &covers);
449
450        // The affected pixels should be darker than 255 (white)
451        let p = pf.pixel(30, 5); // pixel 10
452        assert!(
453            p.r < 255 || p.g < 255 || p.b < 255,
454            "Expected darkened pixel, got {:?}",
455            (p.r, p.g, p.b)
456        );
457    }
458}