1use wgpu;
5
6use teksilo_canvas::RenderFrame;
7use teksilo_canvas::geometry::Transform2D;
8
9use crate::blur::{BlurPipelines, BlurPool};
10use crate::image_manager::ImageManager;
11use crate::path_atlas::PathAtlas;
12use crate::stream_buffer::StreamBuffers;
13use crate::vertex::{AnimQuadVertex, QuadVertex, RectVertex, SdfVertex, ShadowVertex};
14
15const MAX_ANIM_SLOTS: usize = 128;
20
21pub struct Renderer {
23 device: wgpu::Device,
24 queue: wgpu::Queue,
25 max_texture_dimension: u32,
31 rect_pipeline: wgpu::RenderPipeline,
32 sdf_pipeline: wgpu::RenderPipeline,
33 quad_pipeline: wgpu::RenderPipeline,
34 shadow_pipeline: wgpu::RenderPipeline,
35 path_gradient_pipeline: wgpu::RenderPipeline,
42 anim_proc_pipeline: wgpu::RenderPipeline,
46 anim_sprite_pipeline: wgpu::RenderPipeline,
54 anim_uniform_buffer: wgpu::Buffer,
59 anim_uniform_bind_group: wgpu::BindGroup,
61 atlas_texture: Option<AtlasTexture>,
62 path_atlas: PathAtlas,
63 path_atlas_texture: Option<AtlasTexture>,
64 image_manager: ImageManager,
65 streams: StreamBuffers,
70 blur_pipelines: BlurPipelines,
74 blur_pool: BlurPool,
78 quad_bind_group_layout: wgpu::BindGroupLayout,
83 blur_composite_sampler: wgpu::Sampler,
87}
88
89struct AtlasTexture {
90 texture: wgpu::Texture,
91 bind_group: wgpu::BindGroup,
92 width: u32,
93 height: u32,
94}
95
96struct ActiveTarget {
103 intermediate: Option<crate::blur::AcquiredTexture>,
106 viewport_w: u32,
108 viewport_h: u32,
109 opened: bool,
112 pending_composites: Vec<PendingComposite>,
116 clip_stack: Vec<[u32; 4]>,
123 blur_bounds: Option<teksilo_canvas::Rect>,
127 blur_radius_logical: Option<f32>,
128 used_w: Option<u32>,
129 used_h: Option<u32>,
130 bucket_w: Option<u32>,
131 bucket_h: Option<u32>,
132}
133
134impl ActiveTarget {
135 fn surface(viewport_w: u32, viewport_h: u32) -> Self {
136 Self {
137 intermediate: None,
138 viewport_w,
139 viewport_h,
140 opened: false,
141 pending_composites: Vec::new(),
142 clip_stack: Vec::new(),
143 blur_bounds: None,
144 blur_radius_logical: None,
145 used_w: None,
146 used_h: None,
147 bucket_w: None,
148 bucket_h: None,
149 }
150 }
151}
152
153struct PendingComposite {
157 blurred_texture: crate::blur::AcquiredTexture,
158 used_w: u32,
159 used_h: u32,
160 bucket_w: u32,
161 bucket_h: u32,
162 bounds: teksilo_canvas::Rect,
163}
164
165impl Renderer {
166 pub fn new(
168 device: wgpu::Device,
169 queue: wgpu::Queue,
170 surface_format: wgpu::TextureFormat,
171 ) -> Self {
172 let device_max_texture_dimension = device.limits().max_texture_dimension_2d;
175 let rect_pipeline = create_rect_pipeline(&device, surface_format);
176 let sdf_pipeline = create_sdf_pipeline(&device, surface_format);
177 let quad_pipeline = create_quad_pipeline(&device, surface_format);
178 let path_gradient_pipeline = create_path_gradient_pipeline(
182 &device,
183 surface_format,
184 &quad_pipeline.get_bind_group_layout(0),
185 );
186 let shadow_pipeline = create_shadow_pipeline(&device, surface_format);
187 let (anim_proc_pipeline, anim_uniform_buffer, anim_uniform_bind_group, anim_uniform_layout) =
188 create_anim_proc_pipeline(&device, surface_format);
189 let quad_texture_layout = quad_pipeline.get_bind_group_layout(0);
193 let anim_sprite_pipeline = create_anim_sprite_pipeline(
194 &device,
195 surface_format,
196 &anim_uniform_layout,
197 &quad_texture_layout,
198 );
199
200 let quad_bind_group_layout = quad_pipeline.get_bind_group_layout(0);
201 let blur_pool = BlurPool::new(&device, surface_format);
202 let blur_pipelines =
203 BlurPipelines::new(&device, &blur_pool.bind_group_layout, surface_format);
204 let blur_composite_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
205 label: Some("blur_composite_sampler"),
206 address_mode_u: wgpu::AddressMode::ClampToEdge,
207 address_mode_v: wgpu::AddressMode::ClampToEdge,
208 address_mode_w: wgpu::AddressMode::ClampToEdge,
209 mag_filter: wgpu::FilterMode::Linear,
210 min_filter: wgpu::FilterMode::Linear,
211 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
212 ..Default::default()
213 });
214
215 Self {
216 device,
217 queue,
218 max_texture_dimension: device_max_texture_dimension,
219 rect_pipeline,
220 sdf_pipeline,
221 quad_pipeline,
222 path_gradient_pipeline,
223 shadow_pipeline,
224 anim_proc_pipeline,
225 anim_sprite_pipeline,
226 anim_uniform_buffer,
227 anim_uniform_bind_group,
228 atlas_texture: None,
229 path_atlas: {
230 let mut atlas = PathAtlas::new(512, 512);
231 atlas.cap_max_size(device_max_texture_dimension);
232 atlas
233 },
234 path_atlas_texture: None,
235 image_manager: ImageManager::new(),
236 streams: StreamBuffers::new(),
237 blur_pipelines,
238 blur_pool,
239 quad_bind_group_layout,
240 blur_composite_sampler,
241 }
242 }
243
244 pub fn upload_atlas(&mut self, width: u32, height: u32, pixels: &[u8]) {
246 if width == 0 || height == 0 {
247 return;
248 }
249 if width > self.max_texture_dimension || height > self.max_texture_dimension {
257 eprintln!(
258 "teksilo-render: glyph atlas {width}x{height} exceeds this device's \
259 max texture dimension ({}); skipping upload",
260 self.max_texture_dimension
261 );
262 return;
263 }
264
265 let needs_recreate = self
266 .atlas_texture
267 .as_ref()
268 .is_none_or(|t| t.width != width || t.height != height);
269
270 if needs_recreate {
271 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
272 label: Some("glyph_atlas"),
273 size: wgpu::Extent3d {
274 width,
275 height,
276 depth_or_array_layers: 1,
277 },
278 mip_level_count: 1,
279 sample_count: 1,
280 dimension: wgpu::TextureDimension::D2,
281 format: wgpu::TextureFormat::Rgba8UnormSrgb,
282 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
283 view_formats: &[],
284 });
285
286 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
287 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
299 mag_filter: wgpu::FilterMode::Linear,
300 min_filter: wgpu::FilterMode::Linear,
301 ..Default::default()
302 });
303
304 let bind_group_layout = self.quad_pipeline.get_bind_group_layout(0);
305 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
306 label: Some("atlas_bind_group"),
307 layout: &bind_group_layout,
308 entries: &[
309 wgpu::BindGroupEntry {
310 binding: 0,
311 resource: wgpu::BindingResource::TextureView(&view),
312 },
313 wgpu::BindGroupEntry {
314 binding: 1,
315 resource: wgpu::BindingResource::Sampler(&sampler),
316 },
317 ],
318 });
319
320 self.atlas_texture = Some(AtlasTexture {
321 texture,
322 bind_group,
323 width,
324 height,
325 });
326 }
327
328 if let Some(atlas) = &self.atlas_texture {
329 self.queue.write_texture(
330 wgpu::TexelCopyTextureInfo {
331 texture: &atlas.texture,
332 mip_level: 0,
333 origin: wgpu::Origin3d::ZERO,
334 aspect: wgpu::TextureAspect::All,
335 },
336 pixels,
337 wgpu::TexelCopyBufferLayout {
338 offset: 0,
339 bytes_per_row: Some(width * 4),
340 rows_per_image: Some(height),
341 },
342 wgpu::Extent3d {
343 width,
344 height,
345 depth_or_array_layers: 1,
346 },
347 );
348 }
349 }
350
351 pub fn render(
353 &mut self,
354 frame: &RenderFrame,
355 view: &wgpu::TextureView,
356 scale_factor: f32,
357 viewport_width: u32,
358 viewport_height: u32,
359 clear_color: [f32; 4],
360 ) {
361 self.path_atlas.begin_frame();
363 self.blur_pool.begin_frame();
366 let blur_scopes = frame
367 .draw_order
368 .iter()
369 .filter(|c| matches!(c, teksilo_canvas::DrawCommand::BeginBlurredSubtree { .. }))
370 .count();
371 self.blur_pipelines.params.begin_frame(
372 &self.device,
373 u32::try_from(blur_scopes)
374 .unwrap_or(u32::MAX)
375 .saturating_mul(crate::blur::PARAM_SLOTS_PER_SCOPE),
376 );
377
378 for pending in &frame.pending_images {
380 if !self.image_manager.contains(&pending.name) {
381 let layout = self.quad_pipeline.get_bind_group_layout(0);
382 self.image_manager.register_image(
383 &pending.name,
384 pending.width,
385 pending.height,
386 &pending.pixels,
387 &self.device,
388 &self.queue,
389 &layout,
390 );
391 }
392 }
393
394 let mut path_placements: Vec<Option<crate::path_atlas::PathPlacement>> =
406 vec![None; frame.paths.len()];
407 {
408 let mut ptf_stack: Vec<Transform2D> = vec![Transform2D::IDENTITY];
409 let mut ptf_current = Transform2D::IDENTITY;
410 let device_t = |t: &Transform2D| Transform2D {
411 m: [
412 t.m[0],
413 t.m[1],
414 t.m[2],
415 t.m[3],
416 t.m[4] * scale_factor,
417 t.m[5] * scale_factor,
418 ],
419 };
420 for cmd in &frame.draw_order {
421 match cmd {
422 teksilo_canvas::DrawCommand::SetTransform(t) => {
423 let stack_top = ptf_stack.last().copied().unwrap_or(Transform2D::IDENTITY);
424 ptf_current = device_t(t).then(&stack_top);
425 }
426 teksilo_canvas::DrawCommand::PushTransform(t) => {
427 let prev_top = ptf_stack.last().copied().unwrap_or(Transform2D::IDENTITY);
428 let new_top = device_t(t).then(&prev_top);
429 ptf_stack.push(new_top);
430 ptf_current = new_top;
431 }
432 teksilo_canvas::DrawCommand::PopTransform => {
433 if ptf_stack.len() > 1 {
434 ptf_stack.pop();
435 }
436 ptf_current = ptf_stack.last().copied().unwrap_or(Transform2D::IDENTITY);
437 }
438 teksilo_canvas::DrawCommand::Path(idx) => {
439 if let Some(entry) = frame.paths.get(*idx) {
440 let zoom = ptf_current.m[0].hypot(ptf_current.m[1]);
444 let snap = ptf_current == Transform2D::IDENTITY;
454 path_placements[*idx] = self.path_atlas.lookup_or_rasterize(
455 &entry.path,
456 &entry.stroke_style,
457 entry.fill_rule,
458 entry.bounds,
459 scale_factor,
460 zoom,
461 snap,
462 );
463 }
464 }
465 _ => {}
466 }
467 }
468 }
469
470 if self.path_atlas.is_dirty() {
472 let (pw, ph) = self.path_atlas.size();
473 self.upload_path_atlas(pw, ph, self.path_atlas.pixels().to_vec());
474 self.path_atlas.mark_clean();
475 }
476
477 let counts = stream_quad_counts(frame);
479 let StreamQuadCounts {
480 rect: rect_quads,
481 sdf: sdf_quads,
482 quad: quad_quads,
483 shadow: shadow_quads,
484 anim_proc: anim_proc_quads,
485 path_gradient: path_gradient_quads,
486 } = counts;
487 let max_quads = counts.max();
488
489 self.streams.rect.ensure_capacity(
490 &self.device,
491 (rect_quads * 4 * std::mem::size_of::<RectVertex>()) as u64,
492 );
493 self.streams.sdf.ensure_capacity(
494 &self.device,
495 (sdf_quads * 4 * std::mem::size_of::<SdfVertex>()) as u64,
496 );
497 self.streams.quad.ensure_capacity(
498 &self.device,
499 (quad_quads * 4 * std::mem::size_of::<QuadVertex>()) as u64,
500 );
501 self.streams.shadow.ensure_capacity(
502 &self.device,
503 (shadow_quads * 4 * std::mem::size_of::<ShadowVertex>()) as u64,
504 );
505 self.streams.anim_proc.ensure_capacity(
506 &self.device,
507 (anim_proc_quads * 4 * std::mem::size_of::<AnimQuadVertex>()) as u64,
508 );
509 self.streams.path_gradient.ensure_capacity(
510 &self.device,
511 (path_gradient_quads * 4 * std::mem::size_of::<crate::vertex::PathGradientVertex>())
512 as u64,
513 );
514 self.streams.index.ensure_capacity(
515 &self.device,
516 (max_quads * 6 * std::mem::size_of::<u32>()) as u64,
517 );
518 self.streams.reset();
519
520 if !frame.anim_params.is_empty() {
526 let n = frame.anim_params.len().min(MAX_ANIM_SLOTS);
527 debug_assert!(
528 frame.anim_params.len() <= MAX_ANIM_SLOTS,
529 "AnimParams exceeds MAX_ANIM_SLOTS ({}); tail will be dropped",
530 MAX_ANIM_SLOTS
531 );
532 let bytes: &[u8] = bytemuck::cast_slice(&frame.anim_params[..n]);
533 self.queue.write_buffer(&self.anim_uniform_buffer, 0, bytes);
534 }
535
536 let index_data: Vec<u32> = crate::vertex::generate_quad_indices(max_quads);
540 let index_binding = self
541 .streams
542 .index
543 .write(&self.queue, bytemuck::cast_slice(&index_data));
544
545 let mut encoder = self
546 .device
547 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
548 label: Some("teksilo_render"),
549 });
550
551 let mut viewport_width = viewport_width;
555 let mut viewport_height = viewport_height;
556
557 {
558 let surface_clear_color = wgpu::Color {
559 r: clear_color[0] as f64,
560 g: clear_color[1] as f64,
561 b: clear_color[2] as f64,
562 a: clear_color[3] as f64,
563 };
564
565 let mut target_stack: Vec<ActiveTarget> =
576 vec![ActiveTarget::surface(viewport_width, viewport_height)];
577
578 let mut opacity_stack: Vec<f32> = vec![1.0];
580 let mut current_opacity: f32 = 1.0;
581
582 let mut blend_stack: Vec<teksilo_canvas::BlendMode> = Vec::new();
584 let mut current_blend = teksilo_canvas::BlendMode::Normal;
585 let _ = current_blend; let mut transform_stack: Vec<Transform2D> = vec![Transform2D::IDENTITY];
593 let mut current_transform = Transform2D::IDENTITY;
594
595 let mut rect_batch: Vec<RectVertex> = Vec::new();
600 let mut sdf_batch: Vec<SdfVertex> = Vec::new();
601 let mut quad_batch: Vec<QuadVertex> = Vec::new();
602 let mut shadow_batch: Vec<ShadowVertex> = Vec::new();
603 let mut anim_proc_batch: Vec<AnimQuadVertex> = Vec::new();
604 let mut path_gradient_batch: Vec<crate::vertex::PathGradientVertex> = Vec::new();
605
606 #[derive(Clone, Copy, PartialEq, Eq)]
610 enum QuadSource {
611 GlyphAtlas,
612 PathAtlas,
613 }
614 let mut quad_source: Option<QuadSource> = None;
615
616 macro_rules! flush_stream {
624 ($pass:expr, $queue:expr, $stream:expr, $pipeline:expr,
625 $batch:expr, $index_binding:expr) => {
626 if !$batch.is_empty() {
627 let bytes: &[u8] = bytemuck::cast_slice(&$batch);
628 if let (Some((vb, v_off, v_len)), Some((ib, _, _))) =
629 ($stream.write($queue, bytes), $index_binding)
630 {
631 let quads = ($batch.len() / 4) as u32;
632 let index_count = quads * 6;
633 let index_bytes = (index_count as u64) * 4;
634 $pass.set_pipeline($pipeline);
635 $pass.set_vertex_buffer(0, vb.slice(v_off..v_off + v_len));
636 $pass.set_index_buffer(
637 ib.slice(0..index_bytes),
638 wgpu::IndexFormat::Uint32,
639 );
640 $pass.draw_indexed(0..index_count, 0, 0..1);
641 }
642 $batch.clear();
643 }
644 };
645 }
646
647 macro_rules! flush_all {
649 ($pass:expr, $queue:expr, $streams:expr,
650 $rp:expr, $sp:expr, $qp:expr, $pgp:expr, $shp:expr,
651 $rb:expr, $sb:expr, $qb:expr, $pgb:expr, $shb:expr,
652 $atlas:expr, $path_atlas:expr, $qs:expr, $index_binding:expr) => {
653 flush_stream!($pass, $queue, &$streams.rect, $rp, $rb, $index_binding);
654 flush_stream!($pass, $queue, &$streams.sdf, $sp, $sb, $index_binding);
655 if !$qb.is_empty() {
657 let bg = match $qs {
658 Some(QuadSource::PathAtlas) => {
659 $path_atlas.as_ref().map(|a: &AtlasTexture| &a.bind_group)
660 }
661 _ => $atlas.as_ref().map(|a: &AtlasTexture| &a.bind_group),
662 };
663 if let (Some(bind_group), Some((ib, _, _))) = (bg, $index_binding) {
664 let bytes: &[u8] = bytemuck::cast_slice(&$qb);
665 if let Some((vb, v_off, v_len)) = $streams.quad.write($queue, bytes) {
666 let quads = ($qb.len() / 4) as u32;
667 let index_count = quads * 6;
668 let index_bytes = (index_count as u64) * 4;
669 $pass.set_pipeline($qp);
670 $pass.set_bind_group(0, bind_group, &[]);
671 $pass.set_vertex_buffer(0, vb.slice(v_off..v_off + v_len));
672 $pass.set_index_buffer(
673 ib.slice(0..index_bytes),
674 wgpu::IndexFormat::Uint32,
675 );
676 $pass.draw_indexed(0..index_count, 0, 0..1);
677 }
678 }
679 $qb.clear();
680 }
681 if !$pgb.is_empty() {
687 if let (Some(bind_group), Some((ib, _, _))) = (
688 $path_atlas.as_ref().map(|a: &AtlasTexture| &a.bind_group),
689 $index_binding,
690 ) {
691 let bytes: &[u8] = bytemuck::cast_slice(&$pgb);
692 if let Some((vb, v_off, v_len)) =
693 $streams.path_gradient.write($queue, bytes)
694 {
695 let quads = ($pgb.len() / 4) as u32;
696 let index_count = quads * 6;
697 let index_bytes = (index_count as u64) * 4;
698 $pass.set_pipeline($pgp);
699 $pass.set_bind_group(0, bind_group, &[]);
700 $pass.set_vertex_buffer(0, vb.slice(v_off..v_off + v_len));
701 $pass.set_index_buffer(
702 ib.slice(0..index_bytes),
703 wgpu::IndexFormat::Uint32,
704 );
705 $pass.draw_indexed(0..index_count, 0, 0..1);
706 }
707 }
708 $pgb.clear();
709 }
710 flush_stream!($pass, $queue, &$streams.shadow, $shp, $shb, $index_binding);
711 if !anim_proc_batch.is_empty()
720 && let Some((ib, _, _)) = $index_binding
721 {
722 let bytes: &[u8] = bytemuck::cast_slice(&anim_proc_batch);
723 if let Some((vb, v_off, v_len)) = $streams.anim_proc.write($queue, bytes) {
724 let quads = (anim_proc_batch.len() / 4) as u32;
725 let index_count = quads * 6;
726 let index_bytes = (index_count as u64) * 4;
727 $pass.set_pipeline(&self.anim_proc_pipeline);
728 $pass.set_bind_group(0, &self.anim_uniform_bind_group, &[]);
729 $pass.set_vertex_buffer(0, vb.slice(v_off..v_off + v_len));
730 $pass.set_index_buffer(
731 ib.slice(0..index_bytes),
732 wgpu::IndexFormat::Uint32,
733 );
734 $pass.draw_indexed(0..index_count, 0, 0..1);
735 }
736 anim_proc_batch.clear();
737 }
738 };
739 }
740
741 let mut cmd_idx = 0;
748 while cmd_idx <= frame.draw_order.len() {
749 let (target_view, load_op): (&wgpu::TextureView, wgpu::LoadOp<wgpu::Color>) = {
753 let t = target_stack
754 .last_mut()
755 .expect("surface target always present");
756 let v: &wgpu::TextureView = match t.intermediate {
757 Some(h) => self.blur_pool.view(h),
758 None => view,
759 };
760 let lo = if t.opened {
761 wgpu::LoadOp::Load
762 } else if t.intermediate.is_none() {
763 wgpu::LoadOp::Clear(surface_clear_color)
764 } else {
765 wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT)
766 };
767 t.opened = true;
768 viewport_width = t.viewport_w;
769 viewport_height = t.viewport_h;
770 (v, lo)
771 };
772
773 let composites_to_draw: Vec<PendingComposite> = std::mem::take(
779 &mut target_stack
780 .last_mut()
781 .expect("target_stack always has the surface target")
782 .pending_composites,
783 );
784
785 {
786 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
787 label: Some("teksilo_segment_pass"),
788 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
789 view: target_view,
790 resolve_target: None,
791 ops: wgpu::Operations {
792 load: load_op,
793 store: wgpu::StoreOp::Store,
794 },
795 depth_slice: None,
796 })],
797 depth_stencil_attachment: None,
798 timestamp_writes: None,
799 occlusion_query_set: None,
800 multiview_mask: None,
801 });
802
803 if let Some(&[x, y, w, h]) = target_stack
809 .last()
810 .expect("target_stack always has the surface target")
811 .clip_stack
812 .last()
813 {
814 pass.set_scissor_rect(x, y, w, h);
815 }
816
817 for pc in &composites_to_draw {
819 composite_blur_quad(
820 &self.device,
821 &self.queue,
822 &mut pass,
823 &self.blur_pool,
824 &self.quad_pipeline,
825 &self.quad_bind_group_layout,
826 &self.blur_composite_sampler,
827 &self.streams.quad,
828 index_binding,
829 pc.blurred_texture,
830 pc.used_w,
831 pc.used_h,
832 pc.bucket_w,
833 pc.bucket_h,
834 pc.bounds,
835 scale_factor,
836 viewport_width,
837 viewport_height,
838 );
839 quad_source = None;
844 }
845
846 let pass = &mut pass;
847
848 while cmd_idx < frame.draw_order.len() {
851 let cmd = &frame.draw_order[cmd_idx];
852 if matches!(
853 cmd,
854 teksilo_canvas::DrawCommand::BeginBlurredSubtree { .. }
855 | teksilo_canvas::DrawCommand::EndBlurredSubtree
856 ) {
857 break;
858 }
859 match cmd {
860 teksilo_canvas::DrawCommand::Decoration(idx) => {
861 flush_all!(
862 pass,
863 &self.queue,
864 self.streams,
865 &self.rect_pipeline,
866 &self.sdf_pipeline,
867 &self.quad_pipeline,
868 &self.path_gradient_pipeline,
869 &self.shadow_pipeline,
870 rect_batch,
871 sdf_batch,
872 quad_batch,
873 path_gradient_batch,
874 shadow_batch,
875 self.atlas_texture,
876 self.path_atlas_texture,
877 quad_source,
878 index_binding
879 );
880 quad_source = None;
881 let Some(rect) = frame.decorations.get(*idx) else {
882 continue;
883 };
884 let verts = RectVertex::from_decoration(rect, scale_factor);
885 for v in &verts {
886 let tp = apply_transform_pixel(v.position, ¤t_transform);
887 rect_batch.push(RectVertex {
888 position: pixel_to_ndc(tp, viewport_width, viewport_height),
889 color: [
890 v.color[0],
891 v.color[1],
892 v.color[2],
893 v.color[3] * current_opacity,
894 ],
895 });
896 }
897 }
898 teksilo_canvas::DrawCommand::CosmeticLine(idx) => {
899 flush_all!(
900 pass,
901 &self.queue,
902 self.streams,
903 &self.rect_pipeline,
904 &self.sdf_pipeline,
905 &self.quad_pipeline,
906 &self.path_gradient_pipeline,
907 &self.shadow_pipeline,
908 rect_batch,
909 sdf_batch,
910 quad_batch,
911 path_gradient_batch,
912 shadow_batch,
913 self.atlas_texture,
914 self.path_atlas_texture,
915 quad_source,
916 index_binding
917 );
918 quad_source = None;
919 let Some(line) = frame.cosmetic_lines.get(*idx) else {
920 continue;
921 };
922 let p0 = apply_transform_pixel(
927 [line.from[0] * scale_factor, line.from[1] * scale_factor],
928 ¤t_transform,
929 );
930 let p1 = apply_transform_pixel(
931 [line.to[0] * scale_factor, line.to[1] * scale_factor],
932 ¤t_transform,
933 );
934 let thickness = (line.width * scale_factor).max(1.0);
935 let half = thickness * 0.5;
936 let dx = p1[0] - p0[0];
937 let dy = p1[1] - p0[1];
938 let len = (dx * dx + dy * dy).sqrt();
939 if len < 1e-3 {
940 continue;
941 }
942 let nx = -dy / len;
944 let ny = dx / len;
945 let (mut a0, mut a1) = (p0, p1);
948 if dy.abs() < 0.5 {
949 let cy = ((p0[1] + p1[1]) * 0.5 - half).round() + half;
950 a0 = [p0[0], cy];
951 a1 = [p1[0], cy];
952 } else if dx.abs() < 0.5 {
953 let cx = ((p0[0] + p1[0]) * 0.5 - half).round() + half;
954 a0 = [cx, p0[1]];
955 a1 = [cx, p1[1]];
956 }
957 let lin = crate::vertex::srgb_to_linear_rgba(line.color);
958 let color = [lin[0], lin[1], lin[2], lin[3] * current_opacity];
959 let corners = [
960 [a0[0] + nx * half, a0[1] + ny * half],
961 [a1[0] + nx * half, a1[1] + ny * half],
962 [a1[0] - nx * half, a1[1] - ny * half],
963 [a0[0] - nx * half, a0[1] - ny * half],
964 ];
965 for pos in corners {
966 rect_batch.push(RectVertex {
967 position: pixel_to_ndc(
968 pos,
969 viewport_width,
970 viewport_height,
971 ),
972 color,
973 });
974 }
975 }
976 teksilo_canvas::DrawCommand::Shape(idx) => {
977 flush_all!(
978 pass,
979 &self.queue,
980 self.streams,
981 &self.rect_pipeline,
982 &self.sdf_pipeline,
983 &self.quad_pipeline,
984 &self.path_gradient_pipeline,
985 &self.shadow_pipeline,
986 rect_batch,
987 sdf_batch,
988 quad_batch,
989 path_gradient_batch,
990 shadow_batch,
991 self.atlas_texture,
992 self.path_atlas_texture,
993 quad_source,
994 index_binding
995 );
996 quad_source = None;
997 let Some(shape) = frame.shapes.get(*idx) else {
998 continue;
999 };
1000 let verts = if shape.stroke_space
1009 == teksilo_canvas::StrokeSpace::Device
1010 && shape.stroke_width > 0.0
1011 {
1012 let zoom = current_transform.m[0].hypot(current_transform.m[1]);
1017 SdfVertex::from_shape_quad_cosmetic(shape, scale_factor, zoom)
1018 } else {
1019 SdfVertex::from_shape_quad(shape, scale_factor)
1020 };
1021 for v in &verts {
1022 let tp = apply_transform_pixel(v.position, ¤t_transform);
1023 sdf_batch.push(SdfVertex {
1024 position: pixel_to_ndc(tp, viewport_width, viewport_height),
1025 color: [
1026 v.color[0],
1027 v.color[1],
1028 v.color[2],
1029 v.color[3] * current_opacity,
1030 ],
1031 ..*v
1032 });
1033 }
1034 }
1035 teksilo_canvas::DrawCommand::Glyph(idx) => {
1036 if quad_source != Some(QuadSource::GlyphAtlas) {
1039 flush_all!(
1040 pass,
1041 &self.queue,
1042 self.streams,
1043 &self.rect_pipeline,
1044 &self.sdf_pipeline,
1045 &self.quad_pipeline,
1046 &self.path_gradient_pipeline,
1047 &self.shadow_pipeline,
1048 rect_batch,
1049 sdf_batch,
1050 quad_batch,
1051 path_gradient_batch,
1052 shadow_batch,
1053 self.atlas_texture,
1054 self.path_atlas_texture,
1055 quad_source,
1056 index_binding
1057 );
1058 quad_source = Some(QuadSource::GlyphAtlas);
1059 }
1060 if let Some(atlas) = &self.atlas_texture {
1061 let Some(glyph) = frame.glyphs.get(*idx) else {
1062 continue;
1063 };
1064 let verts = QuadVertex::from_glyph_quad_transformed(
1067 glyph,
1068 scale_factor,
1069 atlas.width,
1070 atlas.height,
1071 ¤t_transform,
1072 );
1073 for v in &verts {
1074 quad_batch.push(QuadVertex {
1075 position: pixel_to_ndc(
1076 v.position,
1077 viewport_width,
1078 viewport_height,
1079 ),
1080 color: [
1081 v.color[0],
1082 v.color[1],
1083 v.color[2],
1084 v.color[3] * current_opacity,
1085 ],
1086 ..*v
1087 });
1088 }
1089 }
1090 }
1091 teksilo_canvas::DrawCommand::Shadow(idx) => {
1092 flush_all!(
1093 pass,
1094 &self.queue,
1095 self.streams,
1096 &self.rect_pipeline,
1097 &self.sdf_pipeline,
1098 &self.quad_pipeline,
1099 &self.path_gradient_pipeline,
1100 &self.shadow_pipeline,
1101 rect_batch,
1102 sdf_batch,
1103 quad_batch,
1104 path_gradient_batch,
1105 shadow_batch,
1106 self.atlas_texture,
1107 self.path_atlas_texture,
1108 quad_source,
1109 index_binding
1110 );
1111 quad_source = None;
1112 let Some(shadow) = frame.shadows.get(*idx) else {
1113 continue;
1114 };
1115 let verts = ShadowVertex::from_shadow_quad(shadow, scale_factor);
1116 for v in &verts {
1117 let tp = apply_transform_pixel(v.position, ¤t_transform);
1118 shadow_batch.push(ShadowVertex {
1119 position: pixel_to_ndc(tp, viewport_width, viewport_height),
1120 shadow_color: [
1121 v.shadow_color[0],
1122 v.shadow_color[1],
1123 v.shadow_color[2],
1124 v.shadow_color[3] * current_opacity,
1125 ],
1126 ..*v
1127 });
1128 }
1129 }
1130 teksilo_canvas::DrawCommand::Image(idx) => {
1131 flush_all!(
1133 pass,
1134 &self.queue,
1135 self.streams,
1136 &self.rect_pipeline,
1137 &self.sdf_pipeline,
1138 &self.quad_pipeline,
1139 &self.path_gradient_pipeline,
1140 &self.shadow_pipeline,
1141 rect_batch,
1142 sdf_batch,
1143 quad_batch,
1144 path_gradient_batch,
1145 shadow_batch,
1146 self.atlas_texture,
1147 self.path_atlas_texture,
1148 quad_source,
1149 index_binding
1150 );
1151 quad_source = None;
1152 let Some(image) = frame.images.get(*idx) else {
1153 continue;
1154 };
1155 self.draw_image(
1156 pass,
1157 image,
1158 scale_factor,
1159 viewport_width,
1160 viewport_height,
1161 current_opacity,
1162 ¤t_transform,
1163 index_binding,
1164 );
1165 }
1166 teksilo_canvas::DrawCommand::Path(idx) => {
1167 flush_all!(
1168 pass,
1169 &self.queue,
1170 self.streams,
1171 &self.rect_pipeline,
1172 &self.sdf_pipeline,
1173 &self.quad_pipeline,
1174 &self.path_gradient_pipeline,
1175 &self.shadow_pipeline,
1176 rect_batch,
1177 sdf_batch,
1178 quad_batch,
1179 path_gradient_batch,
1180 shadow_batch,
1181 self.atlas_texture,
1182 self.path_atlas_texture,
1183 quad_source,
1184 index_binding
1185 );
1186 quad_source = None;
1187 if let Some(Some(placement)) = path_placements.get(*idx) {
1188 let Some(entry) = frame.paths.get(*idx) else {
1189 continue;
1190 };
1191 let Some(path_atlas) = self.path_atlas_texture.as_ref() else {
1192 continue;
1193 };
1194 if matches!(entry.paint_data, teksilo_canvas::PaintData::Solid)
1195 {
1196 quad_source = Some(QuadSource::PathAtlas);
1204 let verts = path_quad_verts(
1205 entry,
1206 placement,
1207 path_atlas.width,
1208 path_atlas.height,
1209 current_opacity,
1210 ¤t_transform,
1211 );
1212 for v in &verts {
1213 quad_batch.push(QuadVertex {
1214 position: pixel_to_ndc(
1215 v.position,
1216 viewport_width,
1217 viewport_height,
1218 ),
1219 ..*v
1220 });
1221 }
1222 } else {
1223 let verts = path_gradient_quad_verts(
1230 entry,
1231 placement,
1232 scale_factor,
1233 path_atlas.width,
1234 path_atlas.height,
1235 current_opacity,
1236 ¤t_transform,
1237 );
1238 for v in &verts {
1239 path_gradient_batch.push(
1240 crate::vertex::PathGradientVertex {
1241 position: pixel_to_ndc(
1242 v.position,
1243 viewport_width,
1244 viewport_height,
1245 ),
1246 ..*v
1247 },
1248 );
1249 }
1250 }
1251 }
1252 }
1253 teksilo_canvas::DrawCommand::SetClip(rect) => {
1255 flush_all!(
1256 pass,
1257 &self.queue,
1258 self.streams,
1259 &self.rect_pipeline,
1260 &self.sdf_pipeline,
1261 &self.quad_pipeline,
1262 &self.path_gradient_pipeline,
1263 &self.shadow_pipeline,
1264 rect_batch,
1265 sdf_batch,
1266 quad_batch,
1267 path_gradient_batch,
1268 shadow_batch,
1269 self.atlas_texture,
1270 self.path_atlas_texture,
1271 quad_source,
1272 index_binding
1273 );
1274 quad_source = None;
1275 let device = |x: f32, y: f32| {
1301 apply_transform_pixel(
1302 [x * scale_factor, y * scale_factor],
1303 ¤t_transform,
1304 )
1305 };
1306 let p_tl = device(rect.x, rect.y);
1307 let p_tr = device(rect.x + rect.width, rect.y);
1308 let p_bl = device(rect.x, rect.y + rect.height);
1309 let p_br = device(rect.x + rect.width, rect.y + rect.height);
1310 let min_x = p_tl[0].min(p_tr[0]).min(p_bl[0]).min(p_br[0]);
1311 let min_y = p_tl[1].min(p_tr[1]).min(p_bl[1]).min(p_br[1]);
1312 let max_x = p_tl[0].max(p_tr[0]).max(p_bl[0]).max(p_br[0]);
1313 let max_y = p_tl[1].max(p_tr[1]).max(p_bl[1]).max(p_br[1]);
1314 let x = min_x.max(0.0) as u32;
1315 let y = min_y.max(0.0) as u32;
1316 let w = (max_x - min_x).ceil().max(0.0) as u32;
1317 let h = (max_y - min_y).ceil().max(0.0) as u32;
1318 let x = x.min(viewport_width);
1320 let y = y.min(viewport_height);
1321 let w = w.min(viewport_width.saturating_sub(x));
1322 let h = h.min(viewport_height.saturating_sub(y));
1323 let clip_stack = &mut target_stack
1324 .last_mut()
1325 .expect("target_stack always has the surface target")
1326 .clip_stack;
1327 let clipped = if let Some(&[cx, cy, cw, ch]) = clip_stack.last() {
1328 let ix = x.max(cx);
1329 let iy = y.max(cy);
1330 let ir = (x + w).min(cx + cw);
1331 let ib = (y + h).min(cy + ch);
1332 [ix, iy, ir.saturating_sub(ix), ib.saturating_sub(iy)]
1333 } else {
1334 [x, y, w, h]
1335 };
1336 clip_stack.push(clipped);
1337 pass.set_scissor_rect(
1338 clipped[0], clipped[1], clipped[2], clipped[3],
1339 );
1340 }
1341 teksilo_canvas::DrawCommand::ClearClip => {
1342 flush_all!(
1343 pass,
1344 &self.queue,
1345 self.streams,
1346 &self.rect_pipeline,
1347 &self.sdf_pipeline,
1348 &self.quad_pipeline,
1349 &self.path_gradient_pipeline,
1350 &self.shadow_pipeline,
1351 rect_batch,
1352 sdf_batch,
1353 quad_batch,
1354 path_gradient_batch,
1355 shadow_batch,
1356 self.atlas_texture,
1357 self.path_atlas_texture,
1358 quad_source,
1359 index_binding
1360 );
1361 quad_source = None;
1362 let clip_stack = &mut target_stack
1363 .last_mut()
1364 .expect("target_stack always has the surface target")
1365 .clip_stack;
1366 clip_stack.pop();
1367 if let Some(&[x, y, w, h]) = clip_stack.last() {
1368 pass.set_scissor_rect(x, y, w, h);
1369 } else {
1370 pass.set_scissor_rect(0, 0, viewport_width, viewport_height);
1371 }
1372 }
1373 teksilo_canvas::DrawCommand::SetOpacity(opacity) => {
1374 flush_all!(
1375 pass,
1376 &self.queue,
1377 self.streams,
1378 &self.rect_pipeline,
1379 &self.sdf_pipeline,
1380 &self.quad_pipeline,
1381 &self.path_gradient_pipeline,
1382 &self.shadow_pipeline,
1383 rect_batch,
1384 sdf_batch,
1385 quad_batch,
1386 path_gradient_batch,
1387 shadow_batch,
1388 self.atlas_texture,
1389 self.path_atlas_texture,
1390 quad_source,
1391 index_binding
1392 );
1393 quad_source = None;
1394 opacity_stack.push(current_opacity);
1395 current_opacity *= opacity;
1396 }
1397 teksilo_canvas::DrawCommand::RestoreOpacity => {
1398 flush_all!(
1399 pass,
1400 &self.queue,
1401 self.streams,
1402 &self.rect_pipeline,
1403 &self.sdf_pipeline,
1404 &self.quad_pipeline,
1405 &self.path_gradient_pipeline,
1406 &self.shadow_pipeline,
1407 rect_batch,
1408 sdf_batch,
1409 quad_batch,
1410 path_gradient_batch,
1411 shadow_batch,
1412 self.atlas_texture,
1413 self.path_atlas_texture,
1414 quad_source,
1415 index_binding
1416 );
1417 quad_source = None;
1418 current_opacity = opacity_stack.pop().unwrap_or(1.0);
1419 }
1420 teksilo_canvas::DrawCommand::Rasterized(_) => {}
1421 teksilo_canvas::DrawCommand::AnimatedQuad(idx) => {
1422 let Some(draw) = frame.animated_quads.get(*idx) else {
1423 continue;
1424 };
1425 flush_all!(
1428 pass,
1429 &self.queue,
1430 self.streams,
1431 &self.rect_pipeline,
1432 &self.sdf_pipeline,
1433 &self.quad_pipeline,
1434 &self.path_gradient_pipeline,
1435 &self.shadow_pipeline,
1436 rect_batch,
1437 sdf_batch,
1438 quad_batch,
1439 path_gradient_batch,
1440 shadow_batch,
1441 self.atlas_texture,
1442 self.path_atlas_texture,
1443 quad_source,
1444 index_binding
1445 );
1446 quad_source = None;
1447 match &draw.class {
1448 teksilo_canvas::AnimatedQuadClass::Procedural => {
1449 let verts =
1450 AnimQuadVertex::from_animated_quad(draw, scale_factor);
1451 for v in &verts {
1452 let tp = apply_transform_pixel(
1453 v.position,
1454 ¤t_transform,
1455 );
1456 anim_proc_batch.push(AnimQuadVertex {
1457 position: pixel_to_ndc(
1458 tp,
1459 viewport_width,
1460 viewport_height,
1461 ),
1462 uv: v.uv,
1463 slot: v.slot,
1464 _pad: v._pad,
1465 });
1466 }
1467 }
1468 teksilo_canvas::AnimatedQuadClass::Sprite { image_name } => {
1469 let Some(atlas_bg) =
1475 self.image_manager.get_bind_group(image_name)
1476 else {
1477 continue;
1478 };
1479 let verts =
1480 AnimQuadVertex::from_animated_quad(draw, scale_factor);
1481 let mut ndc_verts = [AnimQuadVertex {
1482 position: [0.0; 2],
1483 uv: [0.0; 2],
1484 slot: 0,
1485 _pad: 0,
1486 };
1487 4];
1488 for (i, v) in verts.iter().enumerate() {
1489 let tp = apply_transform_pixel(
1490 v.position,
1491 ¤t_transform,
1492 );
1493 ndc_verts[i] = AnimQuadVertex {
1494 position: pixel_to_ndc(
1495 tp,
1496 viewport_width,
1497 viewport_height,
1498 ),
1499 uv: v.uv,
1500 slot: v.slot,
1501 _pad: v._pad,
1502 };
1503 }
1504 let bytes: &[u8] = bytemuck::cast_slice(&ndc_verts);
1505 if let (Some((vb, v_off, v_len)), Some((ib, _, _))) = (
1506 self.streams.anim_proc.write(&self.queue, bytes),
1507 index_binding,
1508 ) {
1509 let index_bytes: u64 = 6 * 4;
1510 pass.set_pipeline(&self.anim_sprite_pipeline);
1511 pass.set_bind_group(
1512 0,
1513 &self.anim_uniform_bind_group,
1514 &[],
1515 );
1516 pass.set_bind_group(1, atlas_bg, &[]);
1517 pass.set_vertex_buffer(
1518 0,
1519 vb.slice(v_off..v_off + v_len),
1520 );
1521 pass.set_index_buffer(
1522 ib.slice(0..index_bytes),
1523 wgpu::IndexFormat::Uint32,
1524 );
1525 pass.draw_indexed(0..6, 0, 0..1);
1526 }
1527 }
1528 }
1529 }
1530 teksilo_canvas::DrawCommand::SetBlendMode(mode) => {
1531 blend_stack.push(current_blend);
1532 current_blend = *mode;
1533 }
1534 teksilo_canvas::DrawCommand::RestoreBlendMode => {
1535 current_blend = blend_stack
1536 .pop()
1537 .unwrap_or(teksilo_canvas::BlendMode::Normal);
1538 }
1539 teksilo_canvas::DrawCommand::SetTransform(t) => {
1540 flush_all!(
1541 pass,
1542 &self.queue,
1543 self.streams,
1544 &self.rect_pipeline,
1545 &self.sdf_pipeline,
1546 &self.quad_pipeline,
1547 &self.path_gradient_pipeline,
1548 &self.shadow_pipeline,
1549 rect_batch,
1550 sdf_batch,
1551 quad_batch,
1552 path_gradient_batch,
1553 shadow_batch,
1554 self.atlas_texture,
1555 self.path_atlas_texture,
1556 quad_source,
1557 index_binding
1558 );
1559 quad_source = None;
1560 let device_t = Transform2D {
1566 m: [
1567 t.m[0],
1568 t.m[1],
1569 t.m[2],
1570 t.m[3],
1571 t.m[4] * scale_factor,
1572 t.m[5] * scale_factor,
1573 ],
1574 };
1575 let stack_top = transform_stack
1582 .last()
1583 .copied()
1584 .unwrap_or(Transform2D::IDENTITY);
1585 current_transform = device_t.then(&stack_top);
1586 }
1587 teksilo_canvas::DrawCommand::PushTransform(t) => {
1588 flush_all!(
1589 pass,
1590 &self.queue,
1591 self.streams,
1592 &self.rect_pipeline,
1593 &self.sdf_pipeline,
1594 &self.quad_pipeline,
1595 &self.path_gradient_pipeline,
1596 &self.shadow_pipeline,
1597 rect_batch,
1598 sdf_batch,
1599 quad_batch,
1600 path_gradient_batch,
1601 shadow_batch,
1602 self.atlas_texture,
1603 self.path_atlas_texture,
1604 quad_source,
1605 index_binding
1606 );
1607 quad_source = None;
1608 let device_t = Transform2D {
1611 m: [
1612 t.m[0],
1613 t.m[1],
1614 t.m[2],
1615 t.m[3],
1616 t.m[4] * scale_factor,
1617 t.m[5] * scale_factor,
1618 ],
1619 };
1620 let prev_top = transform_stack
1621 .last()
1622 .copied()
1623 .unwrap_or(Transform2D::IDENTITY);
1624 let new_top = device_t.then(&prev_top);
1625 transform_stack.push(new_top);
1626 current_transform = new_top;
1627 }
1628 teksilo_canvas::DrawCommand::PopTransform => {
1629 flush_all!(
1630 pass,
1631 &self.queue,
1632 self.streams,
1633 &self.rect_pipeline,
1634 &self.sdf_pipeline,
1635 &self.quad_pipeline,
1636 &self.path_gradient_pipeline,
1637 &self.shadow_pipeline,
1638 rect_batch,
1639 sdf_batch,
1640 quad_batch,
1641 path_gradient_batch,
1642 shadow_batch,
1643 self.atlas_texture,
1644 self.path_atlas_texture,
1645 quad_source,
1646 index_binding
1647 );
1648 quad_source = None;
1649 if transform_stack.len() > 1 {
1650 transform_stack.pop();
1651 }
1652 current_transform = transform_stack
1653 .last()
1654 .copied()
1655 .unwrap_or(Transform2D::IDENTITY);
1656 }
1657 teksilo_canvas::DrawCommand::BeginBlurredSubtree { .. }
1658 | teksilo_canvas::DrawCommand::EndBlurredSubtree => {
1659 unreachable!("blur boundaries are handled at the segment level");
1662 }
1663 }
1664 cmd_idx += 1;
1665 }
1666
1667 flush_all!(
1669 pass,
1670 &self.queue,
1671 self.streams,
1672 &self.rect_pipeline,
1673 &self.sdf_pipeline,
1674 &self.quad_pipeline,
1675 &self.path_gradient_pipeline,
1676 &self.shadow_pipeline,
1677 rect_batch,
1678 sdf_batch,
1679 quad_batch,
1680 path_gradient_batch,
1681 shadow_batch,
1682 self.atlas_texture,
1683 self.path_atlas_texture,
1684 quad_source,
1685 index_binding
1686 );
1687 quad_source = None;
1688 } if cmd_idx >= frame.draw_order.len() {
1692 break;
1693 }
1694 match &frame.draw_order[cmd_idx] {
1695 teksilo_canvas::DrawCommand::BeginBlurredSubtree { bounds, radius } => {
1696 let device_w = (bounds.width * scale_factor).ceil().max(1.0) as u32;
1698 let device_h = (bounds.height * scale_factor).ceil().max(1.0) as u32;
1699 let intermediate = self.blur_pool.acquire(&self.device, device_w, device_h);
1700 let (bucket_w, bucket_h) = self.blur_pool.dimensions(intermediate);
1701
1702 let translate = Transform2D {
1707 m: [
1708 1.0,
1709 0.0,
1710 0.0,
1711 1.0,
1712 -bounds.x * scale_factor,
1713 -bounds.y * scale_factor,
1714 ],
1715 };
1716 let prev_top = transform_stack
1717 .last()
1718 .copied()
1719 .unwrap_or(Transform2D::IDENTITY);
1720 let new_top = translate.then(&prev_top);
1721 transform_stack.push(new_top);
1722 current_transform = new_top;
1723
1724 target_stack.push(ActiveTarget {
1725 intermediate: Some(intermediate),
1726 viewport_w: bucket_w,
1727 viewport_h: bucket_h,
1728 opened: false,
1729 pending_composites: Vec::new(),
1730 clip_stack: Vec::new(),
1731 blur_bounds: Some(*bounds),
1732 blur_radius_logical: Some(*radius),
1733 used_w: Some(device_w),
1734 used_h: Some(device_h),
1735 bucket_w: Some(bucket_w),
1736 bucket_h: Some(bucket_h),
1737 });
1738 }
1739 teksilo_canvas::DrawCommand::EndBlurredSubtree => {
1740 let scope = target_stack
1741 .pop()
1742 .expect("EndBlurredSubtree without matching Begin");
1743 debug_assert!(
1744 scope.intermediate.is_some(),
1745 "End popped the surface (impossible if walker is balanced)"
1746 );
1747 let intermediate = scope
1748 .intermediate
1749 .expect("blur scope intermediate set in BeginBlurredSubtree");
1750 let bounds = scope
1751 .blur_bounds
1752 .expect("blur scope bounds set in BeginBlurredSubtree");
1753 let radius = scope
1754 .blur_radius_logical
1755 .expect("blur scope radius set in BeginBlurredSubtree");
1756 let used_w = scope
1757 .used_w
1758 .expect("blur scope used_w set in BeginBlurredSubtree");
1759 let used_h = scope
1760 .used_h
1761 .expect("blur scope used_h set in BeginBlurredSubtree");
1762 let bucket_w = scope
1763 .bucket_w
1764 .expect("blur scope bucket_w set in BeginBlurredSubtree");
1765 let bucket_h = scope
1766 .bucket_h
1767 .expect("blur scope bucket_h set in BeginBlurredSubtree");
1768
1769 if transform_stack.len() > 1 {
1771 transform_stack.pop();
1772 }
1773 current_transform = transform_stack
1774 .last()
1775 .copied()
1776 .unwrap_or(Transform2D::IDENTITY);
1777
1778 let blurred = run_kawase_chain(
1782 &self.device,
1783 &self.queue,
1784 &mut encoder,
1785 &mut self.blur_pool,
1786 &mut self.blur_pipelines,
1787 intermediate,
1788 used_w,
1789 used_h,
1790 bucket_w,
1791 bucket_h,
1792 radius * scale_factor,
1793 );
1794
1795 target_stack
1798 .last_mut()
1799 .expect("target_stack always has the surface target")
1800 .pending_composites
1801 .push(PendingComposite {
1802 blurred_texture: blurred.texture,
1803 used_w: blurred.used_w,
1804 used_h: blurred.used_h,
1805 bucket_w: blurred.bucket_w,
1806 bucket_h: blurred.bucket_h,
1807 bounds,
1808 });
1809 }
1810 _ => unreachable!("inner loop only breaks on Begin/End"),
1811 }
1812 cmd_idx += 1;
1813 }
1814
1815 debug_assert!(
1816 target_stack.len() == 1,
1817 "target_stack not balanced at EOF — unmatched Begin/End in walker output"
1818 );
1819 let final_composites = std::mem::take(
1823 &mut target_stack
1824 .last_mut()
1825 .expect("target_stack always has the surface target")
1826 .pending_composites,
1827 );
1828 if !final_composites.is_empty() {
1829 let surface = target_stack
1830 .last_mut()
1831 .expect("target_stack always has the surface target");
1832 let load_op = if surface.opened {
1833 wgpu::LoadOp::Load
1834 } else {
1835 wgpu::LoadOp::Clear(surface_clear_color)
1836 };
1837 surface.opened = true;
1838 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1839 label: Some("teksilo_final_composite_pass"),
1840 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1841 view,
1842 resolve_target: None,
1843 ops: wgpu::Operations {
1844 load: load_op,
1845 store: wgpu::StoreOp::Store,
1846 },
1847 depth_slice: None,
1848 })],
1849 depth_stencil_attachment: None,
1850 timestamp_writes: None,
1851 occlusion_query_set: None,
1852 multiview_mask: None,
1853 });
1854 for pc in &final_composites {
1855 composite_blur_quad(
1856 &self.device,
1857 &self.queue,
1858 &mut pass,
1859 &self.blur_pool,
1860 &self.quad_pipeline,
1861 &self.quad_bind_group_layout,
1862 &self.blur_composite_sampler,
1863 &self.streams.quad,
1864 index_binding,
1865 pc.blurred_texture,
1866 pc.used_w,
1867 pc.used_h,
1868 pc.bucket_w,
1869 pc.bucket_h,
1870 pc.bounds,
1871 scale_factor,
1872 viewport_width,
1873 viewport_height,
1874 );
1875 }
1876 } else if !target_stack
1877 .last()
1878 .expect("target_stack always has the surface target")
1879 .opened
1880 {
1881 let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1883 label: Some("teksilo_empty_clear_pass"),
1884 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1885 view,
1886 resolve_target: None,
1887 ops: wgpu::Operations {
1888 load: wgpu::LoadOp::Clear(surface_clear_color),
1889 store: wgpu::StoreOp::Store,
1890 },
1891 depth_slice: None,
1892 })],
1893 depth_stencil_attachment: None,
1894 timestamp_writes: None,
1895 occlusion_query_set: None,
1896 multiview_mask: None,
1897 });
1898 }
1899 }
1900
1901 self.queue.submit(std::iter::once(encoder.finish()));
1902 }
1903
1904 #[allow(clippy::too_many_arguments)]
1908 fn draw_image(
1909 &self,
1910 pass: &mut wgpu::RenderPass,
1911 image: &teksilo_canvas::ImageQuad,
1912 scale_factor: f32,
1913 viewport_width: u32,
1914 viewport_height: u32,
1915 opacity: f32,
1916 transform: &Transform2D,
1917 index_binding: Option<(&wgpu::Buffer, u64, u64)>,
1918 ) {
1919 let bind_group = match self.image_manager.get_bind_group(&image.name) {
1920 Some(bg) => bg,
1921 None => return,
1922 };
1923
1924 let [x, y, w, h] = image.screen;
1925 let sx = x * scale_factor;
1926 let sy = y * scale_factor;
1927 let sw = w * scale_factor;
1928 let sh = h * scale_factor;
1929
1930 let (color, flags) = if let Some(tint) = image.tint {
1933 (
1936 crate::vertex::srgb_to_linear_rgba([tint[0], tint[1], tint[2], tint[3] * opacity]),
1937 0,
1938 )
1939 } else {
1940 (
1941 [1.0, 1.0, 1.0, opacity],
1942 crate::vertex::QUAD_FLAG_COLOR_GLYPH,
1943 )
1944 };
1945
1946 let verts = [
1947 QuadVertex {
1948 position: [sx, sy],
1949 tex_coord: [0.0, 0.0],
1950 color,
1951 flags,
1952 _pad: 0,
1953 },
1954 QuadVertex {
1955 position: [sx + sw, sy],
1956 tex_coord: [1.0, 0.0],
1957 color,
1958 flags,
1959 _pad: 0,
1960 },
1961 QuadVertex {
1962 position: [sx + sw, sy + sh],
1963 tex_coord: [1.0, 1.0],
1964 color,
1965 flags,
1966 _pad: 0,
1967 },
1968 QuadVertex {
1969 position: [sx, sy + sh],
1970 tex_coord: [0.0, 1.0],
1971 color,
1972 flags,
1973 _pad: 0,
1974 },
1975 ];
1976
1977 let ndc_verts: [QuadVertex; 4] = std::array::from_fn(|i| {
1978 let v = verts[i];
1979 let tp = apply_transform_pixel(v.position, transform);
1980 QuadVertex {
1981 position: pixel_to_ndc(tp, viewport_width, viewport_height),
1982 ..v
1983 }
1984 });
1985
1986 let bytes: &[u8] = bytemuck::cast_slice(&ndc_verts);
1990 let Some((vb, v_off, v_len)) = self.streams.quad.write(&self.queue, bytes) else {
1991 return;
1992 };
1993 let Some((ib, _, _)) = index_binding else {
1994 return;
1995 };
1996
1997 pass.set_pipeline(&self.quad_pipeline);
1998 pass.set_bind_group(0, bind_group, &[]);
1999 pass.set_vertex_buffer(0, vb.slice(v_off..v_off + v_len));
2000 pass.set_index_buffer(ib.slice(0..24), wgpu::IndexFormat::Uint32);
2001 pass.draw_indexed(0..6, 0, 0..1);
2002 }
2003
2004 fn upload_path_atlas(&mut self, width: u32, height: u32, pixels: Vec<u8>) {
2006 if width == 0 || height == 0 {
2007 return;
2008 }
2009
2010 let needs_recreate = self
2011 .path_atlas_texture
2012 .as_ref()
2013 .is_none_or(|t| t.width != width || t.height != height);
2014
2015 if needs_recreate {
2016 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
2017 label: Some("path_atlas"),
2018 size: wgpu::Extent3d {
2019 width,
2020 height,
2021 depth_or_array_layers: 1,
2022 },
2023 mip_level_count: 1,
2024 sample_count: 1,
2025 dimension: wgpu::TextureDimension::D2,
2026 format: wgpu::TextureFormat::Rgba8UnormSrgb,
2027 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2028 view_formats: &[],
2029 });
2030
2031 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
2032 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
2033 mag_filter: wgpu::FilterMode::Linear,
2034 min_filter: wgpu::FilterMode::Linear,
2035 ..Default::default()
2036 });
2037
2038 let bind_group_layout = self.quad_pipeline.get_bind_group_layout(0);
2039 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2040 label: Some("path_atlas_bind_group"),
2041 layout: &bind_group_layout,
2042 entries: &[
2043 wgpu::BindGroupEntry {
2044 binding: 0,
2045 resource: wgpu::BindingResource::TextureView(&view),
2046 },
2047 wgpu::BindGroupEntry {
2048 binding: 1,
2049 resource: wgpu::BindingResource::Sampler(&sampler),
2050 },
2051 ],
2052 });
2053
2054 self.path_atlas_texture = Some(AtlasTexture {
2055 texture,
2056 bind_group,
2057 width,
2058 height,
2059 });
2060 }
2061
2062 if let Some(atlas) = &self.path_atlas_texture {
2063 self.queue.write_texture(
2064 wgpu::TexelCopyTextureInfo {
2065 texture: &atlas.texture,
2066 mip_level: 0,
2067 origin: wgpu::Origin3d::ZERO,
2068 aspect: wgpu::TextureAspect::All,
2069 },
2070 &pixels,
2071 wgpu::TexelCopyBufferLayout {
2072 offset: 0,
2073 bytes_per_row: Some(width * 4),
2074 rows_per_image: Some(height),
2075 },
2076 wgpu::Extent3d {
2077 width,
2078 height,
2079 depth_or_array_layers: 1,
2080 },
2081 );
2082 }
2083 }
2084
2085 pub fn device(&self) -> &wgpu::Device {
2086 &self.device
2087 }
2088
2089 pub fn queue(&self) -> &wgpu::Queue {
2090 &self.queue
2091 }
2092
2093 pub fn register_image(&mut self, name: &str, width: u32, height: u32, pixels: &[u8]) {
2095 let layout = self.quad_pipeline.get_bind_group_layout(0);
2096 self.image_manager.register_image(
2097 name,
2098 width,
2099 height,
2100 pixels,
2101 &self.device,
2102 &self.queue,
2103 &layout,
2104 );
2105 }
2106
2107 pub fn remove_image(&mut self, name: &str) {
2109 self.image_manager.remove(name);
2110 }
2111}
2112
2113fn path_quad_verts(
2115 entry: &teksilo_canvas::PathEntry,
2116 placement: &crate::path_atlas::PathPlacement,
2117 atlas_width: u32,
2118 atlas_height: u32,
2119 opacity: f32,
2120 transform: &Transform2D,
2121) -> [QuadVertex; 4] {
2122 let region = &placement.region;
2127 let [sx, sy, sw, sh] = placement.device_rect;
2128
2129 let aw = atlas_width.max(1) as f32;
2130 let ah = atlas_height.max(1) as f32;
2131 let u0 = region.x as f32 / aw;
2132 let v0 = region.y as f32 / ah;
2133 let u1 = (region.x + region.w) as f32 / aw;
2134 let v1 = (region.y + region.h) as f32 / ah;
2135
2136 let lin = crate::vertex::srgb_to_linear_rgba(entry.color);
2143 let color = [lin[0], lin[1], lin[2], entry.color[3] * opacity];
2144
2145 let positions = [
2146 apply_transform_pixel([sx, sy], transform),
2147 apply_transform_pixel([sx + sw, sy], transform),
2148 apply_transform_pixel([sx + sw, sy + sh], transform),
2149 apply_transform_pixel([sx, sy + sh], transform),
2150 ];
2151 let uvs = [[u0, v0], [u1, v0], [u1, v1], [u0, v1]];
2152
2153 [
2156 QuadVertex {
2157 position: positions[0],
2158 tex_coord: uvs[0],
2159 color,
2160 flags: 0,
2161 _pad: 0,
2162 },
2163 QuadVertex {
2164 position: positions[1],
2165 tex_coord: uvs[1],
2166 color,
2167 flags: 0,
2168 _pad: 0,
2169 },
2170 QuadVertex {
2171 position: positions[2],
2172 tex_coord: uvs[2],
2173 color,
2174 flags: 0,
2175 _pad: 0,
2176 },
2177 QuadVertex {
2178 position: positions[3],
2179 tex_coord: uvs[3],
2180 color,
2181 flags: 0,
2182 _pad: 0,
2183 },
2184 ]
2185}
2186
2187fn path_gradient_quad_verts(
2196 entry: &teksilo_canvas::PathEntry,
2197 placement: &crate::path_atlas::PathPlacement,
2198 scale_factor: f32,
2199 atlas_width: u32,
2200 atlas_height: u32,
2201 current_opacity: f32,
2202 transform: &Transform2D,
2203) -> [crate::vertex::PathGradientVertex; 4] {
2204 crate::vertex::PathGradientVertex::from_path_entry(
2205 entry,
2206 placement,
2207 scale_factor,
2208 atlas_width,
2209 atlas_height,
2210 current_opacity,
2211 transform,
2212 )
2213}
2214
2215fn pixel_to_ndc(pixel: [f32; 2], viewport_width: u32, viewport_height: u32) -> [f32; 2] {
2216 let x = (pixel[0] / viewport_width as f32) * 2.0 - 1.0;
2217 let y = 1.0 - (pixel[1] / viewport_height as f32) * 2.0; [x, y]
2219}
2220
2221fn apply_transform_pixel(pixel: [f32; 2], transform: &Transform2D) -> [f32; 2] {
2223 let [a, b, c, d, tx, ty] = transform.m;
2224 [
2225 a * pixel[0] + c * pixel[1] + tx,
2226 b * pixel[0] + d * pixel[1] + ty,
2227 ]
2228}
2229
2230struct KawaseResult {
2236 texture: crate::blur::AcquiredTexture,
2237 used_w: u32,
2238 used_h: u32,
2239 bucket_w: u32,
2240 bucket_h: u32,
2241}
2242
2243#[allow(clippy::too_many_arguments)]
2249fn run_kawase_chain(
2250 device: &wgpu::Device,
2251 queue: &wgpu::Queue,
2252 encoder: &mut wgpu::CommandEncoder,
2253 pool: &mut crate::blur::BlurPool,
2254 pipelines: &mut crate::blur::BlurPipelines,
2255 source: crate::blur::AcquiredTexture,
2256 used_w: u32,
2257 used_h: u32,
2258 bucket_w: u32,
2259 bucket_h: u32,
2260 radius_device_px: f32,
2261) -> KawaseResult {
2262 let levels = crate::blur::kawase_levels(radius_device_px);
2263
2264 let mut current = (source, used_w, used_h, bucket_w, bucket_h);
2268
2269 let mut chain: Vec<(crate::blur::AcquiredTexture, u32, u32, u32, u32)> =
2273 Vec::with_capacity(levels as usize + 1);
2274 chain.push(current);
2275
2276 const KERNEL_OFFSET: f32 = 0.5;
2281
2282 for _ in 0..levels {
2284 let (src_handle, src_used_w, src_used_h, src_bucket_w, src_bucket_h) = current;
2285 let dst_used_w = (src_used_w / 2).max(1);
2286 let dst_used_h = (src_used_h / 2).max(1);
2287 let dst = pool.acquire(device, dst_used_w, dst_used_h);
2288 let (dst_bucket_w, dst_bucket_h) = pool.dimensions(dst);
2289
2290 let params = crate::blur::BlurParams {
2292 offset: crate::blur::kawase_offset(src_bucket_w, src_bucket_h, KERNEL_OFFSET),
2293 };
2294 let params_offset = pipelines.params.push(queue, ¶ms);
2295 let bind_group = pool.make_bind_group(device, src_handle, pipelines.params.buffer());
2296
2297 run_kawase_pass(
2298 encoder,
2299 &pipelines.down,
2300 &bind_group,
2301 params_offset,
2302 pool.view(dst),
2303 dst_used_w,
2304 dst_used_h,
2305 "kawase_down_pass",
2306 );
2307
2308 current = (dst, dst_used_w, dst_used_h, dst_bucket_w, dst_bucket_h);
2309 chain.push(current);
2310 }
2311
2312 for level in (0..levels).rev() {
2316 let (src_handle, _src_used_w, _src_used_h, src_bucket_w, src_bucket_h) = current;
2317 let target = chain[level as usize];
2318 let dst_used_w = target.1;
2319 let dst_used_h = target.2;
2320 let dst = pool.acquire(device, dst_used_w, dst_used_h);
2321 let (dst_bucket_w, dst_bucket_h) = pool.dimensions(dst);
2322
2323 let params = crate::blur::BlurParams {
2324 offset: crate::blur::kawase_offset(src_bucket_w, src_bucket_h, KERNEL_OFFSET),
2325 };
2326 let params_offset = pipelines.params.push(queue, ¶ms);
2327 let bind_group = pool.make_bind_group(device, src_handle, pipelines.params.buffer());
2328
2329 run_kawase_pass(
2330 encoder,
2331 &pipelines.up,
2332 &bind_group,
2333 params_offset,
2334 pool.view(dst),
2335 dst_used_w,
2336 dst_used_h,
2337 "kawase_up_pass",
2338 );
2339
2340 current = (dst, dst_used_w, dst_used_h, dst_bucket_w, dst_bucket_h);
2341 }
2342
2343 KawaseResult {
2344 texture: current.0,
2345 used_w: current.1,
2346 used_h: current.2,
2347 bucket_w: current.3,
2348 bucket_h: current.4,
2349 }
2350}
2351
2352#[allow(clippy::too_many_arguments)]
2357fn run_kawase_pass(
2358 encoder: &mut wgpu::CommandEncoder,
2359 pipeline: &wgpu::RenderPipeline,
2360 bind_group: &wgpu::BindGroup,
2361 params_offset: u32,
2362 target_view: &wgpu::TextureView,
2363 used_w: u32,
2364 used_h: u32,
2365 label: &str,
2366) {
2367 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2368 label: Some(label),
2369 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2370 view: target_view,
2371 resolve_target: None,
2372 ops: wgpu::Operations {
2373 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2374 store: wgpu::StoreOp::Store,
2375 },
2376 depth_slice: None,
2377 })],
2378 depth_stencil_attachment: None,
2379 timestamp_writes: None,
2380 occlusion_query_set: None,
2381 multiview_mask: None,
2382 });
2383 pass.set_pipeline(pipeline);
2384 pass.set_bind_group(0, bind_group, &[params_offset]);
2385 pass.set_viewport(0.0, 0.0, used_w as f32, used_h as f32, 0.0, 1.0);
2389 pass.draw(0..3, 0..1);
2390}
2391
2392#[allow(clippy::too_many_arguments)]
2402fn composite_blur_quad(
2403 device: &wgpu::Device,
2404 queue: &wgpu::Queue,
2405 pass: &mut wgpu::RenderPass<'_>,
2406 pool: &crate::blur::BlurPool,
2407 quad_pipeline: &wgpu::RenderPipeline,
2408 quad_bind_group_layout: &wgpu::BindGroupLayout,
2409 sampler: &wgpu::Sampler,
2410 quad_stream: &crate::stream_buffer::StreamBuffer,
2411 index_binding: Option<(&wgpu::Buffer, u64, u64)>,
2412 blurred: crate::blur::AcquiredTexture,
2413 used_w: u32,
2414 used_h: u32,
2415 bucket_w: u32,
2416 bucket_h: u32,
2417 bounds: teksilo_canvas::Rect,
2418 scale_factor: f32,
2419 viewport_width: u32,
2420 viewport_height: u32,
2421) {
2422 let view = pool.view(blurred);
2423 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2424 label: Some("blur_composite_bind_group"),
2425 layout: quad_bind_group_layout,
2426 entries: &[
2427 wgpu::BindGroupEntry {
2428 binding: 0,
2429 resource: wgpu::BindingResource::TextureView(view),
2430 },
2431 wgpu::BindGroupEntry {
2432 binding: 1,
2433 resource: wgpu::BindingResource::Sampler(sampler),
2434 },
2435 ],
2436 });
2437
2438 let sx = bounds.x * scale_factor;
2440 let sy = bounds.y * scale_factor;
2441 let sw = bounds.width * scale_factor;
2442 let sh = bounds.height * scale_factor;
2443
2444 let u_max = used_w as f32 / bucket_w as f32;
2448 let v_max = used_h as f32 / bucket_h as f32;
2449
2450 let flags = 1u32;
2452 let color = [1.0, 1.0, 1.0, 1.0];
2453
2454 let p_tl = pixel_to_ndc([sx, sy], viewport_width, viewport_height);
2455 let p_tr = pixel_to_ndc([sx + sw, sy], viewport_width, viewport_height);
2456 let p_br = pixel_to_ndc([sx + sw, sy + sh], viewport_width, viewport_height);
2457 let p_bl = pixel_to_ndc([sx, sy + sh], viewport_width, viewport_height);
2458
2459 let verts: [QuadVertex; 4] = [
2460 QuadVertex {
2461 position: p_tl,
2462 tex_coord: [0.0, 0.0],
2463 color,
2464 flags,
2465 _pad: 0,
2466 },
2467 QuadVertex {
2468 position: p_tr,
2469 tex_coord: [u_max, 0.0],
2470 color,
2471 flags,
2472 _pad: 0,
2473 },
2474 QuadVertex {
2475 position: p_br,
2476 tex_coord: [u_max, v_max],
2477 color,
2478 flags,
2479 _pad: 0,
2480 },
2481 QuadVertex {
2482 position: p_bl,
2483 tex_coord: [0.0, v_max],
2484 color,
2485 flags,
2486 _pad: 0,
2487 },
2488 ];
2489
2490 let _ = device; let Some((vb, v_off, v_len)) = quad_stream.write(queue, bytemuck::cast_slice(&verts)) else {
2496 return;
2497 };
2498 let Some((ib, _, _)) = index_binding else {
2499 return;
2500 };
2501 let composite_index_bytes: u64 = 6 * std::mem::size_of::<u32>() as u64;
2502
2503 pass.set_pipeline(quad_pipeline);
2504 pass.set_bind_group(0, &bind_group, &[]);
2505 pass.set_viewport(
2506 0.0,
2507 0.0,
2508 viewport_width as f32,
2509 viewport_height as f32,
2510 0.0,
2511 1.0,
2512 );
2513 pass.set_vertex_buffer(0, vb.slice(v_off..v_off + v_len));
2514 pass.set_index_buffer(
2515 ib.slice(0..composite_index_bytes),
2516 wgpu::IndexFormat::Uint32,
2517 );
2518 pass.draw_indexed(0..6, 0, 0..1);
2519}
2520
2521fn create_rect_pipeline(
2524 device: &wgpu::Device,
2525 format: wgpu::TextureFormat,
2526) -> wgpu::RenderPipeline {
2527 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2528 label: Some("rect_shader"),
2529 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/rect.wgsl").into()),
2530 });
2531
2532 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2533 label: Some("rect_pipeline_layout"),
2534 bind_group_layouts: &[],
2535 immediate_size: 0,
2536 });
2537
2538 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2539 label: Some("rect_pipeline"),
2540 layout: Some(&layout),
2541 vertex: wgpu::VertexState {
2542 module: &shader,
2543 entry_point: Some("vs_main"),
2544 buffers: &[Some(wgpu::VertexBufferLayout {
2545 array_stride: std::mem::size_of::<RectVertex>() as u64,
2546 step_mode: wgpu::VertexStepMode::Vertex,
2547 attributes: &[
2548 wgpu::VertexAttribute {
2549 offset: 0,
2550 shader_location: 0,
2551 format: wgpu::VertexFormat::Float32x2,
2552 },
2553 wgpu::VertexAttribute {
2554 offset: 8,
2555 shader_location: 1,
2556 format: wgpu::VertexFormat::Float32x4,
2557 },
2558 ],
2559 })],
2560 compilation_options: Default::default(),
2561 },
2562 fragment: Some(wgpu::FragmentState {
2563 module: &shader,
2564 entry_point: Some("fs_main"),
2565 targets: &[Some(wgpu::ColorTargetState {
2566 format,
2567 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
2568 write_mask: wgpu::ColorWrites::ALL,
2569 })],
2570 compilation_options: Default::default(),
2571 }),
2572 primitive: wgpu::PrimitiveState {
2573 topology: wgpu::PrimitiveTopology::TriangleList,
2574 ..Default::default()
2575 },
2576 depth_stencil: None,
2577 multisample: wgpu::MultisampleState::default(),
2578 multiview_mask: None,
2579 cache: None,
2580 })
2581}
2582
2583fn create_sdf_pipeline(device: &wgpu::Device, format: wgpu::TextureFormat) -> wgpu::RenderPipeline {
2584 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2585 label: Some("sdf_shader"),
2586 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/sdf.wgsl").into()),
2587 });
2588
2589 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2590 label: Some("sdf_pipeline_layout"),
2591 bind_group_layouts: &[],
2592 immediate_size: 0,
2593 });
2594
2595 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2596 label: Some("sdf_pipeline"),
2597 layout: Some(&layout),
2598 vertex: wgpu::VertexState {
2599 module: &shader,
2600 entry_point: Some("vs_main"),
2601 buffers: &[Some(wgpu::VertexBufferLayout {
2602 array_stride: std::mem::size_of::<SdfVertex>() as u64,
2603 step_mode: wgpu::VertexStepMode::Vertex,
2604 attributes: &[
2605 wgpu::VertexAttribute {
2606 offset: 0,
2607 shader_location: 0,
2608 format: wgpu::VertexFormat::Float32x2, },
2610 wgpu::VertexAttribute {
2611 offset: 8,
2612 shader_location: 1,
2613 format: wgpu::VertexFormat::Float32x2, },
2615 wgpu::VertexAttribute {
2616 offset: 16,
2617 shader_location: 2,
2618 format: wgpu::VertexFormat::Float32x4, },
2620 wgpu::VertexAttribute {
2621 offset: 32,
2622 shader_location: 3,
2623 format: wgpu::VertexFormat::Float32x4, },
2625 wgpu::VertexAttribute {
2626 offset: 48,
2627 shader_location: 4,
2628 format: wgpu::VertexFormat::Float32x4, },
2630 wgpu::VertexAttribute {
2631 offset: 64,
2632 shader_location: 5,
2633 format: wgpu::VertexFormat::Float32x4, },
2635 wgpu::VertexAttribute {
2636 offset: 80,
2637 shader_location: 6,
2638 format: wgpu::VertexFormat::Float32x4, },
2640 wgpu::VertexAttribute {
2641 offset: 96,
2642 shader_location: 7,
2643 format: wgpu::VertexFormat::Float32x4, },
2645 wgpu::VertexAttribute {
2646 offset: 112,
2647 shader_location: 8,
2648 format: wgpu::VertexFormat::Float32x4, },
2650 wgpu::VertexAttribute {
2651 offset: 128,
2652 shader_location: 9,
2653 format: wgpu::VertexFormat::Float32x4, },
2655 wgpu::VertexAttribute {
2656 offset: 144,
2657 shader_location: 10,
2658 format: wgpu::VertexFormat::Float32x4, },
2660 ],
2661 })],
2662 compilation_options: Default::default(),
2663 },
2664 fragment: Some(wgpu::FragmentState {
2665 module: &shader,
2666 entry_point: Some("fs_main"),
2667 targets: &[Some(wgpu::ColorTargetState {
2668 format,
2669 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
2670 write_mask: wgpu::ColorWrites::ALL,
2671 })],
2672 compilation_options: Default::default(),
2673 }),
2674 primitive: wgpu::PrimitiveState {
2675 topology: wgpu::PrimitiveTopology::TriangleList,
2676 ..Default::default()
2677 },
2678 depth_stencil: None,
2679 multisample: wgpu::MultisampleState::default(),
2680 multiview_mask: None,
2681 cache: None,
2682 })
2683}
2684
2685fn create_quad_pipeline(
2686 device: &wgpu::Device,
2687 format: wgpu::TextureFormat,
2688) -> wgpu::RenderPipeline {
2689 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2690 label: Some("quad_shader"),
2691 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/quad.wgsl").into()),
2692 });
2693
2694 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2695 label: Some("quad_bind_group_layout"),
2696 entries: &[
2697 wgpu::BindGroupLayoutEntry {
2698 binding: 0,
2699 visibility: wgpu::ShaderStages::FRAGMENT,
2700 ty: wgpu::BindingType::Texture {
2701 sample_type: wgpu::TextureSampleType::Float { filterable: true },
2702 view_dimension: wgpu::TextureViewDimension::D2,
2703 multisampled: false,
2704 },
2705 count: None,
2706 },
2707 wgpu::BindGroupLayoutEntry {
2708 binding: 1,
2709 visibility: wgpu::ShaderStages::FRAGMENT,
2710 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
2711 count: None,
2712 },
2713 ],
2714 });
2715
2716 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2717 label: Some("quad_pipeline_layout"),
2718 bind_group_layouts: &[Some(&bind_group_layout)],
2719 immediate_size: 0,
2720 });
2721
2722 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2723 label: Some("quad_pipeline"),
2724 layout: Some(&layout),
2725 vertex: wgpu::VertexState {
2726 module: &shader,
2727 entry_point: Some("vs_main"),
2728 buffers: &[Some(wgpu::VertexBufferLayout {
2729 array_stride: std::mem::size_of::<QuadVertex>() as u64,
2730 step_mode: wgpu::VertexStepMode::Vertex,
2731 attributes: &[
2732 wgpu::VertexAttribute {
2733 offset: 0,
2734 shader_location: 0,
2735 format: wgpu::VertexFormat::Float32x2, },
2737 wgpu::VertexAttribute {
2738 offset: 8,
2739 shader_location: 1,
2740 format: wgpu::VertexFormat::Float32x2, },
2742 wgpu::VertexAttribute {
2743 offset: 16,
2744 shader_location: 2,
2745 format: wgpu::VertexFormat::Float32x4, },
2747 wgpu::VertexAttribute {
2748 offset: 32,
2749 shader_location: 3,
2750 format: wgpu::VertexFormat::Uint32, },
2752 ],
2753 })],
2754 compilation_options: Default::default(),
2755 },
2756 fragment: Some(wgpu::FragmentState {
2757 module: &shader,
2758 entry_point: Some("fs_main"),
2759 targets: &[Some(wgpu::ColorTargetState {
2760 format,
2761 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
2762 write_mask: wgpu::ColorWrites::ALL,
2763 })],
2764 compilation_options: Default::default(),
2765 }),
2766 primitive: wgpu::PrimitiveState {
2767 topology: wgpu::PrimitiveTopology::TriangleList,
2768 ..Default::default()
2769 },
2770 depth_stencil: None,
2771 multisample: wgpu::MultisampleState::default(),
2772 multiview_mask: None,
2773 cache: None,
2774 })
2775}
2776
2777fn create_path_gradient_pipeline(
2783 device: &wgpu::Device,
2784 format: wgpu::TextureFormat,
2785 texture_bind_group_layout: &wgpu::BindGroupLayout,
2786) -> wgpu::RenderPipeline {
2787 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2788 label: Some("path_gradient_shader"),
2789 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/path_gradient.wgsl").into()),
2790 });
2791
2792 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2793 label: Some("path_gradient_pipeline_layout"),
2794 bind_group_layouts: &[Some(texture_bind_group_layout)],
2795 immediate_size: 0,
2796 });
2797
2798 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2799 label: Some("path_gradient_pipeline"),
2800 layout: Some(&layout),
2801 vertex: wgpu::VertexState {
2802 module: &shader,
2803 entry_point: Some("vs_main"),
2804 buffers: &[Some(wgpu::VertexBufferLayout {
2805 array_stride: std::mem::size_of::<crate::vertex::PathGradientVertex>() as u64,
2806 step_mode: wgpu::VertexStepMode::Vertex,
2807 attributes: &[
2808 wgpu::VertexAttribute {
2809 offset: 0,
2810 shader_location: 0,
2811 format: wgpu::VertexFormat::Float32x2, },
2813 wgpu::VertexAttribute {
2814 offset: 8,
2815 shader_location: 1,
2816 format: wgpu::VertexFormat::Float32x2, },
2818 wgpu::VertexAttribute {
2819 offset: 16,
2820 shader_location: 2,
2821 format: wgpu::VertexFormat::Float32x2, },
2823 wgpu::VertexAttribute {
2824 offset: 24,
2825 shader_location: 3,
2826 format: wgpu::VertexFormat::Uint32, },
2828 wgpu::VertexAttribute {
2830 offset: 32,
2831 shader_location: 4,
2832 format: wgpu::VertexFormat::Float32x4, },
2834 wgpu::VertexAttribute {
2835 offset: 48,
2836 shader_location: 5,
2837 format: wgpu::VertexFormat::Float32x4, },
2839 wgpu::VertexAttribute {
2840 offset: 64,
2841 shader_location: 6,
2842 format: wgpu::VertexFormat::Float32x4, },
2844 wgpu::VertexAttribute {
2845 offset: 80,
2846 shader_location: 7,
2847 format: wgpu::VertexFormat::Float32x4, },
2849 wgpu::VertexAttribute {
2850 offset: 96,
2851 shader_location: 8,
2852 format: wgpu::VertexFormat::Float32x4, },
2854 wgpu::VertexAttribute {
2855 offset: 112,
2856 shader_location: 9,
2857 format: wgpu::VertexFormat::Float32x4, },
2859 ],
2860 })],
2861 compilation_options: Default::default(),
2862 },
2863 fragment: Some(wgpu::FragmentState {
2864 module: &shader,
2865 entry_point: Some("fs_main"),
2866 targets: &[Some(wgpu::ColorTargetState {
2867 format,
2868 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
2869 write_mask: wgpu::ColorWrites::ALL,
2870 })],
2871 compilation_options: Default::default(),
2872 }),
2873 primitive: wgpu::PrimitiveState {
2874 topology: wgpu::PrimitiveTopology::TriangleList,
2875 ..Default::default()
2876 },
2877 depth_stencil: None,
2878 multisample: wgpu::MultisampleState::default(),
2879 multiview_mask: None,
2880 cache: None,
2881 })
2882}
2883
2884fn create_shadow_pipeline(
2885 device: &wgpu::Device,
2886 format: wgpu::TextureFormat,
2887) -> wgpu::RenderPipeline {
2888 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2889 label: Some("shadow_shader"),
2890 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/shadow.wgsl").into()),
2891 });
2892
2893 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2894 label: Some("shadow_pipeline_layout"),
2895 bind_group_layouts: &[],
2896 immediate_size: 0,
2897 });
2898
2899 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2900 label: Some("shadow_pipeline"),
2901 layout: Some(&layout),
2902 vertex: wgpu::VertexState {
2903 module: &shader,
2904 entry_point: Some("vs_main"),
2905 buffers: &[Some(wgpu::VertexBufferLayout {
2906 array_stride: std::mem::size_of::<ShadowVertex>() as u64,
2907 step_mode: wgpu::VertexStepMode::Vertex,
2908 attributes: &[
2909 wgpu::VertexAttribute {
2910 offset: 0,
2911 shader_location: 0,
2912 format: wgpu::VertexFormat::Float32x2, },
2914 wgpu::VertexAttribute {
2915 offset: 8,
2916 shader_location: 1,
2917 format: wgpu::VertexFormat::Float32x2, },
2919 wgpu::VertexAttribute {
2920 offset: 16,
2921 shader_location: 2,
2922 format: wgpu::VertexFormat::Float32x4, },
2924 wgpu::VertexAttribute {
2925 offset: 32,
2926 shader_location: 3,
2927 format: wgpu::VertexFormat::Float32x4, },
2929 wgpu::VertexAttribute {
2930 offset: 48,
2931 shader_location: 4,
2932 format: wgpu::VertexFormat::Float32x4, },
2934 wgpu::VertexAttribute {
2935 offset: 64,
2936 shader_location: 5,
2937 format: wgpu::VertexFormat::Float32x4, },
2939 ],
2940 })],
2941 compilation_options: Default::default(),
2942 },
2943 fragment: Some(wgpu::FragmentState {
2944 module: &shader,
2945 entry_point: Some("fs_main"),
2946 targets: &[Some(wgpu::ColorTargetState {
2947 format,
2948 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
2949 write_mask: wgpu::ColorWrites::ALL,
2950 })],
2951 compilation_options: Default::default(),
2952 }),
2953 primitive: wgpu::PrimitiveState {
2954 topology: wgpu::PrimitiveTopology::TriangleList,
2955 ..Default::default()
2956 },
2957 depth_stencil: None,
2958 multisample: wgpu::MultisampleState::default(),
2959 multiview_mask: None,
2960 cache: None,
2961 })
2962}
2963
2964#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2975pub(crate) struct StreamQuadCounts {
2976 pub rect: usize,
2977 pub sdf: usize,
2978 pub quad: usize,
2979 pub shadow: usize,
2980 pub anim_proc: usize,
2981 pub path_gradient: usize,
2984}
2985
2986impl StreamQuadCounts {
2987 pub fn max(&self) -> usize {
2990 self.rect
2991 .max(self.sdf)
2992 .max(self.quad)
2993 .max(self.shadow)
2994 .max(self.anim_proc)
2995 .max(self.path_gradient)
2996 }
2997}
2998
2999pub(crate) fn stream_quad_counts(frame: &RenderFrame) -> StreamQuadCounts {
3021 let composite_quads = frame
3022 .draw_order
3023 .iter()
3024 .filter(|c| matches!(c, teksilo_canvas::DrawCommand::BeginBlurredSubtree { .. }))
3025 .count();
3026 let gradient_paths = frame
3027 .paths
3028 .iter()
3029 .filter(|p| !matches!(p.paint_data, teksilo_canvas::PaintData::Solid))
3030 .count();
3031 let solid_paths = frame.paths.len() - gradient_paths;
3032 StreamQuadCounts {
3033 rect: frame.decorations.len() + frame.cosmetic_lines.len(),
3034 sdf: frame.shapes.len(),
3035 quad: frame.glyphs.len() + solid_paths + frame.images.len() + composite_quads,
3036 shadow: frame.shadows.len(),
3037 anim_proc: frame.animated_quads.len(),
3038 path_gradient: gradient_paths,
3039 }
3040}
3041
3042fn create_anim_proc_pipeline(
3048 device: &wgpu::Device,
3049 format: wgpu::TextureFormat,
3050) -> (
3051 wgpu::RenderPipeline,
3052 wgpu::Buffer,
3053 wgpu::BindGroup,
3054 wgpu::BindGroupLayout,
3055) {
3056 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
3057 label: Some("anim_procedural_shader"),
3058 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/anim_procedural.wgsl").into()),
3059 });
3060
3061 let buffer_size = (MAX_ANIM_SLOTS * std::mem::size_of::<teksilo_canvas::AnimParams>()) as u64;
3062 let anim_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3063 label: Some("anim_uniform_buffer"),
3064 size: buffer_size,
3065 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3066 mapped_at_creation: false,
3067 });
3068
3069 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3070 label: Some("anim_uniform_bind_group_layout"),
3071 entries: &[wgpu::BindGroupLayoutEntry {
3072 binding: 0,
3073 visibility: wgpu::ShaderStages::FRAGMENT,
3074 ty: wgpu::BindingType::Buffer {
3075 ty: wgpu::BufferBindingType::Uniform,
3076 has_dynamic_offset: false,
3077 min_binding_size: None,
3078 },
3079 count: None,
3080 }],
3081 });
3082
3083 let anim_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3084 label: Some("anim_uniform_bind_group"),
3085 layout: &bind_group_layout,
3086 entries: &[wgpu::BindGroupEntry {
3087 binding: 0,
3088 resource: anim_uniform_buffer.as_entire_binding(),
3089 }],
3090 });
3091
3092 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
3093 label: Some("anim_proc_pipeline_layout"),
3094 bind_group_layouts: &[Some(&bind_group_layout)],
3095 immediate_size: 0,
3096 });
3097
3098 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
3099 label: Some("anim_proc_pipeline"),
3100 layout: Some(&pipeline_layout),
3101 vertex: wgpu::VertexState {
3102 module: &shader,
3103 entry_point: Some("vs_main"),
3104 buffers: &[Some(anim_quad_vertex_layout())],
3105 compilation_options: Default::default(),
3106 },
3107 fragment: Some(wgpu::FragmentState {
3108 module: &shader,
3109 entry_point: Some("fs_main"),
3110 targets: &[Some(wgpu::ColorTargetState {
3111 format,
3112 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
3113 write_mask: wgpu::ColorWrites::ALL,
3114 })],
3115 compilation_options: Default::default(),
3116 }),
3117 primitive: wgpu::PrimitiveState {
3118 topology: wgpu::PrimitiveTopology::TriangleList,
3119 ..Default::default()
3120 },
3121 depth_stencil: None,
3122 multisample: wgpu::MultisampleState::default(),
3123 multiview_mask: None,
3124 cache: None,
3125 });
3126
3127 (
3128 pipeline,
3129 anim_uniform_buffer,
3130 anim_uniform_bind_group,
3131 bind_group_layout,
3132 )
3133}
3134
3135fn create_anim_sprite_pipeline(
3142 device: &wgpu::Device,
3143 format: wgpu::TextureFormat,
3144 uniform_layout: &wgpu::BindGroupLayout,
3145 texture_layout: &wgpu::BindGroupLayout,
3146) -> wgpu::RenderPipeline {
3147 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
3148 label: Some("anim_sprite_shader"),
3149 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/anim_sprite.wgsl").into()),
3150 });
3151
3152 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
3153 label: Some("anim_sprite_pipeline_layout"),
3154 bind_group_layouts: &[Some(uniform_layout), Some(texture_layout)],
3155 immediate_size: 0,
3156 });
3157
3158 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
3159 label: Some("anim_sprite_pipeline"),
3160 layout: Some(&pipeline_layout),
3161 vertex: wgpu::VertexState {
3162 module: &shader,
3163 entry_point: Some("vs_main"),
3164 buffers: &[Some(anim_quad_vertex_layout())],
3165 compilation_options: Default::default(),
3166 },
3167 fragment: Some(wgpu::FragmentState {
3168 module: &shader,
3169 entry_point: Some("fs_main"),
3170 targets: &[Some(wgpu::ColorTargetState {
3171 format,
3172 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
3173 write_mask: wgpu::ColorWrites::ALL,
3174 })],
3175 compilation_options: Default::default(),
3176 }),
3177 primitive: wgpu::PrimitiveState {
3178 topology: wgpu::PrimitiveTopology::TriangleList,
3179 ..Default::default()
3180 },
3181 depth_stencil: None,
3182 multisample: wgpu::MultisampleState::default(),
3183 multiview_mask: None,
3184 cache: None,
3185 })
3186}
3187
3188fn anim_quad_vertex_layout() -> wgpu::VertexBufferLayout<'static> {
3190 const ATTRS: [wgpu::VertexAttribute; 3] = [
3191 wgpu::VertexAttribute {
3192 offset: 0,
3193 shader_location: 0,
3194 format: wgpu::VertexFormat::Float32x2,
3195 },
3196 wgpu::VertexAttribute {
3197 offset: 8,
3198 shader_location: 1,
3199 format: wgpu::VertexFormat::Float32x2,
3200 },
3201 wgpu::VertexAttribute {
3202 offset: 16,
3203 shader_location: 2,
3204 format: wgpu::VertexFormat::Uint32,
3205 },
3206 ];
3207 wgpu::VertexBufferLayout {
3208 array_stride: std::mem::size_of::<AnimQuadVertex>() as u64,
3209 step_mode: wgpu::VertexStepMode::Vertex,
3210 attributes: &ATTRS,
3211 }
3212}
3213
3214#[cfg(test)]
3215mod tests {
3216 use teksilo_canvas::RenderFrame;
3217 use teksilo_canvas::render_frame::{DrawCommand, GlyphQuad, PaintData, ShapeKind, ShapeQuad};
3218
3219 use super::*;
3220
3221 #[test]
3222 fn stream_quad_counts_includes_sprite_anim_quads() {
3223 use teksilo_canvas::render_frame::{AnimatedQuadClass, AnimatedQuadDraw};
3230
3231 let mut frame = RenderFrame::new();
3232 for slot in 0..3 {
3233 frame.animated_quads.push(AnimatedQuadDraw {
3234 screen: [0.0, 0.0, 10.0, 10.0],
3235 slot,
3236 class: AnimatedQuadClass::Procedural,
3237 });
3238 }
3239 for slot in 3..5 {
3240 frame.animated_quads.push(AnimatedQuadDraw {
3241 screen: [0.0, 0.0, 10.0, 10.0],
3242 slot,
3243 class: AnimatedQuadClass::Sprite {
3244 image_name: "icon".to_string(),
3245 },
3246 });
3247 }
3248 frame.glyphs.push(GlyphQuad {
3249 screen: [0.0, 0.0, 8.0, 8.0],
3250 atlas: [0.0, 0.0, 2.0, 2.0],
3251 color: [1.0; 4],
3252 is_color: false,
3253 });
3254
3255 let counts = stream_quad_counts(&frame);
3256 assert_eq!(
3257 counts.anim_proc, 5,
3258 "anim_proc stream must be sized for BOTH Procedural and Sprite quads"
3259 );
3260 assert_eq!(counts.quad, 1);
3261 assert_eq!(counts.rect, 0);
3262 assert_eq!(counts.sdf, 0);
3263 assert_eq!(counts.shadow, 0);
3264 assert_eq!(counts.max(), 5, "index buffer sizes to the largest stream");
3265 }
3266
3267 #[test]
3268 fn stream_quad_counts_splits_solid_and_gradient_paths() {
3269 use teksilo_canvas::render_frame::PathEntry;
3275 use teksilo_canvas::{FillRule, GradientStop, Path, StrokeStyle};
3276 use teksilo_tokens::Color;
3277
3278 let mut frame = RenderFrame::new();
3279 frame.paths.push(PathEntry {
3280 path: Path::new(),
3281 color: [1.0, 0.0, 0.0, 1.0],
3282 stroke_style: StrokeStyle::solid(0.0),
3283 fill_rule: FillRule::Winding,
3284 bounds: [0.0, 0.0, 10.0, 10.0],
3285 paint_data: PaintData::Solid,
3286 });
3287 frame.paths.push(PathEntry {
3288 path: Path::new(),
3289 color: [1.0, 1.0, 1.0, 1.0],
3290 stroke_style: StrokeStyle::solid(0.0),
3291 fill_rule: FillRule::Winding,
3292 bounds: [0.0, 0.0, 20.0, 20.0],
3293 paint_data: PaintData::LinearGradient {
3294 start: [0.0, 0.0],
3295 end: [20.0, 0.0],
3296 stops: vec![
3297 GradientStop {
3298 offset: 0.0,
3299 color: Color::RED,
3300 },
3301 GradientStop {
3302 offset: 1.0,
3303 color: Color::BLUE,
3304 },
3305 ],
3306 },
3307 });
3308
3309 let counts = stream_quad_counts(&frame);
3310 assert_eq!(counts.quad, 1, "the solid path counts toward quad");
3311 assert_eq!(
3312 counts.path_gradient, 1,
3313 "the gradient path counts toward path_gradient, not quad"
3314 );
3315 assert_eq!(counts.rect, 0);
3316 assert_eq!(counts.sdf, 0);
3317 assert_eq!(counts.shadow, 0);
3318 assert_eq!(counts.anim_proc, 0);
3319 assert_eq!(counts.max(), 1);
3320 }
3321
3322 #[test]
3323 fn gradient_path_renders_nonflat_on_gpu() {
3324 use teksilo_canvas::render_frame::PathEntry;
3331 use teksilo_canvas::{FillRule, GradientStop, Path, Rect, StrokeStyle};
3332 use teksilo_tokens::Color;
3333
3334 let Some((mut renderer, device, queue)) = pollster::block_on(
3335 crate::test_support::create_test_renderer("teksilo_render_gradient_path_device"),
3336 ) else {
3337 return; };
3339
3340 let path = Path::rect(Rect::new(1.0, 1.0, 30.0, 30.0));
3342 let bounds = path.bounds();
3343 let mut frame = RenderFrame::new();
3344 frame.paths.push(PathEntry {
3345 path,
3346 color: [1.0, 1.0, 1.0, 1.0],
3347 stroke_style: StrokeStyle::solid(0.0),
3348 fill_rule: FillRule::Winding,
3349 bounds: [bounds.x, bounds.y, bounds.width, bounds.height],
3350 paint_data: PaintData::LinearGradient {
3351 start: [bounds.x, bounds.y],
3352 end: [bounds.x + bounds.width, bounds.y],
3353 stops: vec![
3354 GradientStop {
3355 offset: 0.0,
3356 color: Color::RED,
3357 },
3358 GradientStop {
3359 offset: 1.0,
3360 color: Color::BLUE,
3361 },
3362 ],
3363 },
3364 });
3365 frame.draw_order.push(DrawCommand::Path(0));
3366
3367 let texture = device.create_texture(&wgpu::TextureDescriptor {
3368 label: Some("teksilo_render_gradient_path_target"),
3369 size: wgpu::Extent3d {
3370 width: 32,
3371 height: 32,
3372 depth_or_array_layers: 1,
3373 },
3374 mip_level_count: 1,
3375 sample_count: 1,
3376 dimension: wgpu::TextureDimension::D2,
3377 format: wgpu::TextureFormat::Rgba8UnormSrgb,
3378 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
3379 view_formats: &[],
3380 });
3381 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3382
3383 renderer.render(&frame, &view, 1.0, 32, 32, [0.0, 0.0, 0.0, 0.0]);
3387
3388 let pixels = crate::test_support::read_texture_rgba(&device, &queue, &texture, 32, 32);
3389 let px = |x: usize, y: usize| {
3390 let i = (y * 32 + x) * 4;
3391 [pixels[i], pixels[i + 1], pixels[i + 2], pixels[i + 3]]
3392 };
3393 let left = px(4, 16);
3395 let right = px(27, 16);
3396
3397 assert!(
3398 left[3] > 200 && right[3] > 200,
3399 "gradient square not covered (coverage-mask atlas broken): left={left:?} right={right:?}"
3400 );
3401 assert!(
3404 left[0] as i32 > left[2] as i32 + 40,
3405 "left edge must be red-dominant, got {left:?}"
3406 );
3407 assert!(
3408 right[2] as i32 > right[0] as i32 + 40,
3409 "right edge must be blue-dominant, got {right:?}"
3410 );
3411 assert!(
3412 (left[0] as i32 - right[0] as i32).abs() > 60,
3413 "gradient looks flat (shader not sampling the gradient): left={left:?} right={right:?}"
3414 );
3415 }
3416
3417 #[test]
3418 fn gradient_path_partial_alpha_preserved() {
3419 use teksilo_canvas::render_frame::PathEntry;
3426 use teksilo_canvas::{FillRule, GradientStop, Path, Rect, StrokeStyle};
3427 use teksilo_tokens::Color;
3428
3429 let Some((mut renderer, device, queue)) = pollster::block_on(
3430 crate::test_support::create_test_renderer("teksilo_render_partial_alpha_device"),
3431 ) else {
3432 return;
3433 };
3434
3435 let path = Path::rect(Rect::new(0.0, 0.0, 32.0, 32.0));
3436 let bounds = path.bounds();
3437 let mut frame = RenderFrame::new();
3438 frame.paths.push(PathEntry {
3439 path,
3440 color: [1.0, 1.0, 1.0, 1.0],
3441 stroke_style: StrokeStyle::solid(0.0),
3442 fill_rule: FillRule::Winding,
3443 bounds: [bounds.x, bounds.y, bounds.width, bounds.height],
3444 paint_data: PaintData::LinearGradient {
3445 start: [0.0, 0.0],
3446 end: [32.0, 0.0],
3447 stops: vec![
3448 GradientStop {
3449 offset: 0.0,
3450 color: Color::from_rgba(0.0, 0.62, 0.45, 1.0),
3451 },
3452 GradientStop {
3453 offset: 1.0,
3454 color: Color::from_rgba(0.0, 0.62, 0.45, 0.4),
3455 },
3456 ],
3457 },
3458 });
3459 frame.draw_order.push(DrawCommand::Path(0));
3460
3461 let texture = device.create_texture(&wgpu::TextureDescriptor {
3462 label: Some("partial_alpha_target"),
3463 size: wgpu::Extent3d {
3464 width: 32,
3465 height: 32,
3466 depth_or_array_layers: 1,
3467 },
3468 mip_level_count: 1,
3469 sample_count: 1,
3470 dimension: wgpu::TextureDimension::D2,
3471 format: wgpu::TextureFormat::Rgba8UnormSrgb,
3472 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
3473 view_formats: &[],
3474 });
3475 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3476 renderer.render(&frame, &view, 1.0, 32, 32, [0.0, 0.0, 0.0, 0.0]);
3477
3478 let px = crate::test_support::read_texture_rgba(&device, &queue, &texture, 32, 32);
3479 let alpha = |x: usize| px[(16 * 32 + x) * 4 + 3];
3480 let (left, right) = (alpha(2), alpha(29));
3481 assert!(
3483 left >= 240,
3484 "opaque (a=1.0) end must stay opaque, got {left} (/255)"
3485 );
3486 assert!(
3487 (90..=115).contains(&right),
3488 "a=0.4 stop must read ~102/255, got {right} — a value near ~61 means the \
3489 pipeline under-renders gradient stop alpha (washed-out fills)"
3490 );
3491 }
3492
3493 #[test]
3494 fn glyph_quad_renders_over_shape_in_offscreen_target() {
3495 let Some((mut renderer, device, queue)) = pollster::block_on(
3496 crate::test_support::create_test_renderer("teksilo_render_test_device"),
3497 ) else {
3498 return;
3499 };
3500
3501 renderer.upload_atlas(
3502 2,
3503 2,
3504 &[
3505 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
3506 ],
3507 );
3508
3509 let mut frame = RenderFrame::new();
3510 frame.shapes.push(ShapeQuad {
3511 screen: [4.0, 4.0, 24.0, 24.0],
3512 color: [0.2, 0.6, 0.9, 1.0],
3513 shape: ShapeKind::RoundedRect,
3514 stroke_width: 0.0,
3515 stroke_space: teksilo_canvas::StrokeSpace::Logical,
3516 corner_radii: [0.0; 4],
3517 paint_data: PaintData::Solid,
3518 });
3519 frame.draw_order.push(DrawCommand::Shape(0));
3520
3521 frame.glyphs.push(GlyphQuad {
3522 screen: [10.0, 10.0, 8.0, 8.0],
3523 atlas: [0.0, 0.0, 2.0, 2.0],
3524 color: [1.0, 1.0, 1.0, 1.0],
3525 is_color: false,
3526 });
3527 frame.draw_order.push(DrawCommand::Glyph(0));
3528
3529 let texture = device.create_texture(&wgpu::TextureDescriptor {
3530 label: Some("teksilo_render_test_target"),
3531 size: wgpu::Extent3d {
3532 width: 32,
3533 height: 32,
3534 depth_or_array_layers: 1,
3535 },
3536 mip_level_count: 1,
3537 sample_count: 1,
3538 dimension: wgpu::TextureDimension::D2,
3539 format: wgpu::TextureFormat::Rgba8UnormSrgb,
3540 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
3541 view_formats: &[],
3542 });
3543 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3544
3545 renderer.render(&frame, &view, 1.0, 32, 32, [0.0, 0.0, 0.0, 0.0]);
3546
3547 let pixels = crate::test_support::read_texture_rgba(&device, &queue, &texture, 32, 32);
3548 let center = ((14 * 32 + 14) * 4) as usize;
3549 let blue_only = [
3550 pixels[center],
3551 pixels[center + 1],
3552 pixels[center + 2],
3553 pixels[center + 3],
3554 ];
3555
3556 assert!(
3557 blue_only[0] > 200 && blue_only[1] > 200 && blue_only[2] > 200,
3558 "expected glyph pixel to be visible over shape, got {:?}",
3559 blue_only
3560 );
3561 }
3562
3563 #[test]
3564 fn fractional_origin_glyph_renders_pixel_exact() {
3565 let Some((mut renderer, device, queue)) = pollster::block_on(
3574 crate::test_support::create_test_renderer("teksilo_render_snap_test_device"),
3575 ) else {
3576 return;
3577 };
3578
3579 let mut atlas = [0u8; 4 * 4 * 4];
3582 for y in 0..3 {
3583 for x in 0..3 {
3584 let i = (y * 4 + x) * 4;
3585 atlas[i..i + 4].copy_from_slice(&[255, 255, 255, 255]);
3586 }
3587 }
3588 renderer.upload_atlas(4, 4, &atlas);
3589
3590 let mut frame = RenderFrame::new();
3591 frame.glyphs.push(GlyphQuad {
3593 screen: [10.4, 10.6, 3.0, 3.0],
3594 atlas: [0.0, 0.0, 3.0, 3.0],
3595 color: [1.0, 1.0, 1.0, 1.0],
3596 is_color: false,
3597 });
3598 frame.draw_order.push(DrawCommand::Glyph(0));
3599
3600 let texture = device.create_texture(&wgpu::TextureDescriptor {
3601 label: Some("teksilo_render_snap_test_target"),
3602 size: wgpu::Extent3d {
3603 width: 32,
3604 height: 32,
3605 depth_or_array_layers: 1,
3606 },
3607 mip_level_count: 1,
3608 sample_count: 1,
3609 dimension: wgpu::TextureDimension::D2,
3610 format: wgpu::TextureFormat::Rgba8UnormSrgb,
3611 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
3612 view_formats: &[],
3613 });
3614 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3615
3616 renderer.render(&frame, &view, 1.0, 32, 32, [0.0, 0.0, 0.0, 0.0]);
3617
3618 let pixels = crate::test_support::read_texture_rgba(&device, &queue, &texture, 32, 32);
3619 let alpha = |x: usize, y: usize| pixels[(y * 32 + x) * 4 + 3];
3620
3621 for y in 11..14 {
3625 for x in 10..13 {
3626 assert_eq!(
3627 alpha(x, y),
3628 255,
3629 "interior pixel ({x},{y}) must be fully opaque — \
3630 bilinear edge feathering means the snap did not fire"
3631 );
3632 }
3633 }
3634 for y in 10..15 {
3637 for x in 9..14 {
3638 let inside = (10..13).contains(&x) && (11..14).contains(&y);
3639 if !inside {
3640 assert_eq!(
3641 alpha(x, y),
3642 0,
3643 "ring pixel ({x},{y}) must be untouched — \
3644 the snapped quad must not bleed past its bitmap"
3645 );
3646 }
3647 }
3648 }
3649 }
3650
3651 #[test]
3664 fn fractional_origin_path_renders_pixel_exact() {
3665 let Some((mut renderer, device, queue)) = pollster::block_on(
3666 crate::test_support::create_test_renderer("teksilo_render_path_snap_test_device"),
3667 ) else {
3668 return;
3669 };
3670
3671 let mut path = teksilo_canvas::Path::new();
3674 path.move_to(teksilo_canvas::Point::new(8.5, 4.0));
3675 path.line_to(teksilo_canvas::Point::new(8.5, 12.0));
3676 let stroke_style = teksilo_canvas::StrokeStyle::solid(1.0);
3677 let bounds = path.bounds().expand(stroke_style.width);
3678 assert_eq!(bounds.x, 7.5, "the half-pixel origin this test is about");
3679
3680 let mut frame = RenderFrame::new();
3681 frame.paths.push(teksilo_canvas::PathEntry {
3682 path,
3683 color: [1.0, 1.0, 1.0, 1.0],
3684 stroke_style,
3685 fill_rule: teksilo_canvas::FillRule::Winding,
3686 bounds: bounds.to_array(),
3687 paint_data: teksilo_canvas::PaintData::Solid,
3688 });
3689 frame.draw_order.push(DrawCommand::Path(0));
3690
3691 let texture = device.create_texture(&wgpu::TextureDescriptor {
3692 label: Some("teksilo_render_path_snap_test_target"),
3693 size: wgpu::Extent3d {
3694 width: 32,
3695 height: 32,
3696 depth_or_array_layers: 1,
3697 },
3698 mip_level_count: 1,
3699 sample_count: 1,
3700 dimension: wgpu::TextureDimension::D2,
3701 format: wgpu::TextureFormat::Rgba8UnormSrgb,
3702 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
3703 view_formats: &[],
3704 });
3705 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3706
3707 renderer.render(&frame, &view, 1.0, 32, 32, [0.0, 0.0, 0.0, 0.0]);
3708
3709 let pixels = crate::test_support::read_texture_rgba(&device, &queue, &texture, 32, 32);
3710 let alpha = |x: usize, y: usize| pixels[(y * 32 + x) * 4 + 3];
3711
3712 for y in 5..11 {
3713 assert_eq!(
3714 alpha(8, y),
3715 255,
3716 "the hairline's own column must be fully inked at y={y} — \
3717 anything less means the quad was resampled off the pixel grid"
3718 );
3719 for x in [6, 7, 9, 10] {
3720 assert_eq!(
3721 alpha(x, y),
3722 0,
3723 "({x},{y}) must be untouched — a 1 px stroke that leaks \
3724 into its neighbours is the blur this snap removes"
3725 );
3726 }
3727 }
3728 }
3729
3730 fn render_to_pixels(
3733 renderer: &mut Renderer,
3734 device: &wgpu::Device,
3735 queue: &wgpu::Queue,
3736 frame: &RenderFrame,
3737 ) -> Vec<u8> {
3738 let texture = device.create_texture(&wgpu::TextureDescriptor {
3739 label: Some("teksilo_render_blur_isolation_target"),
3740 size: wgpu::Extent3d {
3741 width: 64,
3742 height: 32,
3743 depth_or_array_layers: 1,
3744 },
3745 mip_level_count: 1,
3746 sample_count: 1,
3747 dimension: wgpu::TextureDimension::D2,
3748 format: wgpu::TextureFormat::Rgba8UnormSrgb,
3749 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
3750 view_formats: &[],
3751 });
3752 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3753 renderer.render(frame, &view, 1.0, 64, 32, [0.0, 0.0, 0.0, 0.0]);
3754 crate::test_support::read_texture_rgba(device, queue, &texture, 64, 32)
3755 }
3756
3757 fn push_blurred_square(
3759 frame: &mut RenderFrame,
3760 bounds: teksilo_canvas::Rect,
3761 radius: f32,
3762 square: teksilo_canvas::Rect,
3763 ) {
3764 frame
3765 .draw_order
3766 .push(DrawCommand::BeginBlurredSubtree { bounds, radius });
3767 let idx = frame.decorations.len();
3768 frame.decorations.push(teksilo_canvas::DecorationRect {
3769 rect: square.to_array(),
3770 color: [1.0, 1.0, 1.0, 1.0],
3771 kind: teksilo_canvas::DecorationKind::WidgetBackground,
3772 });
3773 frame.draw_order.push(DrawCommand::Decoration(idx));
3774 frame.draw_order.push(DrawCommand::EndBlurredSubtree);
3775 }
3776
3777 #[test]
3778 fn a_blur_scope_does_not_take_the_kernel_of_a_later_one() {
3779 use teksilo_canvas::Rect;
3780 let Some((mut renderer, device, queue)) = pollster::block_on(
3781 crate::test_support::create_test_renderer("teksilo_render_blur_isolation_device"),
3782 ) else {
3783 return; };
3785
3786 let a_bounds = Rect::new(0.0, 0.0, 32.0, 32.0);
3788 let a_square = Rect::new(12.0, 12.0, 8.0, 8.0);
3789 let mut alone = RenderFrame::new();
3790 push_blurred_square(&mut alone, a_bounds, 6.0, a_square);
3791
3792 let mut followed = RenderFrame::new();
3796 push_blurred_square(&mut followed, a_bounds, 6.0, a_square);
3797 push_blurred_square(
3798 &mut followed,
3799 Rect::new(40.0, 8.0, 16.0, 16.0),
3800 2.0,
3801 Rect::new(44.0, 12.0, 8.0, 8.0),
3802 );
3803
3804 let a_alone = render_to_pixels(&mut renderer, &device, &queue, &alone);
3805 let a_followed = render_to_pixels(&mut renderer, &device, &queue, &followed);
3806 let region = |pixels: &[u8]| -> Vec<u8> {
3807 (0..32)
3808 .flat_map(|y| pixels[y * 64 * 4..(y * 64 + 32) * 4].to_vec())
3809 .collect()
3810 };
3811 assert!(
3812 region(&a_alone).iter().any(|&b| b != 0),
3813 "scope A must have drawn something"
3814 );
3815 assert_eq!(
3816 region(&a_alone),
3817 region(&a_followed),
3818 "scope A must blur the same whatever blur scope follows it in the frame"
3819 );
3820 }
3821
3822 #[test]
3823 fn each_kawase_pass_gets_its_own_parameter_slot() {
3824 let Some((_renderer, device, queue)) = pollster::block_on(
3825 crate::test_support::create_test_renderer("teksilo_render_blur_slots_device"),
3826 ) else {
3827 return; };
3829 let mut arena = crate::blur::KawaseParamArena::new(&device);
3830 let passes = crate::blur::PARAM_SLOTS_PER_SCOPE + 3;
3832 arena.begin_frame(&device, passes);
3833 let offsets: Vec<u32> = (0..passes)
3834 .map(|i| {
3835 let v = i as f32;
3836 arena.push(
3837 &queue,
3838 &crate::blur::BlurParams {
3839 offset: [v, v + 0.5, 0.0, 0.0],
3840 },
3841 )
3842 })
3843 .collect();
3844 let stride = arena.slot_stride();
3845 for (i, offset) in offsets.iter().enumerate() {
3846 assert_eq!(u64::from(*offset), stride * i as u64);
3847 assert_eq!(
3848 u64::from(*offset) % u64::from(device.limits().min_uniform_buffer_offset_alignment),
3849 0,
3850 "dynamic offsets must respect the device alignment"
3851 );
3852 }
3853
3854 let size = arena.buffer().size();
3855 let readback = device.create_buffer(&wgpu::BufferDescriptor {
3856 label: Some("blur_params_readback"),
3857 size,
3858 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
3859 mapped_at_creation: false,
3860 });
3861 let mut encoder =
3862 device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
3863 encoder.copy_buffer_to_buffer(arena.buffer(), 0, &readback, 0, size);
3864 queue.submit(Some(encoder.finish()));
3865 readback.slice(..).map_async(wgpu::MapMode::Read, |_| {});
3866 device
3867 .poll(wgpu::PollType::Wait {
3868 submission_index: None,
3869 timeout: None,
3870 })
3871 .expect("poll");
3872 let bytes = readback
3873 .slice(..)
3874 .get_mapped_range()
3875 .expect("mapped readback")
3876 .to_vec();
3877 for (i, offset) in offsets.iter().enumerate() {
3878 let at = *offset as usize;
3879 let slot: &[f32] = bytemuck::cast_slice(&bytes[at..at + 16]);
3880 assert_eq!(slot, &[i as f32, i as f32 + 0.5, 0.0, 0.0], "slot {i}");
3881 }
3882 }
3883
3884 fn render_scaled(
3887 renderer: &mut Renderer,
3888 device: &wgpu::Device,
3889 queue: &wgpu::Queue,
3890 frame: &RenderFrame,
3891 scale: f32,
3892 (w, h): (u32, u32),
3893 ) -> impl Fn(u32, u32) -> [u8; 4] {
3894 let texture = device.create_texture(&wgpu::TextureDescriptor {
3895 label: Some("teksilo_render_clip_target"),
3896 size: wgpu::Extent3d {
3897 width: w,
3898 height: h,
3899 depth_or_array_layers: 1,
3900 },
3901 mip_level_count: 1,
3902 sample_count: 1,
3903 dimension: wgpu::TextureDimension::D2,
3904 format: wgpu::TextureFormat::Rgba8UnormSrgb,
3905 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
3906 view_formats: &[],
3907 });
3908 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3909 renderer.render(frame, &view, scale, w, h, [0.0, 0.0, 0.0, 0.0]);
3910 let pixels = crate::test_support::read_texture_rgba(device, queue, &texture, w, h);
3911 move |x, y| {
3912 let i = ((y * w + x) * 4) as usize;
3913 [pixels[i], pixels[i + 1], pixels[i + 2], pixels[i + 3]]
3914 }
3915 }
3916
3917 fn push_white_rect(frame: &mut RenderFrame, rect: teksilo_canvas::Rect) {
3918 let idx = frame.decorations.len();
3919 frame.decorations.push(teksilo_canvas::DecorationRect {
3920 rect: rect.to_array(),
3921 color: [1.0, 1.0, 1.0, 1.0],
3922 kind: teksilo_canvas::DecorationKind::WidgetBackground,
3923 });
3924 frame.draw_order.push(DrawCommand::Decoration(idx));
3925 }
3926
3927 #[test]
3928 fn a_clip_under_a_translation_lands_where_the_content_does_at_fractional_scale() {
3929 use teksilo_canvas::Rect;
3930 let Some((mut renderer, device, queue)) = pollster::block_on(
3931 crate::test_support::create_test_renderer("teksilo_render_clip_translate_device"),
3932 ) else {
3933 return; };
3935 let mut frame = RenderFrame::new();
3939 frame
3940 .draw_order
3941 .push(DrawCommand::PushTransform(Transform2D::translate(
3942 10.0, 0.0,
3943 )));
3944 frame
3945 .draw_order
3946 .push(DrawCommand::SetClip(Rect::new(0.0, 0.0, 20.0, 20.0)));
3947 push_white_rect(&mut frame, Rect::new(0.0, 0.0, 40.0, 20.0));
3948 frame.draw_order.push(DrawCommand::ClearClip);
3949 frame.draw_order.push(DrawCommand::PopTransform);
3950
3951 let px = render_scaled(&mut renderer, &device, &queue, &frame, 1.5, (96, 40));
3952 assert_eq!(px(17, 10)[3], 255, "inside the clip, near its leading edge");
3953 assert_eq!(
3954 px(43, 10)[3],
3955 255,
3956 "inside the clip, near its trailing edge"
3957 );
3958 assert_eq!(px(47, 10)[3], 0, "past the clip's trailing edge");
3959 assert_eq!(px(12, 10)[3], 0, "before the content starts");
3960 }
3961
3962 #[test]
3963 fn a_clip_still_applies_after_a_blur_scope_reopens_the_pass() {
3964 use teksilo_canvas::Rect;
3965 let Some((mut renderer, device, queue)) = pollster::block_on(
3966 crate::test_support::create_test_renderer("teksilo_render_clip_after_blur_device"),
3967 ) else {
3968 return; };
3970 let mut frame = RenderFrame::new();
3974 frame
3975 .draw_order
3976 .push(DrawCommand::SetClip(Rect::new(0.0, 0.0, 20.0, 32.0)));
3977 push_blurred_square(
3978 &mut frame,
3979 Rect::new(0.0, 0.0, 16.0, 16.0),
3980 2.0,
3981 Rect::new(4.0, 4.0, 8.0, 8.0),
3982 );
3983 push_white_rect(&mut frame, Rect::new(0.0, 20.0, 64.0, 12.0));
3984 frame.draw_order.push(DrawCommand::ClearClip);
3985
3986 let px = render_scaled(&mut renderer, &device, &queue, &frame, 1.0, (64, 32));
3987 assert_eq!(px(10, 26)[3], 255, "inside the clip");
3988 assert_eq!(px(40, 26)[3], 0, "outside the clip");
3989 }
3990
3991 #[test]
3992 fn a_clip_set_before_a_blur_scope_does_not_clip_inside_its_intermediate() {
3993 use teksilo_canvas::Rect;
3994 let Some((mut renderer, device, queue)) = pollster::block_on(
3995 crate::test_support::create_test_renderer("teksilo_render_clip_into_blur_device"),
3996 ) else {
3997 return; };
3999 let mut frame = RenderFrame::new();
4005 frame
4006 .draw_order
4007 .push(DrawCommand::SetClip(Rect::new(30.0, 0.0, 34.0, 32.0)));
4008 push_blurred_square(
4009 &mut frame,
4010 Rect::new(30.0, 0.0, 16.0, 16.0),
4011 2.0,
4012 Rect::new(32.0, 2.0, 12.0, 12.0),
4013 );
4014 frame.draw_order.push(DrawCommand::ClearClip);
4015
4016 let px = render_scaled(&mut renderer, &device, &queue, &frame, 1.0, (64, 32));
4017 assert!(
4018 px(38, 8)[3] > 128,
4019 "the blurred square shows at the centre of the scope, got {:?}",
4020 px(38, 8)
4021 );
4022 }
4023}