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    /// Blend a single byte in the RGBA buffer using the C++ alpha formula.
122    ///
123    /// `*p = (((rgb_val - *p) * alpha) + (*p << 16)) >> 16`
124    #[inline]
125    fn blend_byte(dst: u8, src: u8, alpha: i32) -> u8 {
126        (((src as i32 - dst as i32) * alpha + ((dst as i32) << 16)) >> 16) as u8
127    }
128}
129
130impl<'a> PixelFormat for PixfmtRgba32Lcd<'a> {
131    type ColorType = Rgba8;
132
133    fn width(&self) -> u32 {
134        self.rbuf.width() * 3
135    }
136
137    fn height(&self) -> u32 {
138        self.rbuf.height()
139    }
140
141    fn pixel(&self, x: i32, y: i32) -> Rgba8 {
142        // Map subpixel x to actual pixel
143        let pixel = x as usize / 3;
144        let actual_w = self.actual_width() as usize;
145        if pixel >= actual_w {
146            return Rgba8::new(0, 0, 0, 0);
147        }
148        let row = unsafe {
149            let ptr = self.rbuf.row_ptr(y);
150            std::slice::from_raw_parts(ptr, actual_w * BPP)
151        };
152        let off = pixel * BPP;
153        Rgba8::new(
154            row[off] as u32,
155            row[off + 1] as u32,
156            row[off + 2] as u32,
157            row[off + 3] as u32,
158        )
159    }
160
161    fn copy_pixel(&mut self, x: i32, y: i32, c: &Rgba8) {
162        let pixel = x as usize / 3;
163        let actual_w = self.actual_width() as usize;
164        if pixel >= actual_w {
165            return;
166        }
167        let row = unsafe {
168            let ptr = self.rbuf.row_ptr(y);
169            std::slice::from_raw_parts_mut(ptr, actual_w * BPP)
170        };
171        let off = pixel * BPP;
172        row[off] = c.r;
173        row[off + 1] = c.g;
174        row[off + 2] = c.b;
175        row[off + 3] = c.a;
176    }
177
178    fn copy_hline(&mut self, x: i32, y: i32, len: u32, c: &Rgba8) {
179        // Copy len subpixels — sets whole pixels for each affected pixel
180        let actual_w = self.actual_width() as usize;
181        let row = unsafe {
182            let ptr = self.rbuf.row_ptr(y);
183            std::slice::from_raw_parts_mut(ptr, actual_w * BPP)
184        };
185        for k in 0..len as usize {
186            let sp = x as usize + k;
187            let pixel = sp / 3;
188            let channel = sp % 3;
189            if pixel >= actual_w {
190                break;
191            }
192            let byte_off = pixel * BPP + channel;
193            row[byte_off] = [c.r, c.g, c.b][channel];
194            // Set alpha to 255 when we touch any channel of a pixel
195            row[pixel * BPP + 3] = 255;
196        }
197    }
198
199    fn blend_pixel(&mut self, x: i32, y: i32, c: &Rgba8, cover: CoverType) {
200        let sp = x as usize;
201        let pixel = sp / 3;
202        let channel = sp % 3;
203        let actual_w = self.actual_width() as usize;
204        if pixel >= actual_w {
205            return;
206        }
207        let row = unsafe {
208            let ptr = self.rbuf.row_ptr(y);
209            std::slice::from_raw_parts_mut(ptr, actual_w * BPP)
210        };
211        let byte_off = pixel * BPP + channel;
212        let rgb = [c.r, c.g, c.b];
213        let alpha = cover as i32 * c.a as i32;
214        if alpha != 0 {
215            if alpha == 255 * 255 {
216                row[byte_off] = rgb[channel];
217            } else {
218                row[byte_off] = Self::blend_byte(row[byte_off], rgb[channel], alpha);
219            }
220            row[pixel * BPP + 3] = 255;
221        }
222    }
223
224    fn blend_hline(&mut self, x: i32, y: i32, len: u32, c: &Rgba8, cover: CoverType) {
225        let actual_w = self.actual_width() as usize;
226        let row = unsafe {
227            let ptr = self.rbuf.row_ptr(y);
228            std::slice::from_raw_parts_mut(ptr, actual_w * BPP)
229        };
230        let alpha = cover as i32 * c.a as i32;
231        if alpha == 0 {
232            return;
233        }
234        let rgb = [c.r, c.g, c.b];
235
236        for k in 0..len as usize {
237            let sp = x as usize + k;
238            let pixel = sp / 3;
239            let channel = sp % 3;
240            if pixel >= actual_w {
241                break;
242            }
243            let byte_off = pixel * BPP + channel;
244            if alpha == 255 * 255 {
245                row[byte_off] = rgb[channel];
246            } else {
247                row[byte_off] = Self::blend_byte(row[byte_off], rgb[channel], alpha);
248            }
249            row[pixel * BPP + 3] = 255;
250        }
251    }
252
253    /// LCD subpixel coverage distribution — the core of LCD rendering.
254    ///
255    /// Distributes each coverage value across 5 neighboring subpixel positions
256    /// (tertiary, secondary, primary, secondary, tertiary) using the LUT, then
257    /// blends the distributed coverage into the RGBA buffer.
258    ///
259    /// Exact port of C++ `pixfmt_rgb24_lcd::blend_solid_hspan`, adapted for
260    /// RGBA32 byte layout.
261    fn blend_solid_hspan(
262        &mut self,
263        x: i32,
264        y: i32,
265        len: u32,
266        c: &Rgba8,
267        covers: &[CoverType],
268    ) {
269        let len = len as usize;
270
271        // Step 1: Distribute coverage across 5-tap kernel
272        // Matching C++: c3[i+0] += tertiary, c3[i+1] += secondary,
273        //               c3[i+2] += primary,  c3[i+3] += secondary,
274        //               c3[i+4] += tertiary
275        let dist_len = len + 4;
276        let mut c3 = vec![0u8; dist_len];
277
278        for i in 0..len {
279            let cv = covers[i];
280            c3[i] = c3[i].wrapping_add(self.lut.tertiary(cv));
281            c3[i + 1] = c3[i + 1].wrapping_add(self.lut.secondary(cv));
282            c3[i + 2] = c3[i + 2].wrapping_add(self.lut.primary(cv));
283            c3[i + 3] = c3[i + 3].wrapping_add(self.lut.secondary(cv));
284            c3[i + 4] = c3[i + 4].wrapping_add(self.lut.tertiary(cv));
285        }
286
287        // Step 2: Adjust start position (distribution extends 2 subpixels before)
288        let mut sp_start = x as i32 - 2;
289        let mut c3_offset = 0usize;
290        let mut remaining = dist_len;
291
292        if sp_start < 0 {
293            let skip = (-sp_start) as usize;
294            c3_offset = skip;
295            if skip >= remaining {
296                return;
297            }
298            remaining -= skip;
299            sp_start = 0;
300        }
301
302        // Step 3: Apply distributed covers to RGBA buffer
303        let actual_w = self.actual_width() as usize;
304        let row = unsafe {
305            let ptr = self.rbuf.row_ptr(y);
306            std::slice::from_raw_parts_mut(ptr, actual_w * BPP)
307        };
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 = unsafe {
351            let ptr = self.rbuf.row_ptr(y);
352            std::slice::from_raw_parts_mut(ptr, actual_w * BPP)
353        };
354
355        for k in 0..len as usize {
356            let sp = x as usize + k;
357            let pixel = sp / 3;
358            let channel = sp % 3;
359            if pixel >= actual_w {
360                break;
361            }
362
363            let c = &colors[k];
364            let cov = if !covers.is_empty() {
365                covers[k]
366            } else {
367                cover
368            };
369            let alpha = cov as i32 * c.a as i32;
370            if alpha != 0 {
371                let byte_off = pixel * BPP + channel;
372                let rgb = [c.r, c.g, c.b];
373                if alpha == 255 * 255 {
374                    row[byte_off] = rgb[channel];
375                } else {
376                    row[byte_off] =
377                        Self::blend_byte(row[byte_off], rgb[channel], alpha);
378                }
379                row[pixel * BPP + 3] = 255;
380            }
381        }
382    }
383}
384
385// ============================================================================
386// Tests
387// ============================================================================
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    #[test]
394    fn test_lcd_distribution_lut_construction() {
395        // Default weights from the C++ demo: primary=1/3, secondary=2/9, tertiary=1/9
396        let lut = LcdDistributionLut::new(1.0 / 3.0, 2.0 / 9.0, 1.0 / 9.0);
397
398        // Coverage 0 should distribute to 0
399        assert_eq!(lut.primary(0), 0);
400        assert_eq!(lut.secondary(0), 0);
401        assert_eq!(lut.tertiary(0), 0);
402
403        // Coverage 255 should distribute fully
404        // prim + 2*sec + 2*tert should approximately equal 255
405        let total = lut.primary(255) as u32
406            + 2 * lut.secondary(255) as u32
407            + 2 * lut.tertiary(255) as u32;
408        // Due to floor(), total may be slightly less than 255
409        assert!(total <= 255);
410        assert!(total >= 250, "total distribution = {}", total);
411    }
412
413    #[test]
414    fn test_lcd_distribution_lut_normalization() {
415        // Custom weights that don't sum to 1
416        let lut = LcdDistributionLut::new(3.0, 2.0, 1.0);
417        // After normalization: prim=3/9, sec=2/9, tert=1/9
418        // primary(255) = floor(3/9 * 255) = floor(85) = 85
419        assert_eq!(lut.primary(255), 85);
420    }
421
422    fn make_buffer(w: u32, h: u32) -> (Vec<u8>, RowAccessor) {
423        let stride = (w * BPP as u32) as i32;
424        let buf = vec![255u8; (h * w * BPP as u32) as usize];
425        let mut ra = RowAccessor::new();
426        unsafe {
427            ra.attach(buf.as_ptr() as *mut u8, w, h, stride);
428        }
429        (buf, ra)
430    }
431
432    #[test]
433    fn test_pixfmt_lcd_width_height() {
434        let (_buf, mut ra) = make_buffer(100, 50);
435        let lut = LcdDistributionLut::new(1.0 / 3.0, 2.0 / 9.0, 1.0 / 9.0);
436        let pf = PixfmtRgba32Lcd::new(&mut ra, &lut);
437        assert_eq!(pf.width(), 300); // 100 * 3
438        assert_eq!(pf.height(), 50);
439    }
440
441    #[test]
442    fn test_lcd_blend_solid_hspan_black_on_white() {
443        // Blend black text on white background — should darken the pixels
444        let (_buf, mut ra) = make_buffer(100, 10);
445        let lut = LcdDistributionLut::new(1.0 / 3.0, 2.0 / 9.0, 1.0 / 9.0);
446        let mut pf = PixfmtRgba32Lcd::new(&mut ra, &lut);
447
448        // Full-coverage span of 6 subpixels at position 30 (pixel 10)
449        let covers = [255u8; 6];
450        let black = Rgba8::new(0, 0, 0, 255);
451        pf.blend_solid_hspan(30, 5, 6, &black, &covers);
452
453        // The affected pixels should be darker than 255 (white)
454        let p = pf.pixel(30, 5); // pixel 10
455        assert!(
456            p.r < 255 || p.g < 255 || p.b < 255,
457            "Expected darkened pixel, got {:?}",
458            (p.r, p.g, p.b)
459        );
460    }
461}