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 * height) as usize;
44 Self {
45 width,
46 height,
47 pixels: vec![0; size],
48 depth: vec![0.0; size],
49 }
50 }
51
52 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 pub fn pixels(&self) -> &[u32] {
69 &self.pixels
70 }
71
72 pub fn pixels_mut(&mut self) -> &mut [u32] {
74 &mut self.pixels
75 }
76
77 pub fn clear(&mut self) {
79 self.pixels.fill(0);
80 self.depth.fill(0.0);
81 }
82
83 pub fn clear_color(&mut self, color: [f32; 4]) {
85 let packed = pack_rgba(color);
86 self.pixels.fill(packed);
87 }
88
89 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 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 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
135pub struct SoftwareRenderer {
142 fb: Framebuffer,
143 start_time: Instant,
144 last_frame: Instant,
145 #[cfg(feature = "text")]
147 text_engine: cvkg_runic_text::TextEngine,
148 memoize_cache: Option<(u64, u64)>,
151}
152
153impl SoftwareRenderer {
154 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 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 pub fn framebuffer(&self) -> &Framebuffer {
190 &self.fb
191 }
192
193 pub fn width(&self) -> u32 {
195 self.fb.width()
196 }
197
198 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 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 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 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 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 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 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 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 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 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 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 fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
516 (text.len() as f32 * size * 0.6, size)
517 }
518
519 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 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 fn draw_shaped_text(&mut self, _text: &cvkg_runic_text::ShapedText, _x: f32, _y: f32) {
543 }
545
546 fn draw_texture(&mut self, texture_id: u32, _rect: Rect) {
547 tracing::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 tracing::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 tracing::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 tracing::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 tracing::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 self.fill_glass_rect(_rect, _radius, _blur_radius);
593 }
594
595 fn draw_hologram(&mut self, _rect: Rect, hologram_id: &str, _time: f32) {
596 tracing::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 if let Some(&(cached_id, cached_hash)) = self.memoize_cache.as_ref()
608 && cached_id == id
609 && cached_hash == data_hash
610 {
611 return; }
613 self.memoize_cache = Some((id, data_hash));
614 render_fn(self);
615 }
616}
617
618#[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 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 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 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 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 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 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 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); assert!((right[2] - 1.0).abs() < 0.02); }
792
793 #[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 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 }
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 }
850}
851
852#[cfg(test)]
853mod smoke_tests {
854 use super::*;
855
856 #[test]
857 fn framebuffer_constructs() {
858 let fb = Framebuffer::new(64, 64);
859 assert_eq!(fb.width(), 64);
860 assert_eq!(fb.height(), 64);
861 assert_eq!(fb.pixels().len(), 64 * 64);
862 }
863
864 #[test]
865 fn software_renderer_constructs() {
866 let r = SoftwareRenderer::new(64, 64);
867 assert_eq!(r.width(), 64);
868 assert_eq!(r.height(), 64);
869 }
870
871 #[test]
872 fn software_renderer_with_color_constructs() {
873 let r = SoftwareRenderer::with_color(32, 32, [0.5, 0.5, 0.5, 1.0]);
874 assert_eq!(r.width(), 32);
875 assert_eq!(r.height(), 32);
876 }
877}