1use cvkg_core::{ElapsedTime, Material3D, Mesh, Rect, Renderer, Transform3D};
27use std::time::Instant;
28
29#[derive(Debug, Clone)]
33pub struct Framebuffer {
34 width: u32,
35 height: u32,
36 pixels: Vec<u32>, depth: Vec<f32>, }
39
40impl Framebuffer {
41 pub fn new(width: u32, height: u32) -> Self {
43 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 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 pub fn pixels(&self) -> &[u32] {
70 &self.pixels
71 }
72
73 pub fn pixels_mut(&mut self) -> &mut [u32] {
75 &mut self.pixels
76 }
77
78 pub fn clear(&mut self) {
80 self.pixels.fill(0);
81 self.depth.fill(0.0);
82 }
83
84 pub fn clear_color(&mut self, color: [f32; 4]) {
86 let packed = pack_rgba(color);
87 self.pixels.fill(packed);
88 }
89
90 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 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 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
136pub struct SoftwareRenderer {
143 fb: Framebuffer,
144 start_time: Instant,
145 last_frame: Instant,
146 #[cfg(feature = "text")]
148 text_engine: cvkg_runic_text::TextEngine,
149 memoize_cache: Option<(u64, u64)>,
152}
153
154impl SoftwareRenderer {
155 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 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 pub fn framebuffer(&self) -> &Framebuffer {
191 &self.fb
192 }
193
194 pub fn width(&self) -> u32 {
196 self.fb.width()
197 }
198
199 pub fn height(&self) -> u32 {
201 self.fb.height()
202 }
203
204 fn fill_rect_internal(&mut self, rect: Rect, color: [f32; 4]) {
205 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 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 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 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 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 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 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 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 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 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 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 fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
526 (text.chars().count() as f32 * size * 0.6, size)
527 }
528
529 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 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 fn draw_shaped_text(&mut self, _text: &cvkg_runic_text::ShapedText, _x: f32, _y: f32) {
553 }
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 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 if let Some(&(cached_id, cached_hash)) = self.memoize_cache.as_ref()
618 && cached_id == id
619 && cached_hash == data_hash
620 {
621 return; }
623 self.memoize_cache = Some((id, data_hash));
624 render_fn(self);
625 }
626}
627
628#[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 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 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 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 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 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 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 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); assert!((right[2] - 1.0).abs() < 0.02); }
802
803 #[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 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 }
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 }
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}