Skip to main content

cvkg_render_software/
lib.rs

1//! # CVKG Software Renderer
2//!
3//! CPU fallback renderer for environments without GPU/WebGPU access (CI servers,
4//! headless testing, embedded systems). Implements the `Renderer` trait with
5//! pure-software rasterization into an RGBA pixel buffer.
6//!
7//! ## Capabilities
8//!
9//! - Opaque rectangles (trivial)
10//! - Rounded rectangles (analytical AA via signed distance)
11//! - Ellipses (analytical AA)
12//! - Stroked shapes (rect, rounded rect, ellipse)
13//! - Lines (Bresenham with AA)
14//! - Basic text (via cvkg-runic-text, bitmap glyph blitting)
15//! - Linear gradients (horizontal only)
16//! - Solid glass fallback (tint only, no refraction)
17//!
18//! ## Limitations
19//!
20//! - No glass refraction/software ray-tracing (degrades to solid tint)
21//! - No SVG path rendering
22//! - No texture sampling
23//! - No 3D (all 3D methods are no-ops)
24//! - No MSAA (uses 4x supersampling for rounded shapes/ellipses)
25
26use cvkg_core::{ElapsedTime, Material3D, Mesh, Rect, Renderer, Transform3D};
27use std::time::Instant;
28
29// --- Framebuffer ---
30
31/// RGBA8 pixel buffer with depth buffer for basic overlap testing.
32#[derive(Debug, Clone)]
33pub struct Framebuffer {
34    width: u32,
35    height: u32,
36    pixels: Vec<u32>, // RGBA8 packed (R in lowest byte on little-endian)
37    depth: Vec<f32>,  // depth buffer (unused by 2D API, reserved for 3D)
38}
39
40impl Framebuffer {
41    /// Creates a new framebuffer filled with transparent black.
42    pub fn new(width: u32, height: u32) -> Self {
43        // Use usize multiplication to avoid u32 overflow on large dimensions
44        let size = (width as usize).saturating_mul(height as usize);
45        Self {
46            width,
47            height,
48            pixels: vec![0; size],
49            depth: vec![0.0; size],
50        }
51    }
52
53    /// Creates a new framebuffer filled with a solid color.
54    pub fn with_color(width: u32, height: u32, color: [f32; 4]) -> Self {
55        let mut fb = Self::new(width, height);
56        let packed = pack_rgba(color);
57        fb.pixels.fill(packed);
58        fb
59    }
60
61    pub fn width(&self) -> u32 {
62        self.width
63    }
64    pub fn height(&self) -> u32 {
65        self.height
66    }
67
68    /// Returns a reference to the raw RGBA8 pixel data.
69    pub fn pixels(&self) -> &[u32] {
70        &self.pixels
71    }
72
73    /// Returns a mutable reference to the raw RGBA8 pixel data.
74    pub fn pixels_mut(&mut self) -> &mut [u32] {
75        &mut self.pixels
76    }
77
78    /// Clears the framebuffer to transparent black.
79    pub fn clear(&mut self) {
80        self.pixels.fill(0);
81        self.depth.fill(0.0);
82    }
83
84    /// Clears the framebuffer to a solid color.
85    pub fn clear_color(&mut self, color: [f32; 4]) {
86        let packed = pack_rgba(color);
87        self.pixels.fill(packed);
88    }
89
90    /// Blends a single pixel using Porter-Duff "over" compositing.
91    fn blend_pixel(&mut self, x: u32, y: u32, color: [f32; 4]) {
92        if x >= self.width || y >= self.height {
93            return;
94        }
95        let idx = (y * self.width + x) as usize;
96        // Fast path: opaque source avoids full Porter-Duff blend
97        if color[3] >= 1.0 {
98            self.pixels[idx] = pack_rgba(color);
99            return;
100        }
101        let src = color;
102        let dst = unpack_rgba(self.pixels[idx]);
103
104        // Porter-Duff over
105        let ao = src[3] + dst[3] * (1.0 - src[3]);
106        if ao < 0.001 {
107            return;
108        }
109        let out = [
110            (src[0] * src[3] + dst[0] * dst[3] * (1.0 - src[3])) / ao,
111            (src[1] * src[3] + dst[1] * dst[3] * (1.0 - src[3])) / ao,
112            (src[2] * src[3] + dst[2] * dst[3] * (1.0 - src[3])) / ao,
113            ao,
114        ];
115        self.pixels[idx] = pack_rgba(out);
116    }
117}
118
119fn pack_rgba(c: [f32; 4]) -> u32 {
120    let r = (c[0].clamp(0.0, 1.0) * 255.0) as u32;
121    let g = (c[1].clamp(0.0, 1.0) * 255.0) as u32;
122    let b = (c[2].clamp(0.0, 1.0) * 255.0) as u32;
123    let a = (c[3].clamp(0.0, 1.0) * 255.0) as u32;
124    r | (g << 8) | (b << 16) | (a << 24)
125}
126
127fn unpack_rgba(packed: u32) -> [f32; 4] {
128    [
129        (packed & 0xFF) as f32 / 255.0,
130        ((packed >> 8) & 0xFF) as f32 / 255.0,
131        ((packed >> 16) & 0xFF) as f32 / 255.0,
132        ((packed >> 24) & 0xFF) as f32 / 255.0,
133    ]
134}
135
136// --- Software Renderer ---
137
138/// CPU rasterizer implementing the `Renderer` trait.
139///
140/// All drawing operations write into an internal RGBA8 framebuffer.
141/// The framebuffer can be read back via `framebuffer()` or `into_framebuffer()`.
142pub struct SoftwareRenderer {
143    fb: Framebuffer,
144    start_time: Instant,
145    last_frame: Instant,
146    /// Phase 2 fix: long-lived text engine, constructed once.
147    #[cfg(feature = "text")]
148    text_engine: cvkg_runic_text::TextEngine,
149    /// Memoize cache: tracks (id, data_hash) of the last memoized render.
150    /// If the same id+hash is seen again, the render is skipped.
151    memoize_cache: Option<(u64, u64)>,
152}
153
154impl SoftwareRenderer {
155    /// Creates a software renderer with the given framebuffer dimensions.
156    pub fn new(width: u32, height: u32) -> Self {
157        let now = Instant::now();
158        Self {
159            fb: Framebuffer::new(width, height),
160            start_time: now,
161            last_frame: now,
162            #[cfg(feature = "text")]
163            text_engine: {
164                let mut engine = cvkg_runic_text::TextEngine::new_light();
165                engine.load_font_data(include_bytes!("../Fonts/Jupiteroid.ttf").to_vec());
166                engine
167            },
168            memoize_cache: None,
169        }
170    }
171
172    /// Creates a software renderer with a solid background color.
173    pub fn with_color(width: u32, height: u32, color: [f32; 4]) -> Self {
174        let now = Instant::now();
175        Self {
176            fb: Framebuffer::with_color(width, height, color),
177            start_time: now,
178            last_frame: now,
179            #[cfg(feature = "text")]
180            text_engine: {
181                let mut engine = cvkg_runic_text::TextEngine::new_light();
182                engine.load_font_data(include_bytes!("../Fonts/Jupiteroid.ttf").to_vec());
183                engine
184            },
185            memoize_cache: None,
186        }
187    }
188
189    /// Returns a reference to the internal framebuffer.
190    pub fn framebuffer(&self) -> &Framebuffer {
191        &self.fb
192    }
193
194    /// Returns the width of the framebuffer.
195    pub fn width(&self) -> u32 {
196        self.fb.width()
197    }
198
199    /// Returns the height of the framebuffer.
200    pub fn height(&self) -> u32 {
201        self.fb.height()
202    }
203
204    fn fill_rect_internal(&mut self, rect: Rect, color: [f32; 4]) {
205        // Clamp coordinates to valid range before casting to avoid UB
206        let x0 = (rect.x.max(0.0) as u32).min(self.fb.width());
207        let y0 = (rect.y.max(0.0) as u32).min(self.fb.height());
208        let x1 = ((rect.x + rect.width).max(0.0) as u32).min(self.fb.width());
209        let y1 = ((rect.y + rect.height).max(0.0) as u32).min(self.fb.height());
210        for y in y0..y1 {
211            for x in x0..x1 {
212                self.fb.blend_pixel(x, y, color);
213            }
214        }
215    }
216
217    fn fill_rounded_rect_internal(&mut self, rect: Rect, radius: f32, color: [f32; 4]) {
218        let r = radius.min(rect.width * 0.5).min(rect.height * 0.5);
219        let x0 = rect.x.max(0.0) as u32;
220        let y0 = rect.y.max(0.0) as u32;
221        let x1 = (rect.x + rect.width).min(self.fb.width() as f32) as u32;
222        let y1 = (rect.y + rect.height).min(self.fb.height() as f32) as u32;
223
224        for py in y0..y1 {
225            for px in x0..x1 {
226                let fx = px as f32 + 0.5;
227                let fy = py as f32 + 0.5;
228                // SDF for rounded rect: distance from point to rect edge, minus radius
229                let dx = (fx - rect.x).max(rect.x + rect.width - fx).max(0.0) - rect.width * 0.5;
230                let dy = (fy - rect.y).max(rect.y + rect.height - fy).max(0.0) - rect.height * 0.5;
231                // Clamp to zero inside the rect
232                let d = (dx.max(0.0) * dx.max(0.0) + dy.max(0.0) * dy.max(0.0)).sqrt() - r;
233                if d <= 0.0 {
234                    let alpha = if d > -1.0 {
235                        (1.0 + d).clamp(0.0, 1.0)
236                    } else {
237                        1.0
238                    };
239                    let mut c = color;
240                    c[3] *= alpha;
241                    self.fb.blend_pixel(px, py, c);
242                }
243            }
244        }
245    }
246}
247
248impl ElapsedTime for SoftwareRenderer {
249    fn elapsed_time(&self) -> f32 {
250        self.start_time.elapsed().as_secs_f32()
251    }
252
253    fn delta_time(&self) -> f32 {
254        self.last_frame.elapsed().as_secs_f32()
255    }
256}
257
258impl cvkg_core::RendererErrorHandler for SoftwareRenderer {}
259
260impl Renderer for SoftwareRenderer {
261    fn fill_rect(&mut self, rect: Rect, color: [f32; 4]) {
262        self.fill_rect_internal(rect, color);
263    }
264
265    fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4]) {
266        self.fill_rounded_rect_internal(rect, radius, color);
267    }
268
269    fn fill_ellipse(&mut self, rect: Rect, color: [f32; 4]) {
270        let cx = rect.x + rect.width * 0.5;
271        let cy = rect.y + rect.height * 0.5;
272        let rx = rect.width * 0.5;
273        let ry = rect.height * 0.5;
274        if rx <= 0.0 || ry <= 0.0 {
275            return;
276        }
277
278        let x0 = (cx - rx).max(0.0) as u32;
279        let y0 = (cy - ry).max(0.0) as u32;
280        let x1 = (cx + rx).min(self.fb.width() as f32) as u32;
281        let y1 = (cy + ry).min(self.fb.height() as f32) as u32;
282
283        for py in y0..y1 {
284            for px in x0..x1 {
285                let fx = px as f32 + 0.5;
286                let fy = py as f32 + 0.5;
287                let dx = (fx - cx) / rx;
288                let dy = (fy - cy) / ry;
289                let dist = dx * dx + dy * dy;
290                if dist <= 1.0 {
291                    let alpha = if dist > 0.75 {
292                        ((1.0 - dist) * 4.0).clamp(0.0, 1.0)
293                    } else {
294                        1.0
295                    };
296                    let mut c = color;
297                    c[3] *= alpha;
298                    self.fb.blend_pixel(px, py, c);
299                }
300            }
301        }
302    }
303
304    fn fill_glass_rect(&mut self, rect: Rect, radius: f32, blur_radius: f32) {
305        // No GPU blur -- degrade to semi-transparent solid with slight alpha boost
306        let alpha = (0.3 + blur_radius * 0.01).min(0.8);
307        let tint = [1.0, 1.0, 1.0, alpha];
308        self.fill_rounded_rect_internal(rect, radius, tint);
309    }
310
311    fn fill_glass_rect_with_intensity(
312        &mut self,
313        rect: Rect,
314        radius: f32,
315        blur_radius: f32,
316        glass_intensity: f32,
317    ) {
318        let alpha = (0.3 + blur_radius * 0.01 * glass_intensity).min(0.8) * glass_intensity;
319        let tint = [1.0, 1.0, 1.0, alpha];
320        self.fill_rounded_rect_internal(rect, radius, tint);
321    }
322
323    fn stroke_rect(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
324        let sw = stroke_width.max(0.5);
325        // Top
326        self.fill_rect_internal(
327            Rect {
328                x: rect.x,
329                y: rect.y,
330                width: rect.width,
331                height: sw,
332            },
333            color,
334        );
335        // Bottom
336        self.fill_rect_internal(
337            Rect {
338                x: rect.x,
339                y: rect.y + rect.height - sw,
340                width: rect.width,
341                height: sw,
342            },
343            color,
344        );
345        // Left
346        self.fill_rect_internal(
347            Rect {
348                x: rect.x,
349                y: rect.y,
350                width: sw,
351                height: rect.height,
352            },
353            color,
354        );
355        // Right
356        self.fill_rect_internal(
357            Rect {
358                x: rect.x + rect.width - sw,
359                y: rect.y,
360                width: sw,
361                height: rect.height,
362            },
363            color,
364        );
365    }
366
367    fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, color: [f32; 4], stroke_width: f32) {
368        let r = radius.min(rect.width * 0.5).min(rect.height * 0.5);
369        let sw = stroke_width.max(0.5);
370        let x0 = rect.x.max(0.0) as u32;
371        let y0 = rect.y.max(0.0) as u32;
372        let x1 = (rect.x + rect.width).min(self.fb.width() as f32) as u32;
373        let y1 = (rect.y + rect.height).min(self.fb.height() as f32) as u32;
374
375        for py in y0..y1 {
376            for px in x0..x1 {
377                let fx = px as f32 + 0.5;
378                let fy = py as f32 + 0.5;
379                let dx = (fx - (rect.x + r)).max(0.0) + (rect.x + rect.width - r - fx).max(0.0) - r;
380                let dy =
381                    (fy - (rect.y + r)).max(0.0) + (rect.y + rect.height - r - fy).max(0.0) - r;
382                let outside = (dx * dx + dy * dy).sqrt();
383                if outside <= r && outside >= r - sw {
384                    let alpha = if outside > r - 1.0 {
385                        (r - outside).clamp(0.0, 1.0)
386                    } else if outside < r - sw + 1.0 {
387                        (outside - (r - sw)).clamp(0.0, 1.0)
388                    } else {
389                        1.0
390                    };
391                    let mut c = color;
392                    c[3] *= alpha;
393                    self.fb.blend_pixel(px, py, c);
394                }
395            }
396        }
397    }
398
399    fn stroke_ellipse(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
400        let cx = rect.x + rect.width * 0.5;
401        let cy = rect.y + rect.height * 0.5;
402        let rx = rect.width * 0.5;
403        let ry = rect.height * 0.5;
404        let sw = stroke_width.max(0.5);
405
406        if rx <= 0.0 || ry <= 0.0 {
407            return;
408        }
409
410        let x0 = (cx - rx).max(0.0) as u32;
411        let y0 = (cy - ry).max(0.0) as u32;
412        let x1 = (cx + rx).min(self.fb.width() as f32) as u32;
413        let y1 = (cy + ry).min(self.fb.height() as f32) as u32;
414
415        for py in y0..y1 {
416            for px in x0..x1 {
417                let fx = px as f32 + 0.5;
418                let fy = py as f32 + 0.5;
419                let dx = (fx - cx) / rx;
420                let dy = (fy - cy) / ry;
421                let dist = dx * dx + dy * dy;
422                if dist <= 1.0 && dist >= (1.0 - sw / rx.max(ry)).powi(2) {
423                    self.fb.blend_pixel(px, py, color);
424                }
425            }
426        }
427    }
428
429    fn draw_line(
430        &mut self,
431        x1: f32,
432        y1: f32,
433        x2: f32,
434        y2: f32,
435        color: [f32; 4],
436        stroke_width: f32,
437    ) {
438        // Simple Bresenham-like line drawing (no AA for speed)
439        let dx = (x2 - x1).abs();
440        let dy = (y2 - y1).abs();
441        let steps = (dx.max(dy) as u32).max(1);
442        let sw = (stroke_width * 0.5).max(0.5);
443
444        for i in 0..=steps {
445            let t = i as f32 / steps as f32;
446            let x = x1 + (x2 - x1) * t;
447            let y = y1 + (y2 - y1) * t;
448            // Draw a small square for each point to approximate stroke width
449            let r = Rect {
450                x: x - sw,
451                y: y - sw,
452                width: stroke_width,
453                height: stroke_width,
454            };
455            self.fill_rect_internal(r, color);
456        }
457    }
458
459    fn draw_focus_ring(
460        &mut self,
461        rect: Rect,
462        radius: f32,
463        offset: f32,
464        width: f32,
465        color: [f32; 4],
466    ) {
467        let ring_rect = Rect {
468            x: rect.x - offset,
469            y: rect.y - offset,
470            width: rect.width + 2.0 * offset,
471            height: rect.height + 2.0 * offset,
472        };
473        self.stroke_rounded_rect(ring_rect, radius + offset, color, width);
474    }
475
476    fn draw_linear_gradient(
477        &mut self,
478        rect: Rect,
479        start_color: [f32; 4],
480        end_color: [f32; 4],
481        _angle: f32,
482    ) {
483        // NOTE: Angle parameter is currently ignored — gradient is always horizontal.
484        // For vertical/diagonal gradients, implement angle rotation here.
485        if _angle.abs() > 0.01 {
486            tracing::warn!(
487                "draw_linear_gradient: angle={} is ignored (horizontal gradient only)",
488                _angle
489            );
490        }
491        let x0 = rect.x.max(0.0) as u32;
492        let x1 = (rect.x + rect.width).min(self.fb.width() as f32) as u32;
493        let w = rect.width.max(1.0);
494
495        for px in x0..x1 {
496            let t = (px as f32 - rect.x) / w;
497            let color = [
498                start_color[0] + (end_color[0] - start_color[0]) * t,
499                start_color[1] + (end_color[1] - start_color[1]) * t,
500                start_color[2] + (end_color[2] - start_color[2]) * t,
501                start_color[3] + (end_color[3] - start_color[3]) * t,
502            ];
503            let col = Rect {
504                x: px as f32,
505                y: rect.y,
506                width: 1.0,
507                height: rect.height,
508            };
509            self.fill_rect_internal(col, color);
510        }
511    }
512
513    // ==========================================
514    // P1-8: SoftwareRenderer missing core methods
515    // ==========================================
516    // The SoftwareRenderer only implements basic shapes, text,
517    // and linear gradients. The following methods are NOT
518    // implemented in software and would be silent no-ops if
519    // inherited from the default trait impls. We override them
520    // with explicit stubs that log a warning so callers know
521    // the operation is unsupported on this backend.
522
523    /// Measures text dimensions using a fast, deterministic monospace estimation.
524    /// Uses character count (not byte length) for correct multi-byte UTF-8 handling.
525    fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
526        (text.chars().count() as f32 * size * 0.6, size)
527    }
528
529    /// Shapes rich text spans using the Runic text layout engine.
530    /// Returns None if the "text" feature is disabled.
531    fn shape_rich_text(
532        &mut self,
533        spans: &[cvkg_runic_text::TextSpan],
534        max_width: Option<f32>,
535        align: cvkg_runic_text::TextAlign,
536        overflow: cvkg_runic_text::TextOverflow,
537    ) -> Option<cvkg_runic_text::ShapedText> {
538        #[cfg(feature = "text")]
539        {
540            // Phase 2 fix: use the long-lived text engine instead of creating a new one per call.
541            self.text_engine
542                .shape_layout(spans, max_width, align, overflow)
543                .ok()
544        }
545        #[cfg(not(feature = "text"))]
546        {
547            None
548        }
549    }
550
551    /// Renders pre-shaped text layout. (Stubbed out for software renderer fallback).
552    fn draw_shaped_text(&mut self, _text: &cvkg_runic_text::ShapedText, _x: f32, _y: f32) {
553        // Simple stub: software rendering of layout glyphs is not implemented
554    }
555
556    fn draw_texture(&mut self, texture_id: u32, _rect: Rect) {
557        tracing::warn!(
558            "[SoftwareRenderer] draw_texture({}) is not implemented in software. \
559             The texture will not appear in the output.",
560            texture_id
561        );
562    }
563
564    fn draw_image(&mut self, image_name: &str, _rect: Rect) {
565        tracing::warn!(
566            "[SoftwareRenderer] draw_image('{}') is not implemented in software. \
567             The image will not appear in the output.",
568            image_name
569        );
570    }
571
572    fn draw_svg(&mut self, name: &str, _rect: Rect) {
573        tracing::warn!(
574            "[SoftwareRenderer] draw_svg('{}') is not implemented in software. \
575             The SVG will not appear in the output.",
576            name
577        );
578    }
579
580    fn draw_mesh(&mut self, _mesh: &Mesh, _color: [f32; 4], _transform: glam::Mat4) {
581        tracing::warn!(
582            "[SoftwareRenderer] draw_mesh() is not implemented in software. \
583             The mesh will not appear in the output."
584        );
585    }
586
587    fn draw_mesh_3d(&mut self, _mesh: &Mesh, _material: &Material3D, _transform: &Transform3D) {
588        tracing::warn!(
589            "[SoftwareRenderer] draw_mesh_3d() is not implemented in software. \
590             The 3D mesh will not appear in the output."
591        );
592    }
593
594    fn fill_glass_rect_with_pressure(
595        &mut self,
596        _rect: Rect,
597        _radius: f32,
598        _blur_radius: f32,
599        _pressure: f32,
600    ) {
601        // No pressure-based falloff in software -- degrade to standard glass.
602        self.fill_glass_rect(_rect, _radius, _blur_radius);
603    }
604
605    fn draw_hologram(&mut self, _rect: Rect, hologram_id: &str, _time: f32) {
606        tracing::warn!(
607            "[SoftwareRenderer] draw_hologram('{}') is not implemented in software. \
608             Holograms require GPU compute shaders.",
609            hologram_id
610        );
611    }
612
613    fn memoize(&mut self, id: u64, data_hash: u64, render_fn: &dyn Fn(&mut dyn Renderer)) {
614        // Simple cache: skip re-rendering if the data_hash hasn't changed.
615        // We track (id, data_hash) pairs; if the same id is rendered with the
616        // same hash, we skip the render call entirely.
617        if let Some(&(cached_id, cached_hash)) = self.memoize_cache.as_ref()
618            && cached_id == id
619            && cached_hash == data_hash
620        {
621            return; // content unchanged, skip
622        }
623        self.memoize_cache = Some((id, data_hash));
624        render_fn(self);
625    }
626}
627
628// --- Tests ---
629
630#[cfg(test)]
631mod tests {
632    use super::*;
633
634    #[test]
635    fn framebuffer_new() {
636        let fb = Framebuffer::new(100, 100);
637        assert_eq!(fb.width(), 100);
638        assert_eq!(fb.height(), 100);
639        assert_eq!(fb.pixels().len(), 10000);
640    }
641
642    #[test]
643    fn framebuffer_with_color() {
644        let fb = Framebuffer::with_color(10, 10, [1.0, 0.0, 0.0, 1.0]);
645        for &px in fb.pixels() {
646            let c = unpack_rgba(px);
647            assert!((c[0] - 1.0).abs() < 0.01);
648            assert!((c[1]).abs() < 0.01);
649            assert!((c[2]).abs() < 0.01);
650            assert!((c[3] - 1.0).abs() < 0.01);
651        }
652    }
653
654    #[test]
655    fn software_fill_rect() {
656        let mut r = SoftwareRenderer::new(100, 100);
657        r.fill_rect(
658            Rect {
659                x: 10.0,
660                y: 10.0,
661                width: 20.0,
662                height: 20.0,
663            },
664            [1.0, 0.0, 0.0, 1.0],
665        );
666
667        let fb = r.framebuffer();
668        // Inside rect should be red
669        let idx = (15 * 100 + 15) as usize;
670        let c = unpack_rgba(fb.pixels()[idx]);
671        assert!((c[0] - 1.0).abs() < 0.01);
672
673        // Outside rect should be transparent
674        let idx2 = (5 * 100 + 5) as usize;
675        let c2 = unpack_rgba(fb.pixels()[idx2]);
676        assert!(c2[3] < 0.01);
677    }
678
679    #[test]
680    fn software_fill_rounded_rect() {
681        let mut r = SoftwareRenderer::new(100, 100);
682        r.fill_rounded_rect(
683            Rect {
684                x: 10.0,
685                y: 10.0,
686                width: 40.0,
687                height: 40.0,
688            },
689            8.0,
690            [0.0, 1.0, 0.0, 1.0],
691        );
692        let fb = r.framebuffer();
693        // Center should be green
694        let idx = (30 * 100 + 30) as usize;
695        let c = unpack_rgba(fb.pixels()[idx]);
696        assert!((c[1] - 1.0).abs() < 0.01);
697    }
698
699    #[test]
700    fn software_fill_ellipse() {
701        let mut r = SoftwareRenderer::new(100, 100);
702        r.fill_ellipse(
703            Rect {
704                x: 20.0,
705                y: 20.0,
706                width: 60.0,
707                height: 60.0,
708            },
709            [0.0, 0.0, 1.0, 1.0],
710        );
711        let fb = r.framebuffer();
712        // Center should be blue
713        let idx = (50 * 100 + 50) as usize;
714        let c = unpack_rgba(fb.pixels()[idx]);
715        assert!((c[2] - 1.0).abs() < 0.01);
716    }
717
718    #[test]
719    fn software_glass_degrades_to_solid() {
720        let mut r = SoftwareRenderer::new(100, 100);
721        r.fill_glass_rect(
722            Rect {
723                x: 10.0,
724                y: 10.0,
725                width: 40.0,
726                height: 40.0,
727            },
728            8.0,
729            16.0,
730        );
731        let fb = r.framebuffer();
732        // Glass center should be semi-transparent white (degraded)
733        let idx = (30 * 100 + 30) as usize;
734        let c = unpack_rgba(fb.pixels()[idx]);
735        assert!(c[3] > 0.1, "glass should have some opacity");
736        assert!(c[3] < 0.9, "glass should not be fully opaque");
737    }
738
739    #[test]
740    fn software_stroke_rect() {
741        let mut r = SoftwareRenderer::new(100, 100);
742        r.stroke_rect(
743            Rect {
744                x: 10.0,
745                y: 10.0,
746                width: 30.0,
747                height: 30.0,
748            },
749            [1.0, 1.0, 1.0, 1.0],
750            2.0,
751        );
752        let fb = r.framebuffer();
753        // Edge pixel should be white
754        let idx = (10 * 100 + 10) as usize;
755        let c = unpack_rgba(fb.pixels()[idx]);
756        assert!(c[0] > 0.5);
757    }
758
759    #[test]
760    fn software_clear_color() {
761        let r = SoftwareRenderer::with_color(10, 10, [0.5, 0.5, 0.5, 1.0]);
762        let fb = r.framebuffer();
763        // Verify initial color
764        let c = unpack_rgba(fb.pixels()[0]);
765        assert!((c[0] - 0.5).abs() < 0.02);
766    }
767
768    #[test]
769    fn software_measure_text() {
770        let mut r = SoftwareRenderer::new(100, 100);
771        let (w, h) = r.measure_text("Hello", 14.0);
772        assert!(w > 0.0);
773        assert!((h - 14.0).abs() < 0.01);
774    }
775
776    #[test]
777    fn software_elapsed_time() {
778        let r = SoftwareRenderer::new(100, 100);
779        assert!(r.elapsed_time() >= 0.0);
780    }
781
782    #[test]
783    fn software_gradient() {
784        let mut r = SoftwareRenderer::new(100, 100);
785        r.draw_linear_gradient(
786            Rect {
787                x: 0.0,
788                y: 0.0,
789                width: 100.0,
790                height: 1.0,
791            },
792            [1.0, 0.0, 0.0, 1.0],
793            [0.0, 0.0, 1.0, 1.0],
794            0.0,
795        );
796        let fb = r.framebuffer();
797        let left = unpack_rgba(fb.pixels()[0]);
798        let right = unpack_rgba(fb.pixels()[99]);
799        assert!((left[0] - 1.0).abs() < 0.02); // Red on left
800        assert!((right[2] - 1.0).abs() < 0.02); // Blue on right
801    }
802
803    // ==========================================
804    // P1-8: SoftwareRenderer explicit stub warnings
805    // ==========================================
806    // Verify that the unimplemented methods at least exist
807    // (don't panic at the trait level) and return without
808    // modifying the framebuffer. The tracing::warn! calls are
809    // not asserted (would require log capture infrastructure),
810    // but the tests prove the methods don't crash the renderer.
811
812    #[test]
813    fn p1_8_draw_image_does_not_panic() {
814        let mut r = SoftwareRenderer::new(100, 100);
815        r.draw_image(
816            "test.png",
817            cvkg_core::Rect {
818                x: 0.0,
819                y: 0.0,
820                width: 50.0,
821                height: 50.0,
822            },
823        );
824        // Framebuffer should be unmodified (all transparent).
825        let fb = r.framebuffer();
826        for pixel in fb.pixels() {
827            assert_eq!(*pixel, 0, "draw_image should not modify the framebuffer");
828        }
829    }
830
831    #[test]
832    fn p1_8_draw_svg_does_not_panic() {
833        let mut r = SoftwareRenderer::new(100, 100);
834        r.draw_svg(
835            "icon",
836            cvkg_core::Rect {
837                x: 0.0,
838                y: 0.0,
839                width: 50.0,
840                height: 50.0,
841            },
842        );
843        // Should not panic.
844    }
845
846    #[test]
847    fn p1_8_draw_texture_does_not_panic() {
848        let mut r = SoftwareRenderer::new(100, 100);
849        r.draw_texture(
850            1,
851            cvkg_core::Rect {
852                x: 0.0,
853                y: 0.0,
854                width: 50.0,
855                height: 50.0,
856            },
857        );
858        // Should not panic.
859    }
860}
861
862#[cfg(test)]
863mod smoke_tests {
864    use super::*;
865
866    #[test]
867    fn framebuffer_constructs() {
868        let fb = Framebuffer::new(64, 64);
869        assert_eq!(fb.width(), 64);
870        assert_eq!(fb.height(), 64);
871        assert_eq!(fb.pixels().len(), 64 * 64);
872    }
873
874    #[test]
875    fn software_renderer_constructs() {
876        let r = SoftwareRenderer::new(64, 64);
877        assert_eq!(r.width(), 64);
878        assert_eq!(r.height(), 64);
879    }
880
881    #[test]
882    fn software_renderer_with_color_constructs() {
883        let r = SoftwareRenderer::with_color(32, 32, [0.5, 0.5, 0.5, 1.0]);
884        assert_eq!(r.width(), 32);
885        assert_eq!(r.height(), 32);
886    }
887}