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
47#[cfg(feature = "text")]
48use crate::text_bridge::TextBridge;
49#[cfg(feature = "text")]
50use oxiui_text::TextPipeline;
51
52pub struct WgpuBackend {
56 ctx: GpuContext,
57 pipeline: SolidPipeline,
59 gradient_pipeline: GradientPipeline,
60 textured_pipeline: TexturedPipeline,
61 blur_pipeline: BlurPipeline,
63 composite_pipeline: CompositePipeline,
65 solid_mask_pipeline: SolidPipeline,
67 globals_buffer: wgpu::Buffer,
68 globals_bind_group: wgpu::BindGroup,
69 clear_color: Color,
70 last_frame_stats: FrameStats,
72 solid_vertex_buf: Option<wgpu::Buffer>,
74 solid_vertex_buf_capacity: usize,
76 #[cfg(feature = "text")]
81 text_bridge: Option<TextBridge>,
82}
83
84impl WgpuBackend {
85 pub fn headless_with_quality(
100 width: u32,
101 height: u32,
102 quality: &crate::RenderQuality,
103 ) -> Result<Self, UiError> {
104 let sc = quality.sample_count();
105 let ctx = GpuContext::headless_with_sample_count(width, height, sc)?;
106 let pipeline = SolidPipeline::new(&ctx.device, ctx.sample_count);
108 let gradient_pipeline = GradientPipeline::new(&ctx.device, ctx.sample_count);
109 let textured_pipeline = TexturedPipeline::new(&ctx.device, ctx.sample_count);
110 let composite_pipeline = CompositePipeline::new(&ctx.device, ctx.sample_count);
111 let blur_pipeline = BlurPipeline::new(&ctx.device, 1);
113 let solid_mask_pipeline = SolidPipeline::new(&ctx.device, 1);
114
115 let globals = Globals::new(width, height);
116 let globals_buffer = ctx
117 .device
118 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
119 label: Some("oxiui-render-wgpu globals"),
120 contents: bytemuck::bytes_of(&globals),
121 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
122 });
123
124 let globals_bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
125 label: Some("oxiui-render-wgpu globals bind group"),
126 layout: &pipeline.globals_layout,
127 entries: &[wgpu::BindGroupEntry {
128 binding: 0,
129 resource: globals_buffer.as_entire_binding(),
130 }],
131 });
132
133 Ok(Self {
134 ctx,
135 pipeline,
136 gradient_pipeline,
137 textured_pipeline,
138 blur_pipeline,
139 composite_pipeline,
140 solid_mask_pipeline,
141 globals_buffer,
142 globals_bind_group,
143 clear_color: Color(0, 0, 0, 0),
144 last_frame_stats: FrameStats::default(),
145 solid_vertex_buf: None,
146 solid_vertex_buf_capacity: 0,
147 #[cfg(feature = "text")]
148 text_bridge: None,
149 })
150 }
151
152 pub fn headless(width: u32, height: u32) -> Result<Self, UiError> {
169 Self::headless_with_quality(width, height, &crate::RenderQuality::low())
170 }
171
172 pub fn ctx(&self) -> &GpuContext {
174 &self.ctx
175 }
176
177 pub fn set_clear_color(&mut self, color: Color) {
179 self.clear_color = color;
180 }
181
182 pub fn clear_color(&self) -> Color {
184 self.clear_color
185 }
186
187 pub fn width(&self) -> u32 {
189 self.ctx.width
190 }
191
192 pub fn height(&self) -> u32 {
194 self.ctx.height
195 }
196
197 pub fn frame_stats(&self) -> FrameStats {
205 self.last_frame_stats
206 }
207
208 pub fn readback_rgba(&self) -> Result<Vec<u8>, UiError> {
215 let width = self.ctx.width;
216 let height = self.ctx.height;
217 let unpadded_bytes_per_row = width * 4;
218 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
219 let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
220 let buffer_size = (padded_bytes_per_row * height) as wgpu::BufferAddress;
221
222 let readback = self.ctx.device.create_buffer(&wgpu::BufferDescriptor {
223 label: Some("oxiui-render-wgpu readback"),
224 size: buffer_size,
225 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
226 mapped_at_creation: false,
227 });
228
229 let mut encoder = self
230 .ctx
231 .device
232 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
233 label: Some("oxiui-render-wgpu readback encoder"),
234 });
235
236 encoder.copy_texture_to_buffer(
237 wgpu::TexelCopyTextureInfo {
238 texture: &self.ctx.color_texture,
239 mip_level: 0,
240 origin: wgpu::Origin3d::ZERO,
241 aspect: wgpu::TextureAspect::All,
242 },
243 wgpu::TexelCopyBufferInfo {
244 buffer: &readback,
245 layout: wgpu::TexelCopyBufferLayout {
246 offset: 0,
247 bytes_per_row: Some(padded_bytes_per_row),
248 rows_per_image: Some(height),
249 },
250 },
251 wgpu::Extent3d {
252 width,
253 height,
254 depth_or_array_layers: 1,
255 },
256 );
257
258 self.ctx.queue.submit(Some(encoder.finish()));
259
260 let slice = readback.slice(..);
261 slice.map_async(wgpu::MapMode::Read, |_| {});
262 self.ctx
263 .device
264 .poll(wgpu::PollType::wait_indefinitely())
265 .map_err(|e| UiError::Render(format!("GPU poll failed during readback: {e:?}")))?;
266
267 let data = slice.get_mapped_range();
268
269 let mut out = Vec::with_capacity((unpadded_bytes_per_row * height) as usize);
270 for row in 0..height {
271 let start = (row * padded_bytes_per_row) as usize;
272 let end = start + unpadded_bytes_per_row as usize;
273 out.extend_from_slice(&data[start..end]);
274 }
275
276 drop(data);
277 readback.unmap();
278 Ok(out)
279 }
280
281 pub fn read_pixel(&self, x: u32, y: u32) -> Result<Option<(u8, u8, u8, u8)>, UiError> {
283 if x >= self.ctx.width || y >= self.ctx.height {
284 return Ok(None);
285 }
286 let buf = self.readback_rgba()?;
287 let idx = ((y * self.ctx.width + x) * 4) as usize;
288 Ok(Some((buf[idx], buf[idx + 1], buf[idx + 2], buf[idx + 3])))
289 }
290
291 pub fn resize(&mut self, new_width: u32, new_height: u32) -> Result<(), UiError> {
304 self.ctx.resize(new_width, new_height)?;
306
307 let globals = Globals::new(new_width, new_height);
309 self.ctx
310 .queue
311 .write_buffer(&self.globals_buffer, 0, bytemuck::bytes_of(&globals));
312
313 self.solid_vertex_buf = None;
316 self.solid_vertex_buf_capacity = 0;
317
318 Ok(())
319 }
320}
321
322impl RenderBackend for WgpuBackend {
325 fn surface_size(&self) -> Size {
326 Size::new(self.ctx.width as f32, self.ctx.height as f32)
327 }
328
329 fn supports_gradients(&self) -> bool {
330 true
331 }
332
333 fn supports_paths(&self) -> bool {
334 true
335 }
336
337 fn supports_images(&self) -> bool {
338 true
339 }
340
341 fn supports_blur(&self) -> bool {
342 true
343 }
344
345 fn supports_blend_modes(&self) -> bool {
346 true
347 }
348
349 fn supports_backdrop_blur(&self) -> bool {
350 true
351 }
352
353 fn supports_text(&self) -> bool {
354 cfg!(feature = "text")
357 }
358
359 fn execute(&mut self, list: &DrawList) -> Result<(), UiError> {
360 self.last_frame_stats = FrameStats::default();
362
363 let globals = Globals::new(self.ctx.width, self.ctx.height);
365 self.ctx
366 .queue
367 .write_buffer(&self.globals_buffer, 0, bytemuck::bytes_of(&globals));
368
369 #[cfg(feature = "text")]
379 let expanded_list: DrawList;
380 #[cfg(feature = "text")]
381 let list: &DrawList = {
382 use oxiui_core::paint::DrawCommand;
383 let has_text = list
384 .iter()
385 .any(|c| matches!(c, DrawCommand::DrawText { .. }));
386 if has_text {
387 if self.text_bridge.is_none() {
389 let candidates = &[
391 "Helvetica",
392 "Arial",
393 "DejaVu Sans",
394 "Liberation Sans",
395 "sans-serif",
396 ];
397 for &family in candidates {
398 if let Ok(pipeline) = TextPipeline::from_system_font(family) {
399 self.text_bridge = Some(TextBridge::new(pipeline, 1024));
400 break;
401 }
402 }
403 }
404 if let Some(bridge) = &mut self.text_bridge {
405 expanded_list = bridge.expand_draw_text_commands(list);
406 &expanded_list
407 } else {
408 list
409 }
410 } else {
411 list
412 }
413 };
414
415 let (verts, segments, gradient_draws, textured_draws, _backdrop_blur_draws) =
416 build_geometry(list, self.ctx.width, self.ctx.height);
417
418 let clear = self.clear_color;
419 let clear_value = wgpu::Color {
420 r: clear.0 as f64 / 255.0,
421 g: clear.1 as f64 / 255.0,
422 b: clear.2 as f64 / 255.0,
423 a: clear.3 as f64 / 255.0,
424 };
425
426 let (screen_view, screen_resolve) = self.ctx.color_attachment();
429
430 {
438 let mut encoder =
439 self.ctx
440 .device
441 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
442 label: Some("oxiui-render-wgpu clear encoder"),
443 });
444 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
445 label: Some("oxiui-render-wgpu clear pass"),
446 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
447 view: screen_view,
448 depth_slice: None,
449 resolve_target: screen_resolve,
450 ops: wgpu::Operations {
451 load: wgpu::LoadOp::Clear(clear_value),
452 store: wgpu::StoreOp::Store,
453 },
454 })],
455 depth_stencil_attachment: None,
456 timestamp_writes: None,
457 occlusion_query_set: None,
458 multiview_mask: None,
459 });
460 drop(_pass);
462 self.ctx.queue.submit(Some(encoder.finish()));
463 }
464 self.last_frame_stats.render_passes += 1;
466
467 let shadows = crate::gpu::shadow::collect_shadows(list);
475 let shadow_gpu = crate::gpu::shadow::ShadowGpuState {
476 device: &self.ctx.device,
477 queue: &self.ctx.queue,
478 target_view: screen_view,
479 resolve_target: screen_resolve,
480 globals_buffer: &self.globals_buffer,
481 globals_bind_group: &self.globals_bind_group,
482 viewport_w: self.ctx.width,
483 viewport_h: self.ctx.height,
484 };
485 let shadow_pipelines = crate::gpu::shadow::ShadowPipelines {
486 solid: &self.solid_mask_pipeline,
488 blur: &self.blur_pipeline,
489 composite: &self.composite_pipeline,
491 };
492 let shadow_stats =
493 crate::gpu::shadow::render_shadows(&shadow_gpu, &shadow_pipelines, &shadows)?;
494 self.last_frame_stats.render_passes += shadow_stats.render_passes;
495 self.last_frame_stats.draw_calls += shadow_stats.draw_calls;
496
497 let mut encoder = self
498 .ctx
499 .device
500 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
501 label: Some("oxiui-render-wgpu frame encoder"),
502 });
503
504 let (screen_view2, screen_resolve2) = self.ctx.color_attachment();
506
507 let solid_draws = run_solid_pass(SolidPassParams {
509 device: &self.ctx.device,
510 queue: &self.ctx.queue,
511 encoder: &mut encoder,
512 screen_view: screen_view2,
513 screen_resolve: screen_resolve2,
514 pipeline: &self.pipeline,
515 globals_bind_group: &self.globals_bind_group,
516 verts: &verts,
517 segments: &segments,
518 viewport_w: self.ctx.width,
519 viewport_h: self.ctx.height,
520 solid_vertex_buf: &mut self.solid_vertex_buf,
521 solid_vertex_buf_capacity: &mut self.solid_vertex_buf_capacity,
522 });
523 self.last_frame_stats.render_passes += 1;
525 self.last_frame_stats.draw_calls += solid_draws;
526
527 {
529 let (sv, sr) = self.ctx.color_attachment();
530 let (rp, dc) = run_gradient_pass_batched(GradientPassParams {
531 device: &self.ctx.device,
532 queue: &self.ctx.queue,
533 encoder: &mut encoder,
534 screen_view: sv,
535 screen_resolve: sr,
536 pipeline: &self.gradient_pipeline,
537 globals_buffer: &self.globals_buffer,
538 gradient_draws: &gradient_draws,
539 viewport_w: self.ctx.width,
540 viewport_h: self.ctx.height,
541 });
542 self.last_frame_stats.render_passes += rp;
543 self.last_frame_stats.draw_calls += dc;
544 }
545
546 for td in &textured_draws {
548 let (sv, sr) = self.ctx.color_attachment();
549 let (rp, dc) = run_textured_pass(TexturedPassParams {
550 device: &self.ctx.device,
551 queue: &self.ctx.queue,
552 encoder: &mut encoder,
553 screen_view: sv,
554 screen_resolve: sr,
555 pipeline: &self.textured_pipeline,
556 globals_bind_group: &self.globals_bind_group,
557 td,
558 viewport_w: self.ctx.width,
559 viewport_h: self.ctx.height,
560 })?;
561 self.last_frame_stats.render_passes += rp;
562 self.last_frame_stats.draw_calls += dc;
563 }
564
565 self.ctx.queue.submit(Some(encoder.finish()));
566 Ok(())
567 }
568}
569
570#[cfg(test)]
573mod tests {
574 use super::*;
575 use oxiui_core::geometry::{Point, Rect};
576 use oxiui_core::paint::{DrawCommand, DrawList, FillRule, GradientStop, PathData, StrokeStyle};
577 use oxiui_core::Color;
578
579 #[test]
582 fn msaa_smooths_diagonal_edge() {
583 let Some(mut b) =
587 WgpuBackend::headless_with_quality(64, 64, &crate::RenderQuality::balanced()).ok()
588 else {
589 return;
590 };
591 let mut list = DrawList::new();
592 let red = Color(255, 0, 0, 255);
594 let mut path = PathData::new();
595 path.move_to(Point::new(0.0, 0.0));
596 path.line_to(Point::new(63.0, 0.0));
597 path.line_to(Point::new(0.0, 63.0));
598 path.close();
599 list.push_path(path, red);
600 b.execute(&list).expect("execute");
601 let buf = b.readback_rgba().expect("readback");
602 let w = b.width();
603 let pixel = |x: u32, y: u32| -> (u8, u8, u8, u8) {
604 let i = ((y * w + x) * 4) as usize;
605 (buf[i], buf[i + 1], buf[i + 2], buf[i + 3])
606 };
607 if b.ctx().sample_count() > 1 {
608 let mut found_intermediate = false;
611 for d in 5u32..58u32 {
612 let p = pixel(d, d);
613 if p.3 > 0 && p.3 < 255 {
614 found_intermediate = true;
615 break;
616 }
617 }
618 assert!(
619 found_intermediate,
620 "MSAA should produce intermediate-alpha pixels on diagonal edge"
621 );
622 }
623 let inside = pixel(5, 5);
625 assert_eq!(inside.3, 255, "inside pixel must be fully opaque");
626 }
627
628 #[test]
629 fn non_msaa_edge_is_hard() {
630 let Some(mut b) = try_backend(64, 64) else {
631 return;
632 };
633 let mut list = DrawList::new();
634 let red = Color(255, 0, 0, 255);
635 let mut path = PathData::new();
636 path.move_to(Point::new(0.0, 0.0));
637 path.line_to(Point::new(63.0, 0.0));
638 path.line_to(Point::new(0.0, 63.0));
639 path.close();
640 list.push_path(path, red);
641 b.execute(&list).expect("execute");
642 let buf = b.readback_rgba().expect("readback");
643 let w = b.width();
644 for d in 0u32..64u32 {
647 let i = ((d * w + d) * 4) as usize;
648 let a = buf[i + 3];
649 assert!(
650 a == 0 || a == 255,
651 "non-MSAA edge pixel at ({d},{d}) must be 0 or 255, got {a}"
652 );
653 }
654 }
655
656 #[test]
657 fn msaa_default_path_unchanged() {
658 let Some(mut b) = try_backend(64, 64) else {
661 return;
662 };
663 assert_eq!(
664 b.ctx().sample_count(),
665 1,
666 "headless() must use sample_count=1"
667 );
668 let mut list = DrawList::new();
669 list.push_rect(Rect::new(10.0, 10.0, 20.0, 20.0), Color(255, 0, 0, 255));
670 b.execute(&list).expect("execute");
671 let px = b.read_pixel(20, 20).expect("read").expect("pixel");
672 assert_eq!(
673 (px.0, px.1, px.2, px.3),
674 (255, 0, 0, 255),
675 "basic rect fill must still work"
676 );
677 }
678
679 fn try_backend(w: u32, h: u32) -> Option<WgpuBackend> {
680 WgpuBackend::headless(w, h).ok()
681 }
682
683 fn assert_visible(b: &WgpuBackend, x: u32, y: u32, label: &str) {
684 let px = b
685 .read_pixel(x, y)
686 .expect("read_pixel ok")
687 .expect("in bounds");
688 assert!(px.3 > 0, "{label}: pixel ({x},{y}) alpha=0, got {px:?}");
689 }
690
691 fn assert_transparent(b: &WgpuBackend, x: u32, y: u32, label: &str) {
692 let px = b
693 .read_pixel(x, y)
694 .expect("read_pixel ok")
695 .expect("in bounds");
696 assert!(
697 px.3 == 0,
698 "{label}: pixel ({x},{y}) expected transparent, got {px:?}"
699 );
700 }
701
702 #[test]
703 fn test_stroke_rect_renders() {
704 let Some(mut b) = try_backend(100, 100) else {
705 return;
706 };
707 let mut dl = DrawList::new();
708 dl.push(DrawCommand::StrokeRect {
709 rect: Rect::new(10.0, 10.0, 80.0, 80.0),
710 thickness: 4.0,
711 color: Color(255, 0, 0, 255),
712 });
713 b.execute(&dl).expect("execute ok");
714 assert_visible(&b, 12, 10, "stroke_rect top border");
715 assert_transparent(&b, 50, 50, "stroke_rect interior");
716 }
717
718 #[test]
719 fn test_fill_rounded_rect_renders() {
720 let Some(mut b) = try_backend(100, 100) else {
721 return;
722 };
723 let mut dl = DrawList::new();
724 dl.push(DrawCommand::FillRoundedRect {
725 rect: Rect::new(10.0, 10.0, 80.0, 80.0),
726 radius: 10.0,
727 color: Color(0, 200, 0, 255),
728 });
729 b.execute(&dl).expect("execute ok");
730 assert_visible(&b, 50, 50, "rrect centre");
731 assert_transparent(&b, 10, 10, "rrect corner tl");
732 }
733
734 #[test]
735 fn test_fill_rounded_rect_per_corner_renders() {
736 let Some(mut b) = try_backend(100, 100) else {
737 return;
738 };
739 let mut dl = DrawList::new();
740 dl.push(DrawCommand::FillRoundedRectPerCorner {
741 rect: Rect::new(10.0, 10.0, 80.0, 80.0),
742 radii: [15.0, 5.0, 15.0, 5.0],
743 color: Color(0, 100, 200, 255),
744 });
745 b.execute(&dl).expect("execute ok");
746 assert_visible(&b, 50, 50, "rrect-pc centre");
747 }
748
749 #[test]
750 fn test_fill_ellipse_renders() {
751 let Some(mut b) = try_backend(100, 100) else {
752 return;
753 };
754 let mut dl = DrawList::new();
755 dl.push(DrawCommand::FillEllipse {
756 center: Point::new(50.0, 50.0),
757 rx: 30.0,
758 ry: 20.0,
759 color: Color(200, 0, 200, 255),
760 });
761 b.execute(&dl).expect("execute ok");
762 assert_visible(&b, 50, 50, "ellipse centre");
763 assert_transparent(&b, 2, 2, "ellipse exterior");
764 }
765
766 #[test]
767 fn test_line_renders() {
768 let Some(mut b) = try_backend(100, 100) else {
769 return;
770 };
771 let mut dl = DrawList::new();
772 dl.push(DrawCommand::Line {
773 from: Point::new(10.0, 50.0),
774 to: Point::new(90.0, 50.0),
775 color: Color(255, 255, 0, 255),
776 });
777 b.execute(&dl).expect("execute ok");
778 assert_visible(&b, 50, 50, "line mid");
779 }
780
781 #[test]
782 fn test_fill_path_renders() {
783 let Some(mut b) = try_backend(100, 100) else {
784 return;
785 };
786 let mut path = PathData::new();
787 path.move_to(Point::new(20.0, 20.0));
788 path.line_to(Point::new(80.0, 20.0));
789 path.line_to(Point::new(50.0, 80.0));
790 path.close();
791 let mut dl = DrawList::new();
792 dl.push(DrawCommand::FillPath {
793 path,
794 color: Color(255, 0, 128, 255),
795 });
796 b.execute(&dl).expect("execute ok");
797 assert_visible(&b, 50, 40, "fill_path interior");
798 assert_transparent(&b, 2, 2, "fill_path exterior");
799 }
800
801 #[test]
802 fn test_stroke_path_renders() {
803 let Some(mut b) = try_backend(100, 100) else {
804 return;
805 };
806 let mut path = PathData::new();
807 path.move_to(Point::new(20.0, 50.0));
808 path.line_to(Point::new(80.0, 50.0));
809 let style = StrokeStyle {
810 width: 4.0,
811 ..Default::default()
812 };
813 let mut dl = DrawList::new();
814 dl.push(DrawCommand::StrokePath {
815 path,
816 style,
817 color: Color(200, 200, 0, 255),
818 });
819 b.execute(&dl).expect("execute ok");
820 assert_visible(&b, 50, 50, "stroke_path mid");
821 }
822
823 #[test]
824 fn test_linear_gradient_renders() {
825 let Some(mut b) = try_backend(100, 100) else {
826 return;
827 };
828 let stops = vec![
829 GradientStop::new(0.0, Color(255, 0, 0, 255)),
830 GradientStop::new(1.0, Color(0, 0, 255, 255)),
831 ];
832 let mut dl = DrawList::new();
833 dl.push(DrawCommand::LinearGradient {
834 rect: Rect::new(0.0, 0.0, 100.0, 100.0),
835 start: Point::new(0.0, 50.0),
836 end: Point::new(100.0, 50.0),
837 stops,
838 });
839 b.execute(&dl).expect("execute ok");
840 let left = b.read_pixel(5, 50).expect("ok").expect("bounds");
841 assert!(left.0 > 128, "left reddish: {left:?}");
842 let right = b.read_pixel(95, 50).expect("ok").expect("bounds");
843 assert!(right.2 > 128, "right bluish: {right:?}");
844 let mid = b.read_pixel(50, 50).expect("ok").expect("bounds");
845 assert!(mid.3 > 0, "mid visible: {mid:?}");
846 }
847
848 #[test]
849 fn test_radial_gradient_renders() {
850 let Some(mut b) = try_backend(100, 100) else {
851 return;
852 };
853 let stops = vec![
854 GradientStop::new(0.0, Color(255, 255, 255, 255)),
855 GradientStop::new(1.0, Color(0, 0, 0, 255)),
856 ];
857 let mut dl = DrawList::new();
858 dl.push(DrawCommand::RadialGradient {
859 rect: Rect::new(0.0, 0.0, 100.0, 100.0),
860 center: Point::new(50.0, 50.0),
861 radius: 40.0,
862 stops,
863 });
864 b.execute(&dl).expect("execute ok");
865 let centre = b.read_pixel(50, 50).expect("ok").expect("bounds");
866 assert!(centre.0 > 200, "centre bright: {centre:?}");
867 let edge = b.read_pixel(90, 50).expect("ok").expect("bounds");
868 assert!(
869 edge.0 < centre.0,
870 "edge darker: edge={edge:?} centre={centre:?}"
871 );
872 }
873
874 #[test]
875 fn test_supports_probes() {
876 let Some(b) = try_backend(64, 64) else {
877 return;
878 };
879 assert!(b.supports_gradients());
880 assert!(b.supports_paths());
881 }
882
883 #[test]
884 fn image_solid_fill_readback() {
885 use oxiui_core::paint::{DrawList, ImageData, ImageFilter};
886 let Some(mut b) = try_backend(64, 64) else {
887 return;
888 };
889 let image = ImageData::new(
891 vec![
892 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255,
893 ],
894 2,
895 2,
896 );
897 let mut dl = DrawList::new();
898 dl.push_image(
899 image,
900 Rect::new(12.0, 12.0, 40.0, 40.0),
901 ImageFilter::Nearest,
902 );
903 b.execute(&dl).expect("execute ok");
904 let px = b.read_pixel(32, 32).expect("ok").expect("bounds");
905 assert!(px.0 > 200 && px.3 > 200, "centre should be red: {px:?}");
906 assert_transparent(&b, 2, 2, "outside image");
907 }
908
909 #[test]
910 fn nine_slice_renders() {
911 use oxiui_core::paint::{DrawList, ImageData};
912 let Some(mut b) = try_backend(128, 128) else {
913 return;
914 };
915 let mut rgba = vec![0u8; 12 * 12 * 4];
917 for y in 0..12u32 {
918 for x in 0..12u32 {
919 let i = ((y * 12 + x) * 4) as usize;
920 let corner = !(4..8).contains(&x) || !(4..8).contains(&y);
921 if corner {
922 rgba[i] = 255;
923 rgba[i + 1] = 0;
924 rgba[i + 2] = 0;
925 rgba[i + 3] = 255; } else {
927 rgba[i] = 0;
928 rgba[i + 1] = 0;
929 rgba[i + 2] = 255;
930 rgba[i + 3] = 255; }
932 }
933 }
934 let image = ImageData::new(rgba, 12, 12);
935 let mut dl = DrawList::new();
936 dl.push_nine_slice(image, Rect::new(0.0, 0.0, 128.0, 128.0), [4, 4, 4, 4]);
937 b.execute(&dl).expect("execute ok");
938 let corner = b.read_pixel(2, 2).expect("ok").expect("bounds");
940 assert!(corner.0 > 100, "corner should be reddish: {corner:?}");
941 let centre = b.read_pixel(64, 64).expect("ok").expect("bounds");
943 assert!(centre.2 > 100, "centre should be bluish: {centre:?}");
944 }
945
946 #[test]
947 fn tex_vertex_size_is_32() {
948 use crate::gpu::buffer::TexVertex;
949 assert_eq!(core::mem::size_of::<TexVertex>(), 32);
950 }
951
952 #[test]
955 fn box_shadow_zero_blur_is_sharp() {
956 let Some(mut b) = try_backend(128, 128) else {
957 return;
958 };
959 let mut dl = DrawList::new();
960 dl.push_shadow(
961 Rect::new(20.0, 20.0, 80.0, 80.0),
962 Point::new(0.0, 0.0),
963 0.0,
964 Color(0, 0, 0, 200),
965 );
966 b.execute(&dl).expect("execute ok");
967 let interior = b.read_pixel(60, 60).expect("ok").expect("bounds");
969 assert!(interior.3 > 100, "interior should be visible: {interior:?}");
970 let outside = b.read_pixel(5, 5).expect("ok").expect("bounds");
972 assert!(outside.3 == 0, "outside should be transparent: {outside:?}");
973 }
974
975 #[test]
976 fn box_shadow_blur_halo_falloff() {
977 let Some(mut b) = try_backend(200, 200) else {
978 return;
979 };
980 let mut dl = DrawList::new();
981 dl.push_shadow(
982 Rect::new(50.0, 50.0, 100.0, 100.0),
983 Point::new(0.0, 0.0),
984 12.0,
985 Color(0, 0, 0, 255),
986 );
987 b.execute(&dl).expect("execute ok");
988 let interior = b.read_pixel(100, 100).expect("ok").expect("bounds");
990 assert!(interior.3 > 100, "interior should be visible: {interior:?}");
991 let edge = b.read_pixel(45, 100).expect("ok").expect("bounds");
993 let far = b.read_pixel(5, 5).expect("ok").expect("bounds");
995 assert!(far.3 < edge.3, "falloff: far={far:?} edge={edge:?}");
996 }
997
998 #[test]
999 fn box_shadow_offset_translates() {
1000 let Some(mut b) = try_backend(200, 200) else {
1001 return;
1002 };
1003 let mut dl = DrawList::new();
1004 dl.push_shadow(
1005 Rect::new(50.0, 50.0, 80.0, 80.0),
1006 Point::new(20.0, 20.0),
1007 0.0,
1008 Color(0, 0, 0, 255),
1009 );
1010 b.execute(&dl).expect("execute ok");
1011 let orig_pos = b.read_pixel(55, 55).expect("ok").expect("bounds");
1013 assert!(
1014 orig_pos.3 == 0,
1015 "original rect pos should be transparent: {orig_pos:?}"
1016 );
1017 let offset_pos = b.read_pixel(80, 80).expect("ok").expect("bounds");
1019 assert!(
1020 offset_pos.3 > 100,
1021 "offset pos should be visible: {offset_pos:?}"
1022 );
1023 }
1024
1025 #[test]
1026 fn shadows_render_under_solids() {
1027 let Some(mut b) = try_backend(200, 200) else {
1028 return;
1029 };
1030 let mut dl = DrawList::new();
1031 dl.push_shadow(
1033 Rect::new(10.0, 10.0, 180.0, 180.0),
1034 Point::new(0.0, 0.0),
1035 0.0,
1036 Color(255, 0, 0, 255), );
1038 dl.push(DrawCommand::FillRect {
1040 rect: Rect::new(10.0, 10.0, 180.0, 180.0),
1041 color: Color(0, 0, 255, 255), });
1043 b.execute(&dl).expect("execute ok");
1044 let px = b.read_pixel(100, 100).expect("ok").expect("bounds");
1046 assert!(
1047 px.2 > 200 && px.0 < 100,
1048 "blue rect should be on top: {px:?}"
1049 );
1050 }
1051
1052 #[test]
1053 fn fill_path_concave_notch_empty() {
1054 let Some(mut b) = try_backend(64, 64) else {
1057 return;
1058 };
1059 let mut list = DrawList::new();
1060 let red = Color(255, 0, 0, 255);
1061 let mut path = PathData::new();
1063 path.move_to(Point::new(5.0, 5.0));
1064 path.line_to(Point::new(59.0, 5.0));
1065 path.line_to(Point::new(59.0, 59.0));
1066 path.line_to(Point::new(32.0, 40.0)); path.line_to(Point::new(5.0, 59.0));
1068 path.close();
1069 list.push_path(path, red);
1070 b.execute(&list).expect("execute");
1071 let body = b.read_pixel(32, 10).expect("read").expect("pixel");
1073 assert_eq!(body.3, 255, "body should be opaque");
1074 let notch = b.read_pixel(32, 55).expect("read").expect("pixel");
1076 assert_eq!(
1077 notch.3, 0,
1078 "notch must be transparent (concave fill correct)"
1079 );
1080 }
1081
1082 #[test]
1083 fn fill_path_donut_hole_empty() {
1084 let Some(mut b) = try_backend(64, 64) else {
1087 return;
1088 };
1089 let mut list = DrawList::new();
1090 let blue = Color(0, 0, 255, 255);
1091 let mut path = PathData::new();
1092 path.move_to(Point::new(4.0, 4.0));
1094 path.line_to(Point::new(60.0, 4.0));
1095 path.line_to(Point::new(60.0, 60.0));
1096 path.line_to(Point::new(4.0, 60.0));
1097 path.close();
1098 path.move_to(Point::new(20.0, 20.0));
1100 path.line_to(Point::new(20.0, 44.0));
1101 path.line_to(Point::new(44.0, 44.0));
1102 path.line_to(Point::new(44.0, 20.0));
1103 path.close();
1104 list.push_path(path, blue);
1105 b.execute(&list).expect("execute");
1106 let ring = b.read_pixel(10, 10).expect("read").expect("pixel");
1108 assert_eq!(
1109 (ring.0, ring.1, ring.2, ring.3),
1110 (0, 0, 255, 255),
1111 "ring must be blue"
1112 );
1113 let hole = b.read_pixel(32, 32).expect("read").expect("pixel");
1115 assert_eq!(hole.3, 0, "donut hole must be transparent");
1116 }
1117
1118 #[test]
1119 fn fill_rule_evenodd_vs_nonzero() {
1120 let Some(mut b_eo) = try_backend(64, 64) else {
1124 return;
1125 };
1126 let Some(mut b_nz) = try_backend(64, 64) else {
1127 return;
1128 };
1129 let make_path = |fill_rule: FillRule| {
1130 let mut path = PathData::new().with_fill_rule(fill_rule);
1131 path.move_to(Point::new(4.0, 4.0));
1133 path.line_to(Point::new(60.0, 4.0));
1134 path.line_to(Point::new(60.0, 60.0));
1135 path.line_to(Point::new(4.0, 60.0));
1136 path.close();
1137 path.move_to(Point::new(20.0, 20.0));
1139 path.line_to(Point::new(44.0, 20.0));
1140 path.line_to(Point::new(44.0, 44.0));
1141 path.line_to(Point::new(20.0, 44.0));
1142 path.close();
1143 path
1144 };
1145 let green = Color(0, 255, 0, 255);
1146 let mut list_eo = DrawList::new();
1147 list_eo.push_path(make_path(FillRule::EvenOdd), green);
1148 let mut list_nz = DrawList::new();
1149 list_nz.push_path(make_path(FillRule::NonZero), green);
1150 b_eo.execute(&list_eo).expect("execute");
1151 b_nz.execute(&list_nz).expect("execute");
1152 let inner_eo = b_eo.read_pixel(32, 32).expect("read").expect("pixel");
1154 let inner_nz = b_nz.read_pixel(32, 32).expect("read").expect("pixel");
1155 assert_eq!(
1157 inner_eo.3, 0,
1158 "EvenOdd: same-winding inner ring must be transparent (depth=1 = hole)"
1159 );
1160 assert_eq!(
1162 inner_nz.3, 255,
1163 "NonZero: same-winding inner ring must be opaque (winding=2 ≠ 0)"
1164 );
1165 }
1166
1167 #[test]
1170 fn culled_offscreen_rect_is_transparent() {
1171 let Some(mut b) = try_backend(64, 64) else {
1175 return;
1176 };
1177 let mut list = DrawList::new();
1178 list.push_clip(Rect::new(0.0, 0.0, 32.0, 32.0));
1180 list.push_rect(Rect::new(40.0, 40.0, 20.0, 20.0), Color(255, 0, 0, 255));
1182 list.pop_clip();
1183 b.execute(&list).expect("execute");
1184 let px = b.read_pixel(45, 45).expect("read").expect("pixel");
1186 assert_eq!(px.3, 0, "rect outside clip must be culled (transparent)");
1187 let px2 = b.read_pixel(10, 10).expect("read").expect("pixel");
1189 assert_eq!(px2.3, 0, "undrawn area must remain transparent");
1190 }
1191
1192 #[test]
1193 fn culling_does_not_affect_visible_rect() {
1194 let Some(mut b) = try_backend(64, 64) else {
1197 return;
1198 };
1199 let mut list = DrawList::new();
1200 list.push_rect(Rect::new(10.0, 10.0, 40.0, 40.0), Color(0, 255, 0, 255));
1201 b.execute(&list).expect("execute");
1202 let px = b.read_pixel(30, 30).expect("read").expect("pixel");
1203 assert_eq!(
1204 (px.0, px.1, px.2, px.3),
1205 (0, 255, 0, 255),
1206 "visible rect must not be culled"
1207 );
1208 }
1209
1210 #[test]
1213 fn frame_stats_counts_solid_draws() {
1214 let Some(mut backend) = try_backend(64, 64) else {
1215 return;
1216 };
1217 let mut list = DrawList::new();
1219 list.push(DrawCommand::FillRect {
1220 rect: Rect::new(10.0, 10.0, 44.0, 44.0),
1221 color: Color(255, 0, 0, 255),
1222 });
1223 backend.execute(&list).expect("execute failed");
1224 let stats = backend.frame_stats();
1225 assert!(stats.draw_calls >= 1, "should have at least 1 draw call");
1226 assert!(
1227 stats.render_passes >= 1,
1228 "should have at least 1 render pass"
1229 );
1230 }
1231
1232 #[test]
1235 fn two_gradients_one_pass() {
1236 let Some(mut backend) = try_backend(128, 64) else {
1237 return;
1238 };
1239 let mut list = DrawList::new();
1240 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 list.push(DrawCommand::LinearGradient {
1252 rect: Rect::new(64.0, 0.0, 64.0, 64.0),
1253 start: Point::new(64.0, 0.0),
1254 end: Point::new(128.0, 0.0),
1255 stops: vec![
1256 GradientStop::new(0.0, Color(0, 255, 0, 255)),
1257 GradientStop::new(1.0, Color(255, 255, 0, 255)),
1258 ],
1259 });
1260 backend.execute(&list).expect("execute");
1261 let stats = backend.frame_stats();
1262 assert!(
1264 stats.draw_calls >= 2,
1265 "should have at least 2 draw calls for 2 gradients, got {}",
1266 stats.draw_calls
1267 );
1268
1269 let left_px = backend
1271 .read_pixel(2, 32)
1272 .expect("read left")
1273 .expect("bounds");
1274 assert!(left_px.0 > 200, "left should be reddish, got {:?}", left_px);
1275 assert!(
1276 left_px.2 < 100,
1277 "left should not be blue, got {:?}",
1278 left_px
1279 );
1280
1281 let right_px = backend
1283 .read_pixel(66, 32)
1284 .expect("read right")
1285 .expect("bounds");
1286 assert!(
1287 right_px.1 > 200,
1288 "right should be greenish, got {:?}",
1289 right_px
1290 );
1291 assert!(
1292 right_px.0 < 100,
1293 "right should not be red, got {:?}",
1294 right_px
1295 );
1296 }
1297
1298 #[test]
1299 fn gradient_byte_exact_single() {
1300 let Some(mut backend) = try_backend(64, 64) else {
1303 return;
1304 };
1305 let mut list = DrawList::new();
1306 list.push(DrawCommand::LinearGradient {
1307 rect: Rect::new(0.0, 0.0, 64.0, 64.0),
1308 start: Point::new(0.0, 0.0),
1309 end: Point::new(64.0, 0.0),
1310 stops: vec![
1311 GradientStop::new(0.0, Color(255, 0, 0, 255)),
1312 GradientStop::new(1.0, Color(0, 0, 255, 255)),
1313 ],
1314 });
1315 backend.execute(&list).expect("execute");
1316 let left = backend
1318 .read_pixel(1, 32)
1319 .expect("read left")
1320 .expect("bounds");
1321 assert!(
1322 left.0 > 200 && left.2 < 100,
1323 "left should be reddish: {:?}",
1324 left
1325 );
1326 let right = backend
1328 .read_pixel(62, 32)
1329 .expect("read right")
1330 .expect("bounds");
1331 assert!(
1332 right.2 > 200 && right.0 < 100,
1333 "right should be bluish: {:?}",
1334 right
1335 );
1336 let mid = backend
1338 .read_pixel(32, 32)
1339 .expect("read mid")
1340 .expect("bounds");
1341 assert!(mid.3 > 0, "mid should be visible: {:?}", mid);
1342 }
1343
1344 #[test]
1345 fn persistent_buffer_reuse_stable() {
1346 let Some(mut backend) = try_backend(64, 64) else {
1349 return;
1350 };
1351
1352 let mut list1 = DrawList::new();
1354 for i in 0..10u32 {
1355 list1.push(DrawCommand::FillRect {
1356 rect: Rect::new(i as f32 * 4.0, 0.0, 4.0, 64.0),
1357 color: Color(255, 0, 0, 255),
1358 });
1359 }
1360 backend.execute(&list1).expect("frame 1");
1361 let px1 = backend
1362 .read_pixel(2, 32)
1363 .expect("frame 1 pixel")
1364 .expect("bounds");
1365 assert_eq!(px1.0, 255, "frame 1 should be red: {:?}", px1);
1366 assert_eq!(px1.3, 255, "frame 1 should be opaque: {:?}", px1);
1367
1368 let mut list2 = DrawList::new();
1370 list2.push(DrawCommand::FillRect {
1371 rect: Rect::new(0.0, 0.0, 64.0, 64.0),
1372 color: Color(0, 0, 255, 255),
1373 });
1374 backend.execute(&list2).expect("frame 2");
1375 let px2 = backend
1376 .read_pixel(32, 32)
1377 .expect("frame 2 pixel")
1378 .expect("bounds");
1379 assert_eq!(px2.2, 255, "frame 2 should be blue: {:?}", px2);
1380 assert!(
1382 px2.0 < 10,
1383 "frame 2 stale-tail check: should not see red, got {:?}",
1384 px2
1385 );
1386 }
1387}