1use oxiui_core::geometry::Size;
32use oxiui_core::paint::{DrawList, RenderBackend};
33use oxiui_core::{Color, UiError};
34use wgpu::util::DeviceExt;
35
36use crate::gpu::buffer::Globals;
37use crate::gpu::device::GpuContext;
38use crate::gpu::exec::{
39 run_gradient_pass_batched, run_solid_pass, run_textured_pass, FrameStats, GradientPassParams,
40 SolidPassParams, TexturedPassParams,
41};
42use crate::gpu::geometry::build_geometry;
43use crate::gpu::pipeline::{
44 BlurPipeline, CompositePipeline, GradientPipeline, SolidPipeline, TexturedPipeline,
45};
46
47pub struct WgpuBackend {
51 ctx: GpuContext,
52 pipeline: SolidPipeline,
54 gradient_pipeline: GradientPipeline,
55 textured_pipeline: TexturedPipeline,
56 blur_pipeline: BlurPipeline,
58 composite_pipeline: CompositePipeline,
60 solid_mask_pipeline: SolidPipeline,
62 globals_buffer: wgpu::Buffer,
63 globals_bind_group: wgpu::BindGroup,
64 clear_color: Color,
65 last_frame_stats: FrameStats,
67 solid_vertex_buf: Option<wgpu::Buffer>,
69 solid_vertex_buf_capacity: usize,
71}
72
73impl WgpuBackend {
74 pub fn headless_with_quality(
89 width: u32,
90 height: u32,
91 quality: &crate::RenderQuality,
92 ) -> Result<Self, UiError> {
93 let sc = quality.sample_count();
94 let ctx = GpuContext::headless_with_sample_count(width, height, sc)?;
95 let pipeline = SolidPipeline::new(&ctx.device, ctx.sample_count);
97 let gradient_pipeline = GradientPipeline::new(&ctx.device, ctx.sample_count);
98 let textured_pipeline = TexturedPipeline::new(&ctx.device, ctx.sample_count);
99 let composite_pipeline = CompositePipeline::new(&ctx.device, ctx.sample_count);
100 let blur_pipeline = BlurPipeline::new(&ctx.device, 1);
102 let solid_mask_pipeline = SolidPipeline::new(&ctx.device, 1);
103
104 let globals = Globals::new(width, height);
105 let globals_buffer = ctx
106 .device
107 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
108 label: Some("oxiui-render-wgpu globals"),
109 contents: bytemuck::bytes_of(&globals),
110 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
111 });
112
113 let globals_bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
114 label: Some("oxiui-render-wgpu globals bind group"),
115 layout: &pipeline.globals_layout,
116 entries: &[wgpu::BindGroupEntry {
117 binding: 0,
118 resource: globals_buffer.as_entire_binding(),
119 }],
120 });
121
122 Ok(Self {
123 ctx,
124 pipeline,
125 gradient_pipeline,
126 textured_pipeline,
127 blur_pipeline,
128 composite_pipeline,
129 solid_mask_pipeline,
130 globals_buffer,
131 globals_bind_group,
132 clear_color: Color(0, 0, 0, 0),
133 last_frame_stats: FrameStats::default(),
134 solid_vertex_buf: None,
135 solid_vertex_buf_capacity: 0,
136 })
137 }
138
139 pub fn headless(width: u32, height: u32) -> Result<Self, UiError> {
156 Self::headless_with_quality(width, height, &crate::RenderQuality::low())
157 }
158
159 pub fn ctx(&self) -> &GpuContext {
161 &self.ctx
162 }
163
164 pub fn set_clear_color(&mut self, color: Color) {
166 self.clear_color = color;
167 }
168
169 pub fn clear_color(&self) -> Color {
171 self.clear_color
172 }
173
174 pub fn width(&self) -> u32 {
176 self.ctx.width
177 }
178
179 pub fn height(&self) -> u32 {
181 self.ctx.height
182 }
183
184 pub fn frame_stats(&self) -> FrameStats {
192 self.last_frame_stats
193 }
194
195 pub fn readback_rgba(&self) -> Result<Vec<u8>, UiError> {
202 let width = self.ctx.width;
203 let height = self.ctx.height;
204 let unpadded_bytes_per_row = width * 4;
205 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
206 let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
207 let buffer_size = (padded_bytes_per_row * height) as wgpu::BufferAddress;
208
209 let readback = self.ctx.device.create_buffer(&wgpu::BufferDescriptor {
210 label: Some("oxiui-render-wgpu readback"),
211 size: buffer_size,
212 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
213 mapped_at_creation: false,
214 });
215
216 let mut encoder = self
217 .ctx
218 .device
219 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
220 label: Some("oxiui-render-wgpu readback encoder"),
221 });
222
223 encoder.copy_texture_to_buffer(
224 wgpu::TexelCopyTextureInfo {
225 texture: &self.ctx.color_texture,
226 mip_level: 0,
227 origin: wgpu::Origin3d::ZERO,
228 aspect: wgpu::TextureAspect::All,
229 },
230 wgpu::TexelCopyBufferInfo {
231 buffer: &readback,
232 layout: wgpu::TexelCopyBufferLayout {
233 offset: 0,
234 bytes_per_row: Some(padded_bytes_per_row),
235 rows_per_image: Some(height),
236 },
237 },
238 wgpu::Extent3d {
239 width,
240 height,
241 depth_or_array_layers: 1,
242 },
243 );
244
245 self.ctx.queue.submit(Some(encoder.finish()));
246
247 let slice = readback.slice(..);
248 slice.map_async(wgpu::MapMode::Read, |_| {});
249 self.ctx
250 .device
251 .poll(wgpu::PollType::wait_indefinitely())
252 .map_err(|e| UiError::Render(format!("GPU poll failed during readback: {e:?}")))?;
253
254 let data = slice.get_mapped_range();
255
256 let mut out = Vec::with_capacity((unpadded_bytes_per_row * height) as usize);
257 for row in 0..height {
258 let start = (row * padded_bytes_per_row) as usize;
259 let end = start + unpadded_bytes_per_row as usize;
260 out.extend_from_slice(&data[start..end]);
261 }
262
263 drop(data);
264 readback.unmap();
265 Ok(out)
266 }
267
268 pub fn read_pixel(&self, x: u32, y: u32) -> Result<Option<(u8, u8, u8, u8)>, UiError> {
270 if x >= self.ctx.width || y >= self.ctx.height {
271 return Ok(None);
272 }
273 let buf = self.readback_rgba()?;
274 let idx = ((y * self.ctx.width + x) * 4) as usize;
275 Ok(Some((buf[idx], buf[idx + 1], buf[idx + 2], buf[idx + 3])))
276 }
277
278 pub fn resize(&mut self, new_width: u32, new_height: u32) -> Result<(), UiError> {
291 self.ctx.resize(new_width, new_height)?;
293
294 let globals = Globals::new(new_width, new_height);
296 self.ctx
297 .queue
298 .write_buffer(&self.globals_buffer, 0, bytemuck::bytes_of(&globals));
299
300 self.solid_vertex_buf = None;
303 self.solid_vertex_buf_capacity = 0;
304
305 Ok(())
306 }
307}
308
309impl RenderBackend for WgpuBackend {
312 fn surface_size(&self) -> Size {
313 Size::new(self.ctx.width as f32, self.ctx.height as f32)
314 }
315
316 fn supports_gradients(&self) -> bool {
317 true
318 }
319
320 fn supports_paths(&self) -> bool {
321 true
322 }
323
324 fn supports_images(&self) -> bool {
325 true
326 }
327
328 fn supports_blur(&self) -> bool {
329 true
330 }
331
332 fn supports_blend_modes(&self) -> bool {
333 true
334 }
335
336 fn supports_backdrop_blur(&self) -> bool {
337 true
338 }
339
340 fn execute(&mut self, list: &DrawList) -> Result<(), UiError> {
341 self.last_frame_stats = FrameStats::default();
343
344 let globals = Globals::new(self.ctx.width, self.ctx.height);
346 self.ctx
347 .queue
348 .write_buffer(&self.globals_buffer, 0, bytemuck::bytes_of(&globals));
349
350 let (verts, segments, gradient_draws, textured_draws, _backdrop_blur_draws) =
351 build_geometry(list, self.ctx.width, self.ctx.height);
352
353 let clear = self.clear_color;
354 let clear_value = wgpu::Color {
355 r: clear.0 as f64 / 255.0,
356 g: clear.1 as f64 / 255.0,
357 b: clear.2 as f64 / 255.0,
358 a: clear.3 as f64 / 255.0,
359 };
360
361 let (screen_view, screen_resolve) = self.ctx.color_attachment();
364
365 {
373 let mut encoder =
374 self.ctx
375 .device
376 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
377 label: Some("oxiui-render-wgpu clear encoder"),
378 });
379 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
380 label: Some("oxiui-render-wgpu clear pass"),
381 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
382 view: screen_view,
383 depth_slice: None,
384 resolve_target: screen_resolve,
385 ops: wgpu::Operations {
386 load: wgpu::LoadOp::Clear(clear_value),
387 store: wgpu::StoreOp::Store,
388 },
389 })],
390 depth_stencil_attachment: None,
391 timestamp_writes: None,
392 occlusion_query_set: None,
393 multiview_mask: None,
394 });
395 drop(_pass);
397 self.ctx.queue.submit(Some(encoder.finish()));
398 }
399 self.last_frame_stats.render_passes += 1;
401
402 let shadows = crate::gpu::shadow::collect_shadows(list);
410 let shadow_gpu = crate::gpu::shadow::ShadowGpuState {
411 device: &self.ctx.device,
412 queue: &self.ctx.queue,
413 target_view: screen_view,
414 resolve_target: screen_resolve,
415 globals_buffer: &self.globals_buffer,
416 globals_bind_group: &self.globals_bind_group,
417 viewport_w: self.ctx.width,
418 viewport_h: self.ctx.height,
419 };
420 let shadow_pipelines = crate::gpu::shadow::ShadowPipelines {
421 solid: &self.solid_mask_pipeline,
423 blur: &self.blur_pipeline,
424 composite: &self.composite_pipeline,
426 };
427 let shadow_stats =
428 crate::gpu::shadow::render_shadows(&shadow_gpu, &shadow_pipelines, &shadows)?;
429 self.last_frame_stats.render_passes += shadow_stats.render_passes;
430 self.last_frame_stats.draw_calls += shadow_stats.draw_calls;
431
432 let mut encoder = self
433 .ctx
434 .device
435 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
436 label: Some("oxiui-render-wgpu frame encoder"),
437 });
438
439 let (screen_view2, screen_resolve2) = self.ctx.color_attachment();
441
442 let solid_draws = run_solid_pass(SolidPassParams {
444 device: &self.ctx.device,
445 queue: &self.ctx.queue,
446 encoder: &mut encoder,
447 screen_view: screen_view2,
448 screen_resolve: screen_resolve2,
449 pipeline: &self.pipeline,
450 globals_bind_group: &self.globals_bind_group,
451 verts: &verts,
452 segments: &segments,
453 viewport_w: self.ctx.width,
454 viewport_h: self.ctx.height,
455 solid_vertex_buf: &mut self.solid_vertex_buf,
456 solid_vertex_buf_capacity: &mut self.solid_vertex_buf_capacity,
457 });
458 self.last_frame_stats.render_passes += 1;
460 self.last_frame_stats.draw_calls += solid_draws;
461
462 {
464 let (sv, sr) = self.ctx.color_attachment();
465 let (rp, dc) = run_gradient_pass_batched(GradientPassParams {
466 device: &self.ctx.device,
467 queue: &self.ctx.queue,
468 encoder: &mut encoder,
469 screen_view: sv,
470 screen_resolve: sr,
471 pipeline: &self.gradient_pipeline,
472 globals_buffer: &self.globals_buffer,
473 gradient_draws: &gradient_draws,
474 viewport_w: self.ctx.width,
475 viewport_h: self.ctx.height,
476 });
477 self.last_frame_stats.render_passes += rp;
478 self.last_frame_stats.draw_calls += dc;
479 }
480
481 for td in &textured_draws {
483 let (sv, sr) = self.ctx.color_attachment();
484 let (rp, dc) = run_textured_pass(TexturedPassParams {
485 device: &self.ctx.device,
486 queue: &self.ctx.queue,
487 encoder: &mut encoder,
488 screen_view: sv,
489 screen_resolve: sr,
490 pipeline: &self.textured_pipeline,
491 globals_bind_group: &self.globals_bind_group,
492 td,
493 viewport_w: self.ctx.width,
494 viewport_h: self.ctx.height,
495 })?;
496 self.last_frame_stats.render_passes += rp;
497 self.last_frame_stats.draw_calls += dc;
498 }
499
500 self.ctx.queue.submit(Some(encoder.finish()));
501 Ok(())
502 }
503}
504
505#[cfg(test)]
508mod tests {
509 use super::*;
510 use oxiui_core::geometry::{Point, Rect};
511 use oxiui_core::paint::{DrawCommand, DrawList, FillRule, GradientStop, PathData, StrokeStyle};
512 use oxiui_core::Color;
513
514 #[test]
517 fn msaa_smooths_diagonal_edge() {
518 let Some(mut b) =
522 WgpuBackend::headless_with_quality(64, 64, &crate::RenderQuality::balanced()).ok()
523 else {
524 return;
525 };
526 let mut list = DrawList::new();
527 let red = Color(255, 0, 0, 255);
529 let mut path = PathData::new();
530 path.move_to(Point::new(0.0, 0.0));
531 path.line_to(Point::new(63.0, 0.0));
532 path.line_to(Point::new(0.0, 63.0));
533 path.close();
534 list.push_path(path, red);
535 b.execute(&list).expect("execute");
536 let buf = b.readback_rgba().expect("readback");
537 let w = b.width();
538 let pixel = |x: u32, y: u32| -> (u8, u8, u8, u8) {
539 let i = ((y * w + x) * 4) as usize;
540 (buf[i], buf[i + 1], buf[i + 2], buf[i + 3])
541 };
542 if b.ctx().sample_count() > 1 {
543 let mut found_intermediate = false;
546 for d in 5u32..58u32 {
547 let p = pixel(d, d);
548 if p.3 > 0 && p.3 < 255 {
549 found_intermediate = true;
550 break;
551 }
552 }
553 assert!(
554 found_intermediate,
555 "MSAA should produce intermediate-alpha pixels on diagonal edge"
556 );
557 }
558 let inside = pixel(5, 5);
560 assert_eq!(inside.3, 255, "inside pixel must be fully opaque");
561 }
562
563 #[test]
564 fn non_msaa_edge_is_hard() {
565 let Some(mut b) = try_backend(64, 64) else {
566 return;
567 };
568 let mut list = DrawList::new();
569 let red = Color(255, 0, 0, 255);
570 let mut path = PathData::new();
571 path.move_to(Point::new(0.0, 0.0));
572 path.line_to(Point::new(63.0, 0.0));
573 path.line_to(Point::new(0.0, 63.0));
574 path.close();
575 list.push_path(path, red);
576 b.execute(&list).expect("execute");
577 let buf = b.readback_rgba().expect("readback");
578 let w = b.width();
579 for d in 0u32..64u32 {
582 let i = ((d * w + d) * 4) as usize;
583 let a = buf[i + 3];
584 assert!(
585 a == 0 || a == 255,
586 "non-MSAA edge pixel at ({d},{d}) must be 0 or 255, got {a}"
587 );
588 }
589 }
590
591 #[test]
592 fn msaa_default_path_unchanged() {
593 let Some(mut b) = try_backend(64, 64) else {
596 return;
597 };
598 assert_eq!(
599 b.ctx().sample_count(),
600 1,
601 "headless() must use sample_count=1"
602 );
603 let mut list = DrawList::new();
604 list.push_rect(Rect::new(10.0, 10.0, 20.0, 20.0), Color(255, 0, 0, 255));
605 b.execute(&list).expect("execute");
606 let px = b.read_pixel(20, 20).expect("read").expect("pixel");
607 assert_eq!(
608 (px.0, px.1, px.2, px.3),
609 (255, 0, 0, 255),
610 "basic rect fill must still work"
611 );
612 }
613
614 fn try_backend(w: u32, h: u32) -> Option<WgpuBackend> {
615 WgpuBackend::headless(w, h).ok()
616 }
617
618 fn assert_visible(b: &WgpuBackend, x: u32, y: u32, label: &str) {
619 let px = b
620 .read_pixel(x, y)
621 .expect("read_pixel ok")
622 .expect("in bounds");
623 assert!(px.3 > 0, "{label}: pixel ({x},{y}) alpha=0, got {px:?}");
624 }
625
626 fn assert_transparent(b: &WgpuBackend, x: u32, y: u32, label: &str) {
627 let px = b
628 .read_pixel(x, y)
629 .expect("read_pixel ok")
630 .expect("in bounds");
631 assert!(
632 px.3 == 0,
633 "{label}: pixel ({x},{y}) expected transparent, got {px:?}"
634 );
635 }
636
637 #[test]
638 fn test_stroke_rect_renders() {
639 let Some(mut b) = try_backend(100, 100) else {
640 return;
641 };
642 let mut dl = DrawList::new();
643 dl.push(DrawCommand::StrokeRect {
644 rect: Rect::new(10.0, 10.0, 80.0, 80.0),
645 thickness: 4.0,
646 color: Color(255, 0, 0, 255),
647 });
648 b.execute(&dl).expect("execute ok");
649 assert_visible(&b, 12, 10, "stroke_rect top border");
650 assert_transparent(&b, 50, 50, "stroke_rect interior");
651 }
652
653 #[test]
654 fn test_fill_rounded_rect_renders() {
655 let Some(mut b) = try_backend(100, 100) else {
656 return;
657 };
658 let mut dl = DrawList::new();
659 dl.push(DrawCommand::FillRoundedRect {
660 rect: Rect::new(10.0, 10.0, 80.0, 80.0),
661 radius: 10.0,
662 color: Color(0, 200, 0, 255),
663 });
664 b.execute(&dl).expect("execute ok");
665 assert_visible(&b, 50, 50, "rrect centre");
666 assert_transparent(&b, 10, 10, "rrect corner tl");
667 }
668
669 #[test]
670 fn test_fill_rounded_rect_per_corner_renders() {
671 let Some(mut b) = try_backend(100, 100) else {
672 return;
673 };
674 let mut dl = DrawList::new();
675 dl.push(DrawCommand::FillRoundedRectPerCorner {
676 rect: Rect::new(10.0, 10.0, 80.0, 80.0),
677 radii: [15.0, 5.0, 15.0, 5.0],
678 color: Color(0, 100, 200, 255),
679 });
680 b.execute(&dl).expect("execute ok");
681 assert_visible(&b, 50, 50, "rrect-pc centre");
682 }
683
684 #[test]
685 fn test_fill_ellipse_renders() {
686 let Some(mut b) = try_backend(100, 100) else {
687 return;
688 };
689 let mut dl = DrawList::new();
690 dl.push(DrawCommand::FillEllipse {
691 center: Point::new(50.0, 50.0),
692 rx: 30.0,
693 ry: 20.0,
694 color: Color(200, 0, 200, 255),
695 });
696 b.execute(&dl).expect("execute ok");
697 assert_visible(&b, 50, 50, "ellipse centre");
698 assert_transparent(&b, 2, 2, "ellipse exterior");
699 }
700
701 #[test]
702 fn test_line_renders() {
703 let Some(mut b) = try_backend(100, 100) else {
704 return;
705 };
706 let mut dl = DrawList::new();
707 dl.push(DrawCommand::Line {
708 from: Point::new(10.0, 50.0),
709 to: Point::new(90.0, 50.0),
710 color: Color(255, 255, 0, 255),
711 });
712 b.execute(&dl).expect("execute ok");
713 assert_visible(&b, 50, 50, "line mid");
714 }
715
716 #[test]
717 fn test_fill_path_renders() {
718 let Some(mut b) = try_backend(100, 100) else {
719 return;
720 };
721 let mut path = PathData::new();
722 path.move_to(Point::new(20.0, 20.0));
723 path.line_to(Point::new(80.0, 20.0));
724 path.line_to(Point::new(50.0, 80.0));
725 path.close();
726 let mut dl = DrawList::new();
727 dl.push(DrawCommand::FillPath {
728 path,
729 color: Color(255, 0, 128, 255),
730 });
731 b.execute(&dl).expect("execute ok");
732 assert_visible(&b, 50, 40, "fill_path interior");
733 assert_transparent(&b, 2, 2, "fill_path exterior");
734 }
735
736 #[test]
737 fn test_stroke_path_renders() {
738 let Some(mut b) = try_backend(100, 100) else {
739 return;
740 };
741 let mut path = PathData::new();
742 path.move_to(Point::new(20.0, 50.0));
743 path.line_to(Point::new(80.0, 50.0));
744 let style = StrokeStyle {
745 width: 4.0,
746 ..Default::default()
747 };
748 let mut dl = DrawList::new();
749 dl.push(DrawCommand::StrokePath {
750 path,
751 style,
752 color: Color(200, 200, 0, 255),
753 });
754 b.execute(&dl).expect("execute ok");
755 assert_visible(&b, 50, 50, "stroke_path mid");
756 }
757
758 #[test]
759 fn test_linear_gradient_renders() {
760 let Some(mut b) = try_backend(100, 100) else {
761 return;
762 };
763 let stops = vec![
764 GradientStop::new(0.0, Color(255, 0, 0, 255)),
765 GradientStop::new(1.0, Color(0, 0, 255, 255)),
766 ];
767 let mut dl = DrawList::new();
768 dl.push(DrawCommand::LinearGradient {
769 rect: Rect::new(0.0, 0.0, 100.0, 100.0),
770 start: Point::new(0.0, 50.0),
771 end: Point::new(100.0, 50.0),
772 stops,
773 });
774 b.execute(&dl).expect("execute ok");
775 let left = b.read_pixel(5, 50).expect("ok").expect("bounds");
776 assert!(left.0 > 128, "left reddish: {left:?}");
777 let right = b.read_pixel(95, 50).expect("ok").expect("bounds");
778 assert!(right.2 > 128, "right bluish: {right:?}");
779 let mid = b.read_pixel(50, 50).expect("ok").expect("bounds");
780 assert!(mid.3 > 0, "mid visible: {mid:?}");
781 }
782
783 #[test]
784 fn test_radial_gradient_renders() {
785 let Some(mut b) = try_backend(100, 100) else {
786 return;
787 };
788 let stops = vec![
789 GradientStop::new(0.0, Color(255, 255, 255, 255)),
790 GradientStop::new(1.0, Color(0, 0, 0, 255)),
791 ];
792 let mut dl = DrawList::new();
793 dl.push(DrawCommand::RadialGradient {
794 rect: Rect::new(0.0, 0.0, 100.0, 100.0),
795 center: Point::new(50.0, 50.0),
796 radius: 40.0,
797 stops,
798 });
799 b.execute(&dl).expect("execute ok");
800 let centre = b.read_pixel(50, 50).expect("ok").expect("bounds");
801 assert!(centre.0 > 200, "centre bright: {centre:?}");
802 let edge = b.read_pixel(90, 50).expect("ok").expect("bounds");
803 assert!(
804 edge.0 < centre.0,
805 "edge darker: edge={edge:?} centre={centre:?}"
806 );
807 }
808
809 #[test]
810 fn test_supports_probes() {
811 let Some(b) = try_backend(64, 64) else {
812 return;
813 };
814 assert!(b.supports_gradients());
815 assert!(b.supports_paths());
816 }
817
818 #[test]
819 fn image_solid_fill_readback() {
820 use oxiui_core::paint::{DrawList, ImageData, ImageFilter};
821 let Some(mut b) = try_backend(64, 64) else {
822 return;
823 };
824 let image = ImageData::new(
826 vec![
827 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255,
828 ],
829 2,
830 2,
831 );
832 let mut dl = DrawList::new();
833 dl.push_image(
834 image,
835 Rect::new(12.0, 12.0, 40.0, 40.0),
836 ImageFilter::Nearest,
837 );
838 b.execute(&dl).expect("execute ok");
839 let px = b.read_pixel(32, 32).expect("ok").expect("bounds");
840 assert!(px.0 > 200 && px.3 > 200, "centre should be red: {px:?}");
841 assert_transparent(&b, 2, 2, "outside image");
842 }
843
844 #[test]
845 fn nine_slice_renders() {
846 use oxiui_core::paint::{DrawList, ImageData};
847 let Some(mut b) = try_backend(128, 128) else {
848 return;
849 };
850 let mut rgba = vec![0u8; 12 * 12 * 4];
852 for y in 0..12u32 {
853 for x in 0..12u32 {
854 let i = ((y * 12 + x) * 4) as usize;
855 let corner = !(4..8).contains(&x) || !(4..8).contains(&y);
856 if corner {
857 rgba[i] = 255;
858 rgba[i + 1] = 0;
859 rgba[i + 2] = 0;
860 rgba[i + 3] = 255; } else {
862 rgba[i] = 0;
863 rgba[i + 1] = 0;
864 rgba[i + 2] = 255;
865 rgba[i + 3] = 255; }
867 }
868 }
869 let image = ImageData::new(rgba, 12, 12);
870 let mut dl = DrawList::new();
871 dl.push_nine_slice(image, Rect::new(0.0, 0.0, 128.0, 128.0), [4, 4, 4, 4]);
872 b.execute(&dl).expect("execute ok");
873 let corner = b.read_pixel(2, 2).expect("ok").expect("bounds");
875 assert!(corner.0 > 100, "corner should be reddish: {corner:?}");
876 let centre = b.read_pixel(64, 64).expect("ok").expect("bounds");
878 assert!(centre.2 > 100, "centre should be bluish: {centre:?}");
879 }
880
881 #[test]
882 fn tex_vertex_size_is_32() {
883 use crate::gpu::buffer::TexVertex;
884 assert_eq!(core::mem::size_of::<TexVertex>(), 32);
885 }
886
887 #[test]
890 fn box_shadow_zero_blur_is_sharp() {
891 let Some(mut b) = try_backend(128, 128) else {
892 return;
893 };
894 let mut dl = DrawList::new();
895 dl.push_shadow(
896 Rect::new(20.0, 20.0, 80.0, 80.0),
897 Point::new(0.0, 0.0),
898 0.0,
899 Color(0, 0, 0, 200),
900 );
901 b.execute(&dl).expect("execute ok");
902 let interior = b.read_pixel(60, 60).expect("ok").expect("bounds");
904 assert!(interior.3 > 100, "interior should be visible: {interior:?}");
905 let outside = b.read_pixel(5, 5).expect("ok").expect("bounds");
907 assert!(outside.3 == 0, "outside should be transparent: {outside:?}");
908 }
909
910 #[test]
911 fn box_shadow_blur_halo_falloff() {
912 let Some(mut b) = try_backend(200, 200) else {
913 return;
914 };
915 let mut dl = DrawList::new();
916 dl.push_shadow(
917 Rect::new(50.0, 50.0, 100.0, 100.0),
918 Point::new(0.0, 0.0),
919 12.0,
920 Color(0, 0, 0, 255),
921 );
922 b.execute(&dl).expect("execute ok");
923 let interior = b.read_pixel(100, 100).expect("ok").expect("bounds");
925 assert!(interior.3 > 100, "interior should be visible: {interior:?}");
926 let edge = b.read_pixel(45, 100).expect("ok").expect("bounds");
928 let far = b.read_pixel(5, 5).expect("ok").expect("bounds");
930 assert!(far.3 < edge.3, "falloff: far={far:?} edge={edge:?}");
931 }
932
933 #[test]
934 fn box_shadow_offset_translates() {
935 let Some(mut b) = try_backend(200, 200) else {
936 return;
937 };
938 let mut dl = DrawList::new();
939 dl.push_shadow(
940 Rect::new(50.0, 50.0, 80.0, 80.0),
941 Point::new(20.0, 20.0),
942 0.0,
943 Color(0, 0, 0, 255),
944 );
945 b.execute(&dl).expect("execute ok");
946 let orig_pos = b.read_pixel(55, 55).expect("ok").expect("bounds");
948 assert!(
949 orig_pos.3 == 0,
950 "original rect pos should be transparent: {orig_pos:?}"
951 );
952 let offset_pos = b.read_pixel(80, 80).expect("ok").expect("bounds");
954 assert!(
955 offset_pos.3 > 100,
956 "offset pos should be visible: {offset_pos:?}"
957 );
958 }
959
960 #[test]
961 fn shadows_render_under_solids() {
962 let Some(mut b) = try_backend(200, 200) else {
963 return;
964 };
965 let mut dl = DrawList::new();
966 dl.push_shadow(
968 Rect::new(10.0, 10.0, 180.0, 180.0),
969 Point::new(0.0, 0.0),
970 0.0,
971 Color(255, 0, 0, 255), );
973 dl.push(DrawCommand::FillRect {
975 rect: Rect::new(10.0, 10.0, 180.0, 180.0),
976 color: Color(0, 0, 255, 255), });
978 b.execute(&dl).expect("execute ok");
979 let px = b.read_pixel(100, 100).expect("ok").expect("bounds");
981 assert!(
982 px.2 > 200 && px.0 < 100,
983 "blue rect should be on top: {px:?}"
984 );
985 }
986
987 #[test]
988 fn fill_path_concave_notch_empty() {
989 let Some(mut b) = try_backend(64, 64) else {
992 return;
993 };
994 let mut list = DrawList::new();
995 let red = Color(255, 0, 0, 255);
996 let mut path = PathData::new();
998 path.move_to(Point::new(5.0, 5.0));
999 path.line_to(Point::new(59.0, 5.0));
1000 path.line_to(Point::new(59.0, 59.0));
1001 path.line_to(Point::new(32.0, 40.0)); path.line_to(Point::new(5.0, 59.0));
1003 path.close();
1004 list.push_path(path, red);
1005 b.execute(&list).expect("execute");
1006 let body = b.read_pixel(32, 10).expect("read").expect("pixel");
1008 assert_eq!(body.3, 255, "body should be opaque");
1009 let notch = b.read_pixel(32, 55).expect("read").expect("pixel");
1011 assert_eq!(
1012 notch.3, 0,
1013 "notch must be transparent (concave fill correct)"
1014 );
1015 }
1016
1017 #[test]
1018 fn fill_path_donut_hole_empty() {
1019 let Some(mut b) = try_backend(64, 64) else {
1022 return;
1023 };
1024 let mut list = DrawList::new();
1025 let blue = Color(0, 0, 255, 255);
1026 let mut path = PathData::new();
1027 path.move_to(Point::new(4.0, 4.0));
1029 path.line_to(Point::new(60.0, 4.0));
1030 path.line_to(Point::new(60.0, 60.0));
1031 path.line_to(Point::new(4.0, 60.0));
1032 path.close();
1033 path.move_to(Point::new(20.0, 20.0));
1035 path.line_to(Point::new(20.0, 44.0));
1036 path.line_to(Point::new(44.0, 44.0));
1037 path.line_to(Point::new(44.0, 20.0));
1038 path.close();
1039 list.push_path(path, blue);
1040 b.execute(&list).expect("execute");
1041 let ring = b.read_pixel(10, 10).expect("read").expect("pixel");
1043 assert_eq!(
1044 (ring.0, ring.1, ring.2, ring.3),
1045 (0, 0, 255, 255),
1046 "ring must be blue"
1047 );
1048 let hole = b.read_pixel(32, 32).expect("read").expect("pixel");
1050 assert_eq!(hole.3, 0, "donut hole must be transparent");
1051 }
1052
1053 #[test]
1054 fn fill_rule_evenodd_vs_nonzero() {
1055 let Some(mut b_eo) = try_backend(64, 64) else {
1059 return;
1060 };
1061 let Some(mut b_nz) = try_backend(64, 64) else {
1062 return;
1063 };
1064 let make_path = |fill_rule: FillRule| {
1065 let mut path = PathData::new().with_fill_rule(fill_rule);
1066 path.move_to(Point::new(4.0, 4.0));
1068 path.line_to(Point::new(60.0, 4.0));
1069 path.line_to(Point::new(60.0, 60.0));
1070 path.line_to(Point::new(4.0, 60.0));
1071 path.close();
1072 path.move_to(Point::new(20.0, 20.0));
1074 path.line_to(Point::new(44.0, 20.0));
1075 path.line_to(Point::new(44.0, 44.0));
1076 path.line_to(Point::new(20.0, 44.0));
1077 path.close();
1078 path
1079 };
1080 let green = Color(0, 255, 0, 255);
1081 let mut list_eo = DrawList::new();
1082 list_eo.push_path(make_path(FillRule::EvenOdd), green);
1083 let mut list_nz = DrawList::new();
1084 list_nz.push_path(make_path(FillRule::NonZero), green);
1085 b_eo.execute(&list_eo).expect("execute");
1086 b_nz.execute(&list_nz).expect("execute");
1087 let inner_eo = b_eo.read_pixel(32, 32).expect("read").expect("pixel");
1089 let inner_nz = b_nz.read_pixel(32, 32).expect("read").expect("pixel");
1090 assert_eq!(
1092 inner_eo.3, 0,
1093 "EvenOdd: same-winding inner ring must be transparent (depth=1 = hole)"
1094 );
1095 assert_eq!(
1097 inner_nz.3, 255,
1098 "NonZero: same-winding inner ring must be opaque (winding=2 ≠ 0)"
1099 );
1100 }
1101
1102 #[test]
1105 fn culled_offscreen_rect_is_transparent() {
1106 let Some(mut b) = try_backend(64, 64) else {
1110 return;
1111 };
1112 let mut list = DrawList::new();
1113 list.push_clip(Rect::new(0.0, 0.0, 32.0, 32.0));
1115 list.push_rect(Rect::new(40.0, 40.0, 20.0, 20.0), Color(255, 0, 0, 255));
1117 list.pop_clip();
1118 b.execute(&list).expect("execute");
1119 let px = b.read_pixel(45, 45).expect("read").expect("pixel");
1121 assert_eq!(px.3, 0, "rect outside clip must be culled (transparent)");
1122 let px2 = b.read_pixel(10, 10).expect("read").expect("pixel");
1124 assert_eq!(px2.3, 0, "undrawn area must remain transparent");
1125 }
1126
1127 #[test]
1128 fn culling_does_not_affect_visible_rect() {
1129 let Some(mut b) = try_backend(64, 64) else {
1132 return;
1133 };
1134 let mut list = DrawList::new();
1135 list.push_rect(Rect::new(10.0, 10.0, 40.0, 40.0), Color(0, 255, 0, 255));
1136 b.execute(&list).expect("execute");
1137 let px = b.read_pixel(30, 30).expect("read").expect("pixel");
1138 assert_eq!(
1139 (px.0, px.1, px.2, px.3),
1140 (0, 255, 0, 255),
1141 "visible rect must not be culled"
1142 );
1143 }
1144
1145 #[test]
1148 fn frame_stats_counts_solid_draws() {
1149 let Some(mut backend) = try_backend(64, 64) else {
1150 return;
1151 };
1152 let mut list = DrawList::new();
1154 list.push(DrawCommand::FillRect {
1155 rect: Rect::new(10.0, 10.0, 44.0, 44.0),
1156 color: Color(255, 0, 0, 255),
1157 });
1158 backend.execute(&list).expect("execute failed");
1159 let stats = backend.frame_stats();
1160 assert!(stats.draw_calls >= 1, "should have at least 1 draw call");
1161 assert!(
1162 stats.render_passes >= 1,
1163 "should have at least 1 render pass"
1164 );
1165 }
1166
1167 #[test]
1170 fn two_gradients_one_pass() {
1171 let Some(mut backend) = try_backend(128, 64) else {
1172 return;
1173 };
1174 let mut list = DrawList::new();
1175 list.push(DrawCommand::LinearGradient {
1177 rect: Rect::new(0.0, 0.0, 64.0, 64.0),
1178 start: Point::new(0.0, 0.0),
1179 end: Point::new(64.0, 0.0),
1180 stops: vec![
1181 GradientStop::new(0.0, Color(255, 0, 0, 255)),
1182 GradientStop::new(1.0, Color(0, 0, 255, 255)),
1183 ],
1184 });
1185 list.push(DrawCommand::LinearGradient {
1187 rect: Rect::new(64.0, 0.0, 64.0, 64.0),
1188 start: Point::new(64.0, 0.0),
1189 end: Point::new(128.0, 0.0),
1190 stops: vec![
1191 GradientStop::new(0.0, Color(0, 255, 0, 255)),
1192 GradientStop::new(1.0, Color(255, 255, 0, 255)),
1193 ],
1194 });
1195 backend.execute(&list).expect("execute");
1196 let stats = backend.frame_stats();
1197 assert!(
1199 stats.draw_calls >= 2,
1200 "should have at least 2 draw calls for 2 gradients, got {}",
1201 stats.draw_calls
1202 );
1203
1204 let left_px = backend
1206 .read_pixel(2, 32)
1207 .expect("read left")
1208 .expect("bounds");
1209 assert!(left_px.0 > 200, "left should be reddish, got {:?}", left_px);
1210 assert!(
1211 left_px.2 < 100,
1212 "left should not be blue, got {:?}",
1213 left_px
1214 );
1215
1216 let right_px = backend
1218 .read_pixel(66, 32)
1219 .expect("read right")
1220 .expect("bounds");
1221 assert!(
1222 right_px.1 > 200,
1223 "right should be greenish, got {:?}",
1224 right_px
1225 );
1226 assert!(
1227 right_px.0 < 100,
1228 "right should not be red, got {:?}",
1229 right_px
1230 );
1231 }
1232
1233 #[test]
1234 fn gradient_byte_exact_single() {
1235 let Some(mut backend) = try_backend(64, 64) else {
1238 return;
1239 };
1240 let mut list = DrawList::new();
1241 list.push(DrawCommand::LinearGradient {
1242 rect: Rect::new(0.0, 0.0, 64.0, 64.0),
1243 start: Point::new(0.0, 0.0),
1244 end: Point::new(64.0, 0.0),
1245 stops: vec![
1246 GradientStop::new(0.0, Color(255, 0, 0, 255)),
1247 GradientStop::new(1.0, Color(0, 0, 255, 255)),
1248 ],
1249 });
1250 backend.execute(&list).expect("execute");
1251 let left = backend
1253 .read_pixel(1, 32)
1254 .expect("read left")
1255 .expect("bounds");
1256 assert!(
1257 left.0 > 200 && left.2 < 100,
1258 "left should be reddish: {:?}",
1259 left
1260 );
1261 let right = backend
1263 .read_pixel(62, 32)
1264 .expect("read right")
1265 .expect("bounds");
1266 assert!(
1267 right.2 > 200 && right.0 < 100,
1268 "right should be bluish: {:?}",
1269 right
1270 );
1271 let mid = backend
1273 .read_pixel(32, 32)
1274 .expect("read mid")
1275 .expect("bounds");
1276 assert!(mid.3 > 0, "mid should be visible: {:?}", mid);
1277 }
1278
1279 #[test]
1280 fn persistent_buffer_reuse_stable() {
1281 let Some(mut backend) = try_backend(64, 64) else {
1284 return;
1285 };
1286
1287 let mut list1 = DrawList::new();
1289 for i in 0..10u32 {
1290 list1.push(DrawCommand::FillRect {
1291 rect: Rect::new(i as f32 * 4.0, 0.0, 4.0, 64.0),
1292 color: Color(255, 0, 0, 255),
1293 });
1294 }
1295 backend.execute(&list1).expect("frame 1");
1296 let px1 = backend
1297 .read_pixel(2, 32)
1298 .expect("frame 1 pixel")
1299 .expect("bounds");
1300 assert_eq!(px1.0, 255, "frame 1 should be red: {:?}", px1);
1301 assert_eq!(px1.3, 255, "frame 1 should be opaque: {:?}", px1);
1302
1303 let mut list2 = DrawList::new();
1305 list2.push(DrawCommand::FillRect {
1306 rect: Rect::new(0.0, 0.0, 64.0, 64.0),
1307 color: Color(0, 0, 255, 255),
1308 });
1309 backend.execute(&list2).expect("frame 2");
1310 let px2 = backend
1311 .read_pixel(32, 32)
1312 .expect("frame 2 pixel")
1313 .expect("bounds");
1314 assert_eq!(px2.2, 255, "frame 2 should be blue: {:?}", px2);
1315 assert!(
1317 px2.0 < 10,
1318 "frame 2 stale-tail check: should not see red, got {:?}",
1319 px2
1320 );
1321 }
1322}