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