1use wgpu;
5
6use teksilo_canvas::RenderFrame;
7use teksilo_canvas::geometry::Transform2D;
8
9use teksilo_canvas::live_image::LiveTextureStats;
10use teksilo_canvas::live_image::internal::{LivePass, LivePassMode, QuadDecision};
11
12use crate::blur::{BlurPipelines, BlurPool};
13use crate::device_health::DeviceHealth;
14use crate::image_manager::ImageManager;
15use crate::live_texture::WgpuBackend;
16use crate::path_atlas::PathAtlas;
17use crate::stream_buffer::StreamBuffers;
18use crate::vertex::{AnimQuadVertex, QuadVertex, RectVertex, SdfVertex, ShadowVertex};
19
20const MAX_ANIM_SLOTS: usize = 128;
25
26pub struct Renderer {
28 device: wgpu::Device,
29 queue: wgpu::Queue,
30 max_texture_dimension: u32,
36 rect_pipeline: wgpu::RenderPipeline,
37 sdf_pipeline: wgpu::RenderPipeline,
38 quad_pipeline: wgpu::RenderPipeline,
39 shadow_pipeline: wgpu::RenderPipeline,
40 path_gradient_pipeline: wgpu::RenderPipeline,
47 anim_proc_pipeline: wgpu::RenderPipeline,
51 anim_sprite_pipeline: wgpu::RenderPipeline,
59 anim_uniform_buffer: wgpu::Buffer,
64 anim_uniform_bind_group: wgpu::BindGroup,
66 atlas_texture: Option<AtlasTexture>,
67 path_atlas: PathAtlas,
68 path_atlas_texture: Option<AtlasTexture>,
69 image_manager: ImageManager,
70 streams: StreamBuffers,
75 blur_pipelines: BlurPipelines,
79 blur_pool: BlurPool,
83 quad_bind_group_layout: wgpu::BindGroupLayout,
88 blur_composite_sampler: wgpu::Sampler,
92 live: LivePass<WgpuBackend>,
95 #[cfg(any(debug_assertions, feature = "live-image-timings"))]
97 live_timings: crate::live_timings::LiveTimingRings,
98 last_submission: Option<wgpu::SubmissionIndex>,
101}
102
103impl Drop for Renderer {
104 fn drop(&mut self) {
107 if let Some(submission) = self.last_submission.take() {
108 crate::gpu_reclaim::flag_device(self.live.backend().health(), &self.device, submission);
109 }
110 }
111}
112
113struct AtlasTexture {
114 texture: wgpu::Texture,
115 bind_group: wgpu::BindGroup,
116 width: u32,
117 height: u32,
118}
119
120struct ActiveTarget {
127 intermediate: Option<crate::blur::AcquiredTexture>,
130 viewport_w: u32,
132 viewport_h: u32,
133 opened: bool,
136 pending_composites: Vec<PendingComposite>,
140 clip_stack: Vec<[u32; 4]>,
147 blur_bounds: Option<teksilo_canvas::Rect>,
151 blur_radius_logical: Option<f32>,
152 used_w: Option<u32>,
153 used_h: Option<u32>,
154 bucket_w: Option<u32>,
155 bucket_h: Option<u32>,
156}
157
158impl ActiveTarget {
159 fn surface(viewport_w: u32, viewport_h: u32) -> Self {
160 Self {
161 intermediate: None,
162 viewport_w,
163 viewport_h,
164 opened: false,
165 pending_composites: Vec::new(),
166 clip_stack: Vec::new(),
167 blur_bounds: None,
168 blur_radius_logical: None,
169 used_w: None,
170 used_h: None,
171 bucket_w: None,
172 bucket_h: None,
173 }
174 }
175}
176
177struct PendingComposite {
181 blurred_texture: crate::blur::AcquiredTexture,
182 used_w: u32,
183 used_h: u32,
184 bucket_w: u32,
185 bucket_h: u32,
186 bounds: teksilo_canvas::Rect,
187}
188
189impl Renderer {
190 pub fn new(
195 device: wgpu::Device,
196 queue: wgpu::Queue,
197 surface_format: wgpu::TextureFormat,
198 ) -> Self {
199 let health = DeviceHealth::install(&device);
200 Self::with_device_health(device, queue, surface_format, health)
201 }
202
203 pub fn with_device_health(
209 device: wgpu::Device,
210 queue: wgpu::Queue,
211 surface_format: wgpu::TextureFormat,
212 health: DeviceHealth,
213 ) -> Self {
214 let device_max_texture_dimension = device.limits().max_texture_dimension_2d;
217 let rect_pipeline = create_rect_pipeline(&device, surface_format);
218 let sdf_pipeline = create_sdf_pipeline(&device, surface_format);
219 let quad_pipeline = create_quad_pipeline(&device, surface_format);
220 let path_gradient_pipeline = create_path_gradient_pipeline(
224 &device,
225 surface_format,
226 &quad_pipeline.get_bind_group_layout(0),
227 );
228 let shadow_pipeline = create_shadow_pipeline(&device, surface_format);
229 let (anim_proc_pipeline, anim_uniform_buffer, anim_uniform_bind_group, anim_uniform_layout) =
230 create_anim_proc_pipeline(&device, surface_format);
231 let quad_texture_layout = quad_pipeline.get_bind_group_layout(0);
235 let anim_sprite_pipeline = create_anim_sprite_pipeline(
236 &device,
237 surface_format,
238 &anim_uniform_layout,
239 &quad_texture_layout,
240 );
241
242 let quad_bind_group_layout = quad_pipeline.get_bind_group_layout(0);
243 let blur_pool = BlurPool::new(&device, surface_format);
244 let blur_pipelines =
245 BlurPipelines::new(&device, &blur_pool.bind_group_layout, surface_format);
246 let blur_composite_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
247 label: Some("blur_composite_sampler"),
248 address_mode_u: wgpu::AddressMode::ClampToEdge,
249 address_mode_v: wgpu::AddressMode::ClampToEdge,
250 address_mode_w: wgpu::AddressMode::ClampToEdge,
251 mag_filter: wgpu::FilterMode::Linear,
252 min_filter: wgpu::FilterMode::Linear,
253 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
254 ..Default::default()
255 });
256
257 let live_textures = health.live_textures().clone();
258 let mut live = LivePass::new(WgpuBackend::new(
259 device.clone(),
260 queue.clone(),
261 quad_bind_group_layout.clone(),
262 health,
263 ));
264 live.join_device(&live_textures);
265
266 Self {
267 device,
268 queue,
269 max_texture_dimension: device_max_texture_dimension,
270 rect_pipeline,
271 sdf_pipeline,
272 quad_pipeline,
273 path_gradient_pipeline,
274 shadow_pipeline,
275 anim_proc_pipeline,
276 anim_sprite_pipeline,
277 anim_uniform_buffer,
278 anim_uniform_bind_group,
279 atlas_texture: None,
280 path_atlas: {
281 let mut atlas = PathAtlas::new(512, 512);
282 atlas.cap_max_size(device_max_texture_dimension);
283 atlas
284 },
285 path_atlas_texture: None,
286 image_manager: ImageManager::new(),
287 streams: StreamBuffers::new(),
288 blur_pipelines,
289 blur_pool,
290 quad_bind_group_layout,
291 blur_composite_sampler,
292 live,
293 #[cfg(any(debug_assertions, feature = "live-image-timings"))]
294 live_timings: crate::live_timings::LiveTimingRings::new(),
295 last_submission: None,
296 }
297 }
298
299 pub fn upload_atlas(&mut self, width: u32, height: u32, pixels: &[u8]) {
301 if width == 0 || height == 0 {
302 return;
303 }
304 if width > self.max_texture_dimension || height > self.max_texture_dimension {
312 eprintln!(
313 "teksilo-render: glyph atlas {width}x{height} exceeds this device's \
314 max texture dimension ({}); skipping upload",
315 self.max_texture_dimension
316 );
317 return;
318 }
319
320 let needs_recreate = self
321 .atlas_texture
322 .as_ref()
323 .is_none_or(|t| t.width != width || t.height != height);
324
325 if needs_recreate {
326 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
327 label: Some("glyph_atlas"),
328 size: wgpu::Extent3d {
329 width,
330 height,
331 depth_or_array_layers: 1,
332 },
333 mip_level_count: 1,
334 sample_count: 1,
335 dimension: wgpu::TextureDimension::D2,
336 format: wgpu::TextureFormat::Rgba8UnormSrgb,
337 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
338 view_formats: &[],
339 });
340
341 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
342 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
354 mag_filter: wgpu::FilterMode::Linear,
355 min_filter: wgpu::FilterMode::Linear,
356 ..Default::default()
357 });
358
359 let bind_group_layout = self.quad_pipeline.get_bind_group_layout(0);
360 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
361 label: Some("atlas_bind_group"),
362 layout: &bind_group_layout,
363 entries: &[
364 wgpu::BindGroupEntry {
365 binding: 0,
366 resource: wgpu::BindingResource::TextureView(&view),
367 },
368 wgpu::BindGroupEntry {
369 binding: 1,
370 resource: wgpu::BindingResource::Sampler(&sampler),
371 },
372 ],
373 });
374
375 self.atlas_texture = Some(AtlasTexture {
376 texture,
377 bind_group,
378 width,
379 height,
380 });
381 }
382
383 if let Some(atlas) = &self.atlas_texture {
384 self.queue.write_texture(
385 wgpu::TexelCopyTextureInfo {
386 texture: &atlas.texture,
387 mip_level: 0,
388 origin: wgpu::Origin3d::ZERO,
389 aspect: wgpu::TextureAspect::All,
390 },
391 pixels,
392 wgpu::TexelCopyBufferLayout {
393 offset: 0,
394 bytes_per_row: Some(width * 4),
395 rows_per_image: Some(height),
396 },
397 wgpu::Extent3d {
398 width,
399 height,
400 depth_or_array_layers: 1,
401 },
402 );
403 }
404 }
405
406 pub fn render(
412 &mut self,
413 frame: &RenderFrame,
414 view: &wgpu::TextureView,
415 scale_factor: f32,
416 viewport_width: u32,
417 viewport_height: u32,
418 clear_color: [f32; 4],
419 ) {
420 self.render_frame(
421 frame,
422 view,
423 scale_factor,
424 viewport_width,
425 viewport_height,
426 clear_color,
427 LivePassMode::Present,
428 );
429 }
430
431 pub fn render_capture(
437 &mut self,
438 frame: &RenderFrame,
439 view: &wgpu::TextureView,
440 scale_factor: f32,
441 viewport_width: u32,
442 viewport_height: u32,
443 clear_color: [f32; 4],
444 ) {
445 self.render_frame(
446 frame,
447 view,
448 scale_factor,
449 viewport_width,
450 viewport_height,
451 clear_color,
452 LivePassMode::Capture,
453 );
454 }
455
456 pub fn live_texture_stats(&self) -> LiveTextureStats {
458 self.live.stats()
459 }
460
461 #[cfg(any(debug_assertions, feature = "live-image-timings"))]
466 pub fn live_texture_timings(&self) -> crate::LiveImageTimings {
467 self.live_timings.timings()
468 }
469
470 #[doc(hidden)]
473 pub fn live_image_decisions(&self) -> &[QuadDecision] {
474 self.live.decisions()
475 }
476
477 #[doc(hidden)]
480 pub fn set_live_refresh_interval(&mut self, interval: std::time::Duration) {
481 self.live.set_refresh_interval(interval);
482 }
483
484 #[doc(hidden)]
486 pub fn live_refresh_interval(&self) -> std::time::Duration {
487 self.live.refresh_interval()
488 }
489
490 #[doc(hidden)]
494 pub fn set_live_max_dimension(&mut self, max: u32) {
495 self.live.backend_mut().set_max_dimension(max);
496 }
497
498 #[doc(hidden)]
499 pub fn set_live_band_bytes(&mut self, bytes: u64) {
500 self.live.set_band_bytes(bytes);
501 }
502
503 #[doc(hidden)]
504 pub fn set_live_staging_budget(&mut self, bytes: u64) {
505 self.live.set_staging_budget(bytes);
506 }
507
508 #[doc(hidden)]
509 pub fn set_live_park_budget(&mut self, bytes: u64) {
510 self.live.set_park_budget(bytes);
511 }
512
513 #[doc(hidden)]
514 pub fn fail_next_live_texture(&mut self) {
515 self.live.backend_mut().fail_next_create();
516 }
517
518 #[doc(hidden)]
522 pub fn live_staging_stats(&self) -> crate::live_staging::StagingStats {
523 self.live.backend().staging_stats()
524 }
525
526 #[doc(hidden)]
527 pub fn set_live_staging_idle_lifetime(&mut self, lifetime: std::time::Duration) {
528 self.live.backend_mut().set_staging_idle_lifetime(lifetime);
529 }
530
531 #[doc(hidden)]
532 pub fn fail_next_live_staging(&mut self) {
533 self.live.backend_mut().fail_next_staging();
534 }
535
536 #[doc(hidden)]
540 pub fn read_live_texture_level(
541 &self,
542 id: teksilo_canvas::live_image::LiveImageId,
543 level: u32,
544 ) -> Option<(u32, u32, Vec<u8>)> {
545 let texture = self.live.texture(id)?;
546 if level >= texture.levels() {
547 return None;
548 }
549 let (w, h) = texture.size();
550 let (lw, lh) = teksilo_canvas::live_image::internal::mip_level_size(w, h, level);
551 let pixels = crate::test_support::try_read_texture_level_rgba(
552 &self.device,
553 &self.queue,
554 &texture.texture,
555 level,
556 lw,
557 lh,
558 )
559 .ok()?;
560 Some((lw, lh, pixels))
561 }
562
563 #[doc(hidden)]
565 pub fn device_health(&self) -> &DeviceHealth {
566 self.live.backend().health()
567 }
568
569 #[allow(clippy::too_many_arguments)]
570 fn render_frame(
571 &mut self,
572 frame: &RenderFrame,
573 view: &wgpu::TextureView,
574 scale_factor: f32,
575 viewport_width: u32,
576 viewport_height: u32,
577 clear_color: [f32; 4],
578 mode: LivePassMode,
579 ) {
580 self.path_atlas.begin_frame();
582 self.blur_pool.begin_frame();
585 let blur_scopes = frame
586 .draw_order
587 .iter()
588 .filter(|c| matches!(c, teksilo_canvas::DrawCommand::BeginBlurredSubtree { .. }))
589 .count();
590 self.blur_pipelines.params.begin_frame(
591 &self.device,
592 u32::try_from(blur_scopes)
593 .unwrap_or(u32::MAX)
594 .saturating_mul(crate::blur::PARAM_SLOTS_PER_SCOPE),
595 );
596
597 for pending in &frame.pending_images {
599 if !self.image_manager.contains(&pending.name) {
600 let layout = self.quad_pipeline.get_bind_group_layout(0);
601 self.image_manager.register_image(
602 &pending.name,
603 pending.width,
604 pending.height,
605 &pending.pixels,
606 &self.device,
607 &self.queue,
608 &layout,
609 );
610 }
611 }
612
613 #[cfg(any(debug_assertions, feature = "live-image-timings"))]
616 let live_started = std::time::Instant::now();
617 self.live.prepare(&frame.live_images, mode);
618 #[cfg(any(debug_assertions, feature = "live-image-timings"))]
619 let live_prepared = live_started.elapsed();
620
621 let mut path_placements: Vec<Option<crate::path_atlas::PathPlacement>> =
633 vec![None; frame.paths.len()];
634 {
635 let mut ptf_stack: Vec<Transform2D> = vec![Transform2D::IDENTITY];
636 let mut ptf_current = Transform2D::IDENTITY;
637 let device_t = |t: &Transform2D| Transform2D {
638 m: [
639 t.m[0],
640 t.m[1],
641 t.m[2],
642 t.m[3],
643 t.m[4] * scale_factor,
644 t.m[5] * scale_factor,
645 ],
646 };
647 for cmd in &frame.draw_order {
648 match cmd {
649 teksilo_canvas::DrawCommand::SetTransform(t) => {
650 let stack_top = ptf_stack.last().copied().unwrap_or(Transform2D::IDENTITY);
651 ptf_current = device_t(t).then(&stack_top);
652 }
653 teksilo_canvas::DrawCommand::PushTransform(t) => {
654 let prev_top = ptf_stack.last().copied().unwrap_or(Transform2D::IDENTITY);
655 let new_top = device_t(t).then(&prev_top);
656 ptf_stack.push(new_top);
657 ptf_current = new_top;
658 }
659 teksilo_canvas::DrawCommand::PopTransform => {
660 if ptf_stack.len() > 1 {
661 ptf_stack.pop();
662 }
663 ptf_current = ptf_stack.last().copied().unwrap_or(Transform2D::IDENTITY);
664 }
665 teksilo_canvas::DrawCommand::Path(idx) => {
666 if let Some(entry) = frame.paths.get(*idx) {
667 let zoom = ptf_current.m[0].hypot(ptf_current.m[1]);
671 let snap = ptf_current == Transform2D::IDENTITY;
681 path_placements[*idx] = self.path_atlas.lookup_or_rasterize(
682 &entry.path,
683 &entry.stroke_style,
684 entry.fill_rule,
685 entry.bounds,
686 scale_factor,
687 zoom,
688 snap,
689 );
690 }
691 }
692 _ => {}
693 }
694 }
695 }
696
697 if self.path_atlas.is_dirty() {
699 let (pw, ph) = self.path_atlas.size();
700 self.upload_path_atlas(pw, ph, self.path_atlas.pixels().to_vec());
701 self.path_atlas.mark_clean();
702 }
703
704 let counts = stream_quad_counts(frame);
706 let StreamQuadCounts {
707 rect: rect_quads,
708 sdf: sdf_quads,
709 quad: quad_quads,
710 shadow: shadow_quads,
711 anim_proc: anim_proc_quads,
712 path_gradient: path_gradient_quads,
713 } = counts;
714 let max_quads = counts.max();
715
716 self.streams.rect.ensure_capacity(
717 &self.device,
718 (rect_quads * 4 * std::mem::size_of::<RectVertex>()) as u64,
719 );
720 self.streams.sdf.ensure_capacity(
721 &self.device,
722 (sdf_quads * 4 * std::mem::size_of::<SdfVertex>()) as u64,
723 );
724 self.streams.quad.ensure_capacity(
725 &self.device,
726 (quad_quads * 4 * std::mem::size_of::<QuadVertex>()) as u64,
727 );
728 self.streams.shadow.ensure_capacity(
729 &self.device,
730 (shadow_quads * 4 * std::mem::size_of::<ShadowVertex>()) as u64,
731 );
732 self.streams.anim_proc.ensure_capacity(
733 &self.device,
734 (anim_proc_quads * 4 * std::mem::size_of::<AnimQuadVertex>()) as u64,
735 );
736 self.streams.path_gradient.ensure_capacity(
737 &self.device,
738 (path_gradient_quads * 4 * std::mem::size_of::<crate::vertex::PathGradientVertex>())
739 as u64,
740 );
741 self.streams.index.ensure_capacity(
742 &self.device,
743 (max_quads * 6 * std::mem::size_of::<u32>()) as u64,
744 );
745 self.streams.reset();
746
747 if !frame.anim_params.is_empty() {
753 let n = frame.anim_params.len().min(MAX_ANIM_SLOTS);
754 debug_assert!(
755 frame.anim_params.len() <= MAX_ANIM_SLOTS,
756 "AnimParams exceeds MAX_ANIM_SLOTS ({}); tail will be dropped",
757 MAX_ANIM_SLOTS
758 );
759 let bytes: &[u8] = bytemuck::cast_slice(&frame.anim_params[..n]);
760 self.queue.write_buffer(&self.anim_uniform_buffer, 0, bytes);
761 }
762
763 let index_data: Vec<u32> = crate::vertex::generate_quad_indices(max_quads);
767 let index_binding = self
768 .streams
769 .index
770 .write(&self.queue, bytemuck::cast_slice(&index_data));
771
772 let mut encoder = self
773 .device
774 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
775 label: Some("teksilo_render"),
776 });
777 #[cfg(any(debug_assertions, feature = "live-image-timings"))]
782 let mips_started = std::time::Instant::now();
783 self.live.backend_mut().encode(&mut encoder);
784 #[cfg(any(debug_assertions, feature = "live-image-timings"))]
785 self.live_timings.record(
786 (!frame.live_images.is_empty()).then(|| live_prepared + mips_started.elapsed()),
787 self.live.last_timings(),
788 );
789
790 let mut viewport_width = viewport_width;
794 let mut viewport_height = viewport_height;
795
796 {
797 let surface_clear_color = wgpu::Color {
798 r: clear_color[0] as f64,
799 g: clear_color[1] as f64,
800 b: clear_color[2] as f64,
801 a: clear_color[3] as f64,
802 };
803
804 let mut target_stack: Vec<ActiveTarget> =
815 vec![ActiveTarget::surface(viewport_width, viewport_height)];
816
817 let mut opacity_stack: Vec<f32> = vec![1.0];
819 let mut current_opacity: f32 = 1.0;
820
821 let mut blend_stack: Vec<teksilo_canvas::BlendMode> = Vec::new();
823 let mut current_blend = teksilo_canvas::BlendMode::Normal;
824 let _ = current_blend; let mut transform_stack: Vec<Transform2D> = vec![Transform2D::IDENTITY];
832 let mut current_transform = Transform2D::IDENTITY;
833
834 let mut rect_batch: Vec<RectVertex> = Vec::new();
839 let mut sdf_batch: Vec<SdfVertex> = Vec::new();
840 let mut quad_batch: Vec<QuadVertex> = Vec::new();
841 let mut shadow_batch: Vec<ShadowVertex> = Vec::new();
842 let mut anim_proc_batch: Vec<AnimQuadVertex> = Vec::new();
843 let mut path_gradient_batch: Vec<crate::vertex::PathGradientVertex> = Vec::new();
844
845 #[derive(Clone, Copy, PartialEq, Eq)]
849 enum QuadSource {
850 GlyphAtlas,
851 PathAtlas,
852 }
853 let mut quad_source: Option<QuadSource> = None;
854
855 macro_rules! flush_stream {
863 ($pass:expr, $queue:expr, $stream:expr, $pipeline:expr,
864 $batch:expr, $index_binding:expr) => {
865 if !$batch.is_empty() {
866 let bytes: &[u8] = bytemuck::cast_slice(&$batch);
867 if let (Some((vb, v_off, v_len)), Some((ib, _, _))) =
868 ($stream.write($queue, bytes), $index_binding)
869 {
870 let quads = ($batch.len() / 4) as u32;
871 let index_count = quads * 6;
872 let index_bytes = (index_count as u64) * 4;
873 $pass.set_pipeline($pipeline);
874 $pass.set_vertex_buffer(0, vb.slice(v_off..v_off + v_len));
875 $pass.set_index_buffer(
876 ib.slice(0..index_bytes),
877 wgpu::IndexFormat::Uint32,
878 );
879 $pass.draw_indexed(0..index_count, 0, 0..1);
880 }
881 $batch.clear();
882 }
883 };
884 }
885
886 macro_rules! flush_all {
888 ($pass:expr, $queue:expr, $streams:expr,
889 $rp:expr, $sp:expr, $qp:expr, $pgp:expr, $shp:expr,
890 $rb:expr, $sb:expr, $qb:expr, $pgb:expr, $shb:expr,
891 $atlas:expr, $path_atlas:expr, $qs:expr, $index_binding:expr) => {
892 flush_stream!($pass, $queue, &$streams.rect, $rp, $rb, $index_binding);
893 flush_stream!($pass, $queue, &$streams.sdf, $sp, $sb, $index_binding);
894 if !$qb.is_empty() {
896 let bg = match $qs {
897 Some(QuadSource::PathAtlas) => {
898 $path_atlas.as_ref().map(|a: &AtlasTexture| &a.bind_group)
899 }
900 _ => $atlas.as_ref().map(|a: &AtlasTexture| &a.bind_group),
901 };
902 if let (Some(bind_group), Some((ib, _, _))) = (bg, $index_binding) {
903 let bytes: &[u8] = bytemuck::cast_slice(&$qb);
904 if let Some((vb, v_off, v_len)) = $streams.quad.write($queue, bytes) {
905 let quads = ($qb.len() / 4) as u32;
906 let index_count = quads * 6;
907 let index_bytes = (index_count as u64) * 4;
908 $pass.set_pipeline($qp);
909 $pass.set_bind_group(0, bind_group, &[]);
910 $pass.set_vertex_buffer(0, vb.slice(v_off..v_off + v_len));
911 $pass.set_index_buffer(
912 ib.slice(0..index_bytes),
913 wgpu::IndexFormat::Uint32,
914 );
915 $pass.draw_indexed(0..index_count, 0, 0..1);
916 }
917 }
918 $qb.clear();
919 }
920 if !$pgb.is_empty() {
926 if let (Some(bind_group), Some((ib, _, _))) = (
927 $path_atlas.as_ref().map(|a: &AtlasTexture| &a.bind_group),
928 $index_binding,
929 ) {
930 let bytes: &[u8] = bytemuck::cast_slice(&$pgb);
931 if let Some((vb, v_off, v_len)) =
932 $streams.path_gradient.write($queue, bytes)
933 {
934 let quads = ($pgb.len() / 4) as u32;
935 let index_count = quads * 6;
936 let index_bytes = (index_count as u64) * 4;
937 $pass.set_pipeline($pgp);
938 $pass.set_bind_group(0, bind_group, &[]);
939 $pass.set_vertex_buffer(0, vb.slice(v_off..v_off + v_len));
940 $pass.set_index_buffer(
941 ib.slice(0..index_bytes),
942 wgpu::IndexFormat::Uint32,
943 );
944 $pass.draw_indexed(0..index_count, 0, 0..1);
945 }
946 }
947 $pgb.clear();
948 }
949 flush_stream!($pass, $queue, &$streams.shadow, $shp, $shb, $index_binding);
950 if !anim_proc_batch.is_empty()
959 && let Some((ib, _, _)) = $index_binding
960 {
961 let bytes: &[u8] = bytemuck::cast_slice(&anim_proc_batch);
962 if let Some((vb, v_off, v_len)) = $streams.anim_proc.write($queue, bytes) {
963 let quads = (anim_proc_batch.len() / 4) as u32;
964 let index_count = quads * 6;
965 let index_bytes = (index_count as u64) * 4;
966 $pass.set_pipeline(&self.anim_proc_pipeline);
967 $pass.set_bind_group(0, &self.anim_uniform_bind_group, &[]);
968 $pass.set_vertex_buffer(0, vb.slice(v_off..v_off + v_len));
969 $pass.set_index_buffer(
970 ib.slice(0..index_bytes),
971 wgpu::IndexFormat::Uint32,
972 );
973 $pass.draw_indexed(0..index_count, 0, 0..1);
974 }
975 anim_proc_batch.clear();
976 }
977 };
978 }
979
980 let mut cmd_idx = 0;
987 while cmd_idx <= frame.draw_order.len() {
988 let (target_view, load_op): (&wgpu::TextureView, wgpu::LoadOp<wgpu::Color>) = {
992 let t = target_stack
993 .last_mut()
994 .expect("surface target always present");
995 let v: &wgpu::TextureView = match t.intermediate {
996 Some(h) => self.blur_pool.view(h),
997 None => view,
998 };
999 let lo = if t.opened {
1000 wgpu::LoadOp::Load
1001 } else if t.intermediate.is_none() {
1002 wgpu::LoadOp::Clear(surface_clear_color)
1003 } else {
1004 wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT)
1005 };
1006 t.opened = true;
1007 viewport_width = t.viewport_w;
1008 viewport_height = t.viewport_h;
1009 (v, lo)
1010 };
1011
1012 let composites_to_draw: Vec<PendingComposite> = std::mem::take(
1018 &mut target_stack
1019 .last_mut()
1020 .expect("target_stack always has the surface target")
1021 .pending_composites,
1022 );
1023
1024 {
1025 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1026 label: Some("teksilo_segment_pass"),
1027 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1028 view: target_view,
1029 resolve_target: None,
1030 ops: wgpu::Operations {
1031 load: load_op,
1032 store: wgpu::StoreOp::Store,
1033 },
1034 depth_slice: None,
1035 })],
1036 depth_stencil_attachment: None,
1037 timestamp_writes: None,
1038 occlusion_query_set: None,
1039 multiview_mask: None,
1040 });
1041
1042 if let Some(&[x, y, w, h]) = target_stack
1048 .last()
1049 .expect("target_stack always has the surface target")
1050 .clip_stack
1051 .last()
1052 {
1053 pass.set_scissor_rect(x, y, w, h);
1054 }
1055
1056 for pc in &composites_to_draw {
1058 composite_blur_quad(
1059 &self.device,
1060 &self.queue,
1061 &mut pass,
1062 &self.blur_pool,
1063 &self.quad_pipeline,
1064 &self.quad_bind_group_layout,
1065 &self.blur_composite_sampler,
1066 &self.streams.quad,
1067 index_binding,
1068 pc.blurred_texture,
1069 pc.used_w,
1070 pc.used_h,
1071 pc.bucket_w,
1072 pc.bucket_h,
1073 pc.bounds,
1074 scale_factor,
1075 viewport_width,
1076 viewport_height,
1077 );
1078 quad_source = None;
1083 }
1084
1085 let pass = &mut pass;
1086
1087 while cmd_idx < frame.draw_order.len() {
1090 let cmd = &frame.draw_order[cmd_idx];
1091 if matches!(
1092 cmd,
1093 teksilo_canvas::DrawCommand::BeginBlurredSubtree { .. }
1094 | teksilo_canvas::DrawCommand::EndBlurredSubtree
1095 ) {
1096 break;
1097 }
1098 match cmd {
1099 teksilo_canvas::DrawCommand::Decoration(idx) => {
1100 flush_all!(
1101 pass,
1102 &self.queue,
1103 self.streams,
1104 &self.rect_pipeline,
1105 &self.sdf_pipeline,
1106 &self.quad_pipeline,
1107 &self.path_gradient_pipeline,
1108 &self.shadow_pipeline,
1109 rect_batch,
1110 sdf_batch,
1111 quad_batch,
1112 path_gradient_batch,
1113 shadow_batch,
1114 self.atlas_texture,
1115 self.path_atlas_texture,
1116 quad_source,
1117 index_binding
1118 );
1119 quad_source = None;
1120 let Some(rect) = frame.decorations.get(*idx) else {
1121 continue;
1122 };
1123 let verts = RectVertex::from_decoration(rect, scale_factor);
1124 for v in &verts {
1125 let tp = apply_transform_pixel(v.position, ¤t_transform);
1126 rect_batch.push(RectVertex {
1127 position: pixel_to_ndc(tp, viewport_width, viewport_height),
1128 color: [
1129 v.color[0],
1130 v.color[1],
1131 v.color[2],
1132 v.color[3] * current_opacity,
1133 ],
1134 });
1135 }
1136 }
1137 teksilo_canvas::DrawCommand::CosmeticLine(idx) => {
1138 flush_all!(
1139 pass,
1140 &self.queue,
1141 self.streams,
1142 &self.rect_pipeline,
1143 &self.sdf_pipeline,
1144 &self.quad_pipeline,
1145 &self.path_gradient_pipeline,
1146 &self.shadow_pipeline,
1147 rect_batch,
1148 sdf_batch,
1149 quad_batch,
1150 path_gradient_batch,
1151 shadow_batch,
1152 self.atlas_texture,
1153 self.path_atlas_texture,
1154 quad_source,
1155 index_binding
1156 );
1157 quad_source = None;
1158 let Some(line) = frame.cosmetic_lines.get(*idx) else {
1159 continue;
1160 };
1161 let p0 = apply_transform_pixel(
1166 [line.from[0] * scale_factor, line.from[1] * scale_factor],
1167 ¤t_transform,
1168 );
1169 let p1 = apply_transform_pixel(
1170 [line.to[0] * scale_factor, line.to[1] * scale_factor],
1171 ¤t_transform,
1172 );
1173 let thickness = (line.width * scale_factor).max(1.0);
1174 let half = thickness * 0.5;
1175 let dx = p1[0] - p0[0];
1176 let dy = p1[1] - p0[1];
1177 let len = (dx * dx + dy * dy).sqrt();
1178 if len < 1e-3 {
1179 continue;
1180 }
1181 let nx = -dy / len;
1183 let ny = dx / len;
1184 let (mut a0, mut a1) = (p0, p1);
1187 if dy.abs() < 0.5 {
1188 let cy = ((p0[1] + p1[1]) * 0.5 - half).round() + half;
1189 a0 = [p0[0], cy];
1190 a1 = [p1[0], cy];
1191 } else if dx.abs() < 0.5 {
1192 let cx = ((p0[0] + p1[0]) * 0.5 - half).round() + half;
1193 a0 = [cx, p0[1]];
1194 a1 = [cx, p1[1]];
1195 }
1196 let lin = crate::vertex::srgb_to_linear_rgba(line.color);
1197 let color = [lin[0], lin[1], lin[2], lin[3] * current_opacity];
1198 let corners = [
1199 [a0[0] + nx * half, a0[1] + ny * half],
1200 [a1[0] + nx * half, a1[1] + ny * half],
1201 [a1[0] - nx * half, a1[1] - ny * half],
1202 [a0[0] - nx * half, a0[1] - ny * half],
1203 ];
1204 for pos in corners {
1205 rect_batch.push(RectVertex {
1206 position: pixel_to_ndc(
1207 pos,
1208 viewport_width,
1209 viewport_height,
1210 ),
1211 color,
1212 });
1213 }
1214 }
1215 teksilo_canvas::DrawCommand::Shape(idx) => {
1216 flush_all!(
1217 pass,
1218 &self.queue,
1219 self.streams,
1220 &self.rect_pipeline,
1221 &self.sdf_pipeline,
1222 &self.quad_pipeline,
1223 &self.path_gradient_pipeline,
1224 &self.shadow_pipeline,
1225 rect_batch,
1226 sdf_batch,
1227 quad_batch,
1228 path_gradient_batch,
1229 shadow_batch,
1230 self.atlas_texture,
1231 self.path_atlas_texture,
1232 quad_source,
1233 index_binding
1234 );
1235 quad_source = None;
1236 let Some(shape) = frame.shapes.get(*idx) else {
1237 continue;
1238 };
1239 let verts = if shape.stroke_space
1248 == teksilo_canvas::StrokeSpace::Device
1249 && shape.stroke_width > 0.0
1250 {
1251 let zoom = current_transform.m[0].hypot(current_transform.m[1]);
1256 SdfVertex::from_shape_quad_cosmetic(shape, scale_factor, zoom)
1257 } else {
1258 SdfVertex::from_shape_quad(shape, scale_factor)
1259 };
1260 for v in &verts {
1261 let tp = apply_transform_pixel(v.position, ¤t_transform);
1262 sdf_batch.push(SdfVertex {
1263 position: pixel_to_ndc(tp, viewport_width, viewport_height),
1264 color: [
1265 v.color[0],
1266 v.color[1],
1267 v.color[2],
1268 v.color[3] * current_opacity,
1269 ],
1270 ..*v
1271 });
1272 }
1273 }
1274 teksilo_canvas::DrawCommand::Glyph(idx) => {
1275 if quad_source != Some(QuadSource::GlyphAtlas) {
1278 flush_all!(
1279 pass,
1280 &self.queue,
1281 self.streams,
1282 &self.rect_pipeline,
1283 &self.sdf_pipeline,
1284 &self.quad_pipeline,
1285 &self.path_gradient_pipeline,
1286 &self.shadow_pipeline,
1287 rect_batch,
1288 sdf_batch,
1289 quad_batch,
1290 path_gradient_batch,
1291 shadow_batch,
1292 self.atlas_texture,
1293 self.path_atlas_texture,
1294 quad_source,
1295 index_binding
1296 );
1297 quad_source = Some(QuadSource::GlyphAtlas);
1298 }
1299 if let Some(atlas) = &self.atlas_texture {
1300 let Some(glyph) = frame.glyphs.get(*idx) else {
1301 continue;
1302 };
1303 let verts = QuadVertex::from_glyph_quad_transformed(
1306 glyph,
1307 scale_factor,
1308 atlas.width,
1309 atlas.height,
1310 ¤t_transform,
1311 );
1312 for v in &verts {
1313 quad_batch.push(QuadVertex {
1314 position: pixel_to_ndc(
1315 v.position,
1316 viewport_width,
1317 viewport_height,
1318 ),
1319 color: [
1320 v.color[0],
1321 v.color[1],
1322 v.color[2],
1323 v.color[3] * current_opacity,
1324 ],
1325 ..*v
1326 });
1327 }
1328 }
1329 }
1330 teksilo_canvas::DrawCommand::Shadow(idx) => {
1331 flush_all!(
1332 pass,
1333 &self.queue,
1334 self.streams,
1335 &self.rect_pipeline,
1336 &self.sdf_pipeline,
1337 &self.quad_pipeline,
1338 &self.path_gradient_pipeline,
1339 &self.shadow_pipeline,
1340 rect_batch,
1341 sdf_batch,
1342 quad_batch,
1343 path_gradient_batch,
1344 shadow_batch,
1345 self.atlas_texture,
1346 self.path_atlas_texture,
1347 quad_source,
1348 index_binding
1349 );
1350 quad_source = None;
1351 let Some(shadow) = frame.shadows.get(*idx) else {
1352 continue;
1353 };
1354 let verts = ShadowVertex::from_shadow_quad(shadow, scale_factor);
1355 for v in &verts {
1356 let tp = apply_transform_pixel(v.position, ¤t_transform);
1357 shadow_batch.push(ShadowVertex {
1358 position: pixel_to_ndc(tp, viewport_width, viewport_height),
1359 shadow_color: [
1360 v.shadow_color[0],
1361 v.shadow_color[1],
1362 v.shadow_color[2],
1363 v.shadow_color[3] * current_opacity,
1364 ],
1365 ..*v
1366 });
1367 }
1368 }
1369 teksilo_canvas::DrawCommand::Image(idx) => {
1370 flush_all!(
1372 pass,
1373 &self.queue,
1374 self.streams,
1375 &self.rect_pipeline,
1376 &self.sdf_pipeline,
1377 &self.quad_pipeline,
1378 &self.path_gradient_pipeline,
1379 &self.shadow_pipeline,
1380 rect_batch,
1381 sdf_batch,
1382 quad_batch,
1383 path_gradient_batch,
1384 shadow_batch,
1385 self.atlas_texture,
1386 self.path_atlas_texture,
1387 quad_source,
1388 index_binding
1389 );
1390 quad_source = None;
1391 let Some(image) = frame.images.get(*idx) else {
1392 continue;
1393 };
1394 self.draw_image(
1395 pass,
1396 image,
1397 scale_factor,
1398 viewport_width,
1399 viewport_height,
1400 current_opacity,
1401 ¤t_transform,
1402 index_binding,
1403 );
1404 }
1405 teksilo_canvas::DrawCommand::Path(idx) => {
1406 flush_all!(
1407 pass,
1408 &self.queue,
1409 self.streams,
1410 &self.rect_pipeline,
1411 &self.sdf_pipeline,
1412 &self.quad_pipeline,
1413 &self.path_gradient_pipeline,
1414 &self.shadow_pipeline,
1415 rect_batch,
1416 sdf_batch,
1417 quad_batch,
1418 path_gradient_batch,
1419 shadow_batch,
1420 self.atlas_texture,
1421 self.path_atlas_texture,
1422 quad_source,
1423 index_binding
1424 );
1425 quad_source = None;
1426 if let Some(Some(placement)) = path_placements.get(*idx) {
1427 let Some(entry) = frame.paths.get(*idx) else {
1428 continue;
1429 };
1430 let Some(path_atlas) = self.path_atlas_texture.as_ref() else {
1431 continue;
1432 };
1433 if matches!(entry.paint_data, teksilo_canvas::PaintData::Solid)
1434 {
1435 quad_source = Some(QuadSource::PathAtlas);
1443 let verts = path_quad_verts(
1444 entry,
1445 placement,
1446 path_atlas.width,
1447 path_atlas.height,
1448 current_opacity,
1449 ¤t_transform,
1450 );
1451 for v in &verts {
1452 quad_batch.push(QuadVertex {
1453 position: pixel_to_ndc(
1454 v.position,
1455 viewport_width,
1456 viewport_height,
1457 ),
1458 ..*v
1459 });
1460 }
1461 } else {
1462 let verts = path_gradient_quad_verts(
1469 entry,
1470 placement,
1471 scale_factor,
1472 path_atlas.width,
1473 path_atlas.height,
1474 current_opacity,
1475 ¤t_transform,
1476 );
1477 for v in &verts {
1478 path_gradient_batch.push(
1479 crate::vertex::PathGradientVertex {
1480 position: pixel_to_ndc(
1481 v.position,
1482 viewport_width,
1483 viewport_height,
1484 ),
1485 ..*v
1486 },
1487 );
1488 }
1489 }
1490 }
1491 }
1492 teksilo_canvas::DrawCommand::SetClip(rect) => {
1494 flush_all!(
1495 pass,
1496 &self.queue,
1497 self.streams,
1498 &self.rect_pipeline,
1499 &self.sdf_pipeline,
1500 &self.quad_pipeline,
1501 &self.path_gradient_pipeline,
1502 &self.shadow_pipeline,
1503 rect_batch,
1504 sdf_batch,
1505 quad_batch,
1506 path_gradient_batch,
1507 shadow_batch,
1508 self.atlas_texture,
1509 self.path_atlas_texture,
1510 quad_source,
1511 index_binding
1512 );
1513 quad_source = None;
1514 let device = |x: f32, y: f32| {
1540 apply_transform_pixel(
1541 [x * scale_factor, y * scale_factor],
1542 ¤t_transform,
1543 )
1544 };
1545 let p_tl = device(rect.x, rect.y);
1546 let p_tr = device(rect.x + rect.width, rect.y);
1547 let p_bl = device(rect.x, rect.y + rect.height);
1548 let p_br = device(rect.x + rect.width, rect.y + rect.height);
1549 let min_x = p_tl[0].min(p_tr[0]).min(p_bl[0]).min(p_br[0]);
1550 let min_y = p_tl[1].min(p_tr[1]).min(p_bl[1]).min(p_br[1]);
1551 let max_x = p_tl[0].max(p_tr[0]).max(p_bl[0]).max(p_br[0]);
1552 let max_y = p_tl[1].max(p_tr[1]).max(p_bl[1]).max(p_br[1]);
1553 let x0 = min_x.floor().max(0.0);
1558 let y0 = min_y.floor().max(0.0);
1559 let x1 = max_x.ceil().max(x0);
1560 let y1 = max_y.ceil().max(y0);
1561 let x = x0 as u32;
1562 let y = y0 as u32;
1563 let w = (x1 - x0) as u32;
1564 let h = (y1 - y0) as u32;
1565 let x = x.min(viewport_width);
1567 let y = y.min(viewport_height);
1568 let w = w.min(viewport_width.saturating_sub(x));
1569 let h = h.min(viewport_height.saturating_sub(y));
1570 let clip_stack = &mut target_stack
1571 .last_mut()
1572 .expect("target_stack always has the surface target")
1573 .clip_stack;
1574 let clipped = if let Some(&[cx, cy, cw, ch]) = clip_stack.last() {
1575 let ix = x.max(cx);
1576 let iy = y.max(cy);
1577 let ir = (x + w).min(cx + cw);
1578 let ib = (y + h).min(cy + ch);
1579 [ix, iy, ir.saturating_sub(ix), ib.saturating_sub(iy)]
1580 } else {
1581 [x, y, w, h]
1582 };
1583 clip_stack.push(clipped);
1584 pass.set_scissor_rect(
1585 clipped[0], clipped[1], clipped[2], clipped[3],
1586 );
1587 }
1588 teksilo_canvas::DrawCommand::ClearClip => {
1589 flush_all!(
1590 pass,
1591 &self.queue,
1592 self.streams,
1593 &self.rect_pipeline,
1594 &self.sdf_pipeline,
1595 &self.quad_pipeline,
1596 &self.path_gradient_pipeline,
1597 &self.shadow_pipeline,
1598 rect_batch,
1599 sdf_batch,
1600 quad_batch,
1601 path_gradient_batch,
1602 shadow_batch,
1603 self.atlas_texture,
1604 self.path_atlas_texture,
1605 quad_source,
1606 index_binding
1607 );
1608 quad_source = None;
1609 let clip_stack = &mut target_stack
1610 .last_mut()
1611 .expect("target_stack always has the surface target")
1612 .clip_stack;
1613 clip_stack.pop();
1614 if let Some(&[x, y, w, h]) = clip_stack.last() {
1615 pass.set_scissor_rect(x, y, w, h);
1616 } else {
1617 pass.set_scissor_rect(0, 0, viewport_width, viewport_height);
1618 }
1619 }
1620 teksilo_canvas::DrawCommand::SetOpacity(opacity) => {
1621 flush_all!(
1622 pass,
1623 &self.queue,
1624 self.streams,
1625 &self.rect_pipeline,
1626 &self.sdf_pipeline,
1627 &self.quad_pipeline,
1628 &self.path_gradient_pipeline,
1629 &self.shadow_pipeline,
1630 rect_batch,
1631 sdf_batch,
1632 quad_batch,
1633 path_gradient_batch,
1634 shadow_batch,
1635 self.atlas_texture,
1636 self.path_atlas_texture,
1637 quad_source,
1638 index_binding
1639 );
1640 quad_source = None;
1641 opacity_stack.push(current_opacity);
1642 current_opacity *= opacity;
1643 }
1644 teksilo_canvas::DrawCommand::RestoreOpacity => {
1645 flush_all!(
1646 pass,
1647 &self.queue,
1648 self.streams,
1649 &self.rect_pipeline,
1650 &self.sdf_pipeline,
1651 &self.quad_pipeline,
1652 &self.path_gradient_pipeline,
1653 &self.shadow_pipeline,
1654 rect_batch,
1655 sdf_batch,
1656 quad_batch,
1657 path_gradient_batch,
1658 shadow_batch,
1659 self.atlas_texture,
1660 self.path_atlas_texture,
1661 quad_source,
1662 index_binding
1663 );
1664 quad_source = None;
1665 current_opacity = opacity_stack.pop().unwrap_or(1.0);
1666 }
1667 teksilo_canvas::DrawCommand::Rasterized(_) => {}
1668 teksilo_canvas::DrawCommand::LiveImage(idx) => {
1669 flush_all!(
1672 pass,
1673 &self.queue,
1674 self.streams,
1675 &self.rect_pipeline,
1676 &self.sdf_pipeline,
1677 &self.quad_pipeline,
1678 &self.path_gradient_pipeline,
1679 &self.shadow_pipeline,
1680 rect_batch,
1681 sdf_batch,
1682 quad_batch,
1683 path_gradient_batch,
1684 shadow_batch,
1685 self.atlas_texture,
1686 self.path_atlas_texture,
1687 quad_source,
1688 index_binding
1689 );
1690 quad_source = None;
1691 self.draw_live_image(
1692 pass,
1693 frame,
1694 *idx,
1695 scale_factor,
1696 viewport_width,
1697 viewport_height,
1698 current_opacity,
1699 ¤t_transform,
1700 index_binding,
1701 );
1702 }
1703 teksilo_canvas::DrawCommand::AnimatedQuad(idx) => {
1704 let Some(draw) = frame.animated_quads.get(*idx) else {
1705 continue;
1706 };
1707 flush_all!(
1710 pass,
1711 &self.queue,
1712 self.streams,
1713 &self.rect_pipeline,
1714 &self.sdf_pipeline,
1715 &self.quad_pipeline,
1716 &self.path_gradient_pipeline,
1717 &self.shadow_pipeline,
1718 rect_batch,
1719 sdf_batch,
1720 quad_batch,
1721 path_gradient_batch,
1722 shadow_batch,
1723 self.atlas_texture,
1724 self.path_atlas_texture,
1725 quad_source,
1726 index_binding
1727 );
1728 quad_source = None;
1729 match &draw.class {
1730 teksilo_canvas::AnimatedQuadClass::Procedural => {
1731 let verts =
1732 AnimQuadVertex::from_animated_quad(draw, scale_factor);
1733 for v in &verts {
1734 let tp = apply_transform_pixel(
1735 v.position,
1736 ¤t_transform,
1737 );
1738 anim_proc_batch.push(AnimQuadVertex {
1739 position: pixel_to_ndc(
1740 tp,
1741 viewport_width,
1742 viewport_height,
1743 ),
1744 uv: v.uv,
1745 slot: v.slot,
1746 _pad: v._pad,
1747 });
1748 }
1749 }
1750 teksilo_canvas::AnimatedQuadClass::Sprite { image_name } => {
1751 let Some(atlas_bg) =
1757 self.image_manager.get_bind_group(image_name)
1758 else {
1759 continue;
1760 };
1761 let verts =
1762 AnimQuadVertex::from_animated_quad(draw, scale_factor);
1763 let mut ndc_verts = [AnimQuadVertex {
1764 position: [0.0; 2],
1765 uv: [0.0; 2],
1766 slot: 0,
1767 _pad: 0,
1768 };
1769 4];
1770 for (i, v) in verts.iter().enumerate() {
1771 let tp = apply_transform_pixel(
1772 v.position,
1773 ¤t_transform,
1774 );
1775 ndc_verts[i] = AnimQuadVertex {
1776 position: pixel_to_ndc(
1777 tp,
1778 viewport_width,
1779 viewport_height,
1780 ),
1781 uv: v.uv,
1782 slot: v.slot,
1783 _pad: v._pad,
1784 };
1785 }
1786 let bytes: &[u8] = bytemuck::cast_slice(&ndc_verts);
1787 if let (Some((vb, v_off, v_len)), Some((ib, _, _))) = (
1788 self.streams.anim_proc.write(&self.queue, bytes),
1789 index_binding,
1790 ) {
1791 let index_bytes: u64 = 6 * 4;
1792 pass.set_pipeline(&self.anim_sprite_pipeline);
1793 pass.set_bind_group(
1794 0,
1795 &self.anim_uniform_bind_group,
1796 &[],
1797 );
1798 pass.set_bind_group(1, atlas_bg, &[]);
1799 pass.set_vertex_buffer(
1800 0,
1801 vb.slice(v_off..v_off + v_len),
1802 );
1803 pass.set_index_buffer(
1804 ib.slice(0..index_bytes),
1805 wgpu::IndexFormat::Uint32,
1806 );
1807 pass.draw_indexed(0..6, 0, 0..1);
1808 }
1809 }
1810 }
1811 }
1812 teksilo_canvas::DrawCommand::SetBlendMode(mode) => {
1813 blend_stack.push(current_blend);
1814 current_blend = *mode;
1815 }
1816 teksilo_canvas::DrawCommand::RestoreBlendMode => {
1817 current_blend = blend_stack
1818 .pop()
1819 .unwrap_or(teksilo_canvas::BlendMode::Normal);
1820 }
1821 teksilo_canvas::DrawCommand::SetTransform(t) => {
1822 flush_all!(
1823 pass,
1824 &self.queue,
1825 self.streams,
1826 &self.rect_pipeline,
1827 &self.sdf_pipeline,
1828 &self.quad_pipeline,
1829 &self.path_gradient_pipeline,
1830 &self.shadow_pipeline,
1831 rect_batch,
1832 sdf_batch,
1833 quad_batch,
1834 path_gradient_batch,
1835 shadow_batch,
1836 self.atlas_texture,
1837 self.path_atlas_texture,
1838 quad_source,
1839 index_binding
1840 );
1841 quad_source = None;
1842 let device_t = Transform2D {
1848 m: [
1849 t.m[0],
1850 t.m[1],
1851 t.m[2],
1852 t.m[3],
1853 t.m[4] * scale_factor,
1854 t.m[5] * scale_factor,
1855 ],
1856 };
1857 let stack_top = transform_stack
1864 .last()
1865 .copied()
1866 .unwrap_or(Transform2D::IDENTITY);
1867 current_transform = device_t.then(&stack_top);
1868 }
1869 teksilo_canvas::DrawCommand::PushTransform(t) => {
1870 flush_all!(
1871 pass,
1872 &self.queue,
1873 self.streams,
1874 &self.rect_pipeline,
1875 &self.sdf_pipeline,
1876 &self.quad_pipeline,
1877 &self.path_gradient_pipeline,
1878 &self.shadow_pipeline,
1879 rect_batch,
1880 sdf_batch,
1881 quad_batch,
1882 path_gradient_batch,
1883 shadow_batch,
1884 self.atlas_texture,
1885 self.path_atlas_texture,
1886 quad_source,
1887 index_binding
1888 );
1889 quad_source = None;
1890 let device_t = Transform2D {
1893 m: [
1894 t.m[0],
1895 t.m[1],
1896 t.m[2],
1897 t.m[3],
1898 t.m[4] * scale_factor,
1899 t.m[5] * scale_factor,
1900 ],
1901 };
1902 let prev_top = transform_stack
1903 .last()
1904 .copied()
1905 .unwrap_or(Transform2D::IDENTITY);
1906 let new_top = device_t.then(&prev_top);
1907 transform_stack.push(new_top);
1908 current_transform = new_top;
1909 }
1910 teksilo_canvas::DrawCommand::PopTransform => {
1911 flush_all!(
1912 pass,
1913 &self.queue,
1914 self.streams,
1915 &self.rect_pipeline,
1916 &self.sdf_pipeline,
1917 &self.quad_pipeline,
1918 &self.path_gradient_pipeline,
1919 &self.shadow_pipeline,
1920 rect_batch,
1921 sdf_batch,
1922 quad_batch,
1923 path_gradient_batch,
1924 shadow_batch,
1925 self.atlas_texture,
1926 self.path_atlas_texture,
1927 quad_source,
1928 index_binding
1929 );
1930 quad_source = None;
1931 if transform_stack.len() > 1 {
1932 transform_stack.pop();
1933 }
1934 current_transform = transform_stack
1935 .last()
1936 .copied()
1937 .unwrap_or(Transform2D::IDENTITY);
1938 }
1939 teksilo_canvas::DrawCommand::BeginBlurredSubtree { .. }
1940 | teksilo_canvas::DrawCommand::EndBlurredSubtree => {
1941 unreachable!("blur boundaries are handled at the segment level");
1944 }
1945 }
1946 cmd_idx += 1;
1947 }
1948
1949 flush_all!(
1951 pass,
1952 &self.queue,
1953 self.streams,
1954 &self.rect_pipeline,
1955 &self.sdf_pipeline,
1956 &self.quad_pipeline,
1957 &self.path_gradient_pipeline,
1958 &self.shadow_pipeline,
1959 rect_batch,
1960 sdf_batch,
1961 quad_batch,
1962 path_gradient_batch,
1963 shadow_batch,
1964 self.atlas_texture,
1965 self.path_atlas_texture,
1966 quad_source,
1967 index_binding
1968 );
1969 quad_source = None;
1970 } if cmd_idx >= frame.draw_order.len() {
1974 break;
1975 }
1976 match &frame.draw_order[cmd_idx] {
1977 teksilo_canvas::DrawCommand::BeginBlurredSubtree { bounds, radius } => {
1978 let device_w = (bounds.width * scale_factor).ceil().max(1.0) as u32;
1980 let device_h = (bounds.height * scale_factor).ceil().max(1.0) as u32;
1981 let intermediate = self.blur_pool.acquire(&self.device, device_w, device_h);
1982 let (bucket_w, bucket_h) = self.blur_pool.dimensions(intermediate);
1983
1984 let translate = Transform2D {
1989 m: [
1990 1.0,
1991 0.0,
1992 0.0,
1993 1.0,
1994 -bounds.x * scale_factor,
1995 -bounds.y * scale_factor,
1996 ],
1997 };
1998 let prev_top = transform_stack
1999 .last()
2000 .copied()
2001 .unwrap_or(Transform2D::IDENTITY);
2002 let new_top = translate.then(&prev_top);
2003 transform_stack.push(new_top);
2004 current_transform = new_top;
2005
2006 target_stack.push(ActiveTarget {
2007 intermediate: Some(intermediate),
2008 viewport_w: bucket_w,
2009 viewport_h: bucket_h,
2010 opened: false,
2011 pending_composites: Vec::new(),
2012 clip_stack: Vec::new(),
2013 blur_bounds: Some(*bounds),
2014 blur_radius_logical: Some(*radius),
2015 used_w: Some(device_w),
2016 used_h: Some(device_h),
2017 bucket_w: Some(bucket_w),
2018 bucket_h: Some(bucket_h),
2019 });
2020 }
2021 teksilo_canvas::DrawCommand::EndBlurredSubtree => {
2022 let scope = target_stack
2023 .pop()
2024 .expect("EndBlurredSubtree without matching Begin");
2025 debug_assert!(
2026 scope.intermediate.is_some(),
2027 "End popped the surface (impossible if walker is balanced)"
2028 );
2029 let intermediate = scope
2030 .intermediate
2031 .expect("blur scope intermediate set in BeginBlurredSubtree");
2032 let bounds = scope
2033 .blur_bounds
2034 .expect("blur scope bounds set in BeginBlurredSubtree");
2035 let radius = scope
2036 .blur_radius_logical
2037 .expect("blur scope radius set in BeginBlurredSubtree");
2038 let used_w = scope
2039 .used_w
2040 .expect("blur scope used_w set in BeginBlurredSubtree");
2041 let used_h = scope
2042 .used_h
2043 .expect("blur scope used_h set in BeginBlurredSubtree");
2044 let bucket_w = scope
2045 .bucket_w
2046 .expect("blur scope bucket_w set in BeginBlurredSubtree");
2047 let bucket_h = scope
2048 .bucket_h
2049 .expect("blur scope bucket_h set in BeginBlurredSubtree");
2050
2051 if transform_stack.len() > 1 {
2053 transform_stack.pop();
2054 }
2055 current_transform = transform_stack
2056 .last()
2057 .copied()
2058 .unwrap_or(Transform2D::IDENTITY);
2059
2060 let blurred = run_kawase_chain(
2064 &self.device,
2065 &self.queue,
2066 &mut encoder,
2067 &mut self.blur_pool,
2068 &mut self.blur_pipelines,
2069 intermediate,
2070 used_w,
2071 used_h,
2072 bucket_w,
2073 bucket_h,
2074 radius * scale_factor,
2075 );
2076
2077 target_stack
2080 .last_mut()
2081 .expect("target_stack always has the surface target")
2082 .pending_composites
2083 .push(PendingComposite {
2084 blurred_texture: blurred.texture,
2085 used_w: blurred.used_w,
2086 used_h: blurred.used_h,
2087 bucket_w: blurred.bucket_w,
2088 bucket_h: blurred.bucket_h,
2089 bounds,
2090 });
2091 }
2092 _ => unreachable!("inner loop only breaks on Begin/End"),
2093 }
2094 cmd_idx += 1;
2095 }
2096
2097 debug_assert!(
2098 target_stack.len() == 1,
2099 "target_stack not balanced at EOF — unmatched Begin/End in walker output"
2100 );
2101 let final_composites = std::mem::take(
2105 &mut target_stack
2106 .last_mut()
2107 .expect("target_stack always has the surface target")
2108 .pending_composites,
2109 );
2110 if !final_composites.is_empty() {
2111 let surface = target_stack
2112 .last_mut()
2113 .expect("target_stack always has the surface target");
2114 let load_op = if surface.opened {
2115 wgpu::LoadOp::Load
2116 } else {
2117 wgpu::LoadOp::Clear(surface_clear_color)
2118 };
2119 surface.opened = true;
2120 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2121 label: Some("teksilo_final_composite_pass"),
2122 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2123 view,
2124 resolve_target: None,
2125 ops: wgpu::Operations {
2126 load: load_op,
2127 store: wgpu::StoreOp::Store,
2128 },
2129 depth_slice: None,
2130 })],
2131 depth_stencil_attachment: None,
2132 timestamp_writes: None,
2133 occlusion_query_set: None,
2134 multiview_mask: None,
2135 });
2136 for pc in &final_composites {
2137 composite_blur_quad(
2138 &self.device,
2139 &self.queue,
2140 &mut pass,
2141 &self.blur_pool,
2142 &self.quad_pipeline,
2143 &self.quad_bind_group_layout,
2144 &self.blur_composite_sampler,
2145 &self.streams.quad,
2146 index_binding,
2147 pc.blurred_texture,
2148 pc.used_w,
2149 pc.used_h,
2150 pc.bucket_w,
2151 pc.bucket_h,
2152 pc.bounds,
2153 scale_factor,
2154 viewport_width,
2155 viewport_height,
2156 );
2157 }
2158 } else if !target_stack
2159 .last()
2160 .expect("target_stack always has the surface target")
2161 .opened
2162 {
2163 let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2165 label: Some("teksilo_empty_clear_pass"),
2166 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2167 view,
2168 resolve_target: None,
2169 ops: wgpu::Operations {
2170 load: wgpu::LoadOp::Clear(surface_clear_color),
2171 store: wgpu::StoreOp::Store,
2172 },
2173 depth_slice: None,
2174 })],
2175 depth_stencil_attachment: None,
2176 timestamp_writes: None,
2177 occlusion_query_set: None,
2178 multiview_mask: None,
2179 });
2180 }
2181 }
2182
2183 let submission = self.queue.submit(std::iter::once(encoder.finish()));
2184 let idle = self.live.stats().textures == 0;
2187 self.live.backend_mut().after_submit(idle);
2188 if self.live.backend_mut().take_released() {
2189 crate::gpu_reclaim::flag_device(
2190 self.live.backend().health(),
2191 &self.device,
2192 submission.clone(),
2193 );
2194 }
2195 self.last_submission = Some(submission);
2196 }
2197
2198 #[allow(clippy::too_many_arguments)]
2206 fn draw_live_image(
2207 &self,
2208 pass: &mut wgpu::RenderPass,
2209 frame: &RenderFrame,
2210 idx: usize,
2211 scale_factor: f32,
2212 viewport_width: u32,
2213 viewport_height: u32,
2214 opacity: f32,
2215 transform: &Transform2D,
2216 index_binding: Option<(&wgpu::Buffer, u64, u64)>,
2217 ) {
2218 let (Some(quad), Some(decision)) =
2219 (frame.live_images.get(idx), self.live.decisions().get(idx))
2220 else {
2221 return;
2222 };
2223 if !decision.draw {
2224 return;
2225 }
2226 let Some(texture) = self.live.texture(quad.consumer.source().id()) else {
2227 return;
2228 };
2229 let Some(bind_group) = texture.bind_group(quad.filter) else {
2230 return;
2231 };
2232 let format = quad.consumer.source().format();
2233 let mut flags = crate::vertex::QUAD_FLAG_COLOR_GLYPH;
2234 if format.is_opaque() {
2235 flags |= crate::vertex::QUAD_FLAG_OPAQUE;
2236 }
2237 if format.is_bgr() {
2238 flags |= crate::vertex::QUAD_FLAG_SWAP_RB;
2239 }
2240 let verts = crate::vertex::live_quad_verts(
2241 quad,
2242 texture.size(),
2243 scale_factor,
2244 transform,
2245 [1.0, 1.0, 1.0, opacity],
2246 flags,
2247 );
2248 let ndc: [QuadVertex; 4] = std::array::from_fn(|i| QuadVertex {
2249 position: pixel_to_ndc(verts[i].position, viewport_width, viewport_height),
2250 ..verts[i]
2251 });
2252 let bytes: &[u8] = bytemuck::cast_slice(&ndc);
2253 let Some((vb, v_off, v_len)) = self.streams.quad.write(&self.queue, bytes) else {
2254 return;
2255 };
2256 let Some((ib, _, _)) = index_binding else {
2257 return;
2258 };
2259 pass.set_pipeline(&self.quad_pipeline);
2260 pass.set_bind_group(0, bind_group, &[]);
2261 pass.set_vertex_buffer(0, vb.slice(v_off..v_off + v_len));
2262 pass.set_index_buffer(ib.slice(0..24), wgpu::IndexFormat::Uint32);
2263 pass.draw_indexed(0..6, 0, 0..1);
2264 }
2265
2266 #[allow(clippy::too_many_arguments)]
2270 fn draw_image(
2271 &self,
2272 pass: &mut wgpu::RenderPass,
2273 image: &teksilo_canvas::ImageQuad,
2274 scale_factor: f32,
2275 viewport_width: u32,
2276 viewport_height: u32,
2277 opacity: f32,
2278 transform: &Transform2D,
2279 index_binding: Option<(&wgpu::Buffer, u64, u64)>,
2280 ) {
2281 let bind_group = match self.image_manager.get_bind_group(&image.name) {
2282 Some(bg) => bg,
2283 None => return,
2284 };
2285
2286 let [x, y, w, h] = image.screen;
2287 let sx = x * scale_factor;
2288 let sy = y * scale_factor;
2289 let sw = w * scale_factor;
2290 let sh = h * scale_factor;
2291
2292 let (color, flags) = if let Some(tint) = image.tint {
2295 (
2298 crate::vertex::srgb_to_linear_rgba([tint[0], tint[1], tint[2], tint[3] * opacity]),
2299 0,
2300 )
2301 } else {
2302 (
2303 [1.0, 1.0, 1.0, opacity],
2304 crate::vertex::QUAD_FLAG_COLOR_GLYPH,
2305 )
2306 };
2307
2308 let verts = [
2309 QuadVertex {
2310 position: [sx, sy],
2311 tex_coord: [0.0, 0.0],
2312 color,
2313 flags,
2314 _pad: 0,
2315 },
2316 QuadVertex {
2317 position: [sx + sw, sy],
2318 tex_coord: [1.0, 0.0],
2319 color,
2320 flags,
2321 _pad: 0,
2322 },
2323 QuadVertex {
2324 position: [sx + sw, sy + sh],
2325 tex_coord: [1.0, 1.0],
2326 color,
2327 flags,
2328 _pad: 0,
2329 },
2330 QuadVertex {
2331 position: [sx, sy + sh],
2332 tex_coord: [0.0, 1.0],
2333 color,
2334 flags,
2335 _pad: 0,
2336 },
2337 ];
2338
2339 let ndc_verts: [QuadVertex; 4] = std::array::from_fn(|i| {
2340 let v = verts[i];
2341 let tp = apply_transform_pixel(v.position, transform);
2342 QuadVertex {
2343 position: pixel_to_ndc(tp, viewport_width, viewport_height),
2344 ..v
2345 }
2346 });
2347
2348 let bytes: &[u8] = bytemuck::cast_slice(&ndc_verts);
2352 let Some((vb, v_off, v_len)) = self.streams.quad.write(&self.queue, bytes) else {
2353 return;
2354 };
2355 let Some((ib, _, _)) = index_binding else {
2356 return;
2357 };
2358
2359 pass.set_pipeline(&self.quad_pipeline);
2360 pass.set_bind_group(0, bind_group, &[]);
2361 pass.set_vertex_buffer(0, vb.slice(v_off..v_off + v_len));
2362 pass.set_index_buffer(ib.slice(0..24), wgpu::IndexFormat::Uint32);
2363 pass.draw_indexed(0..6, 0, 0..1);
2364 }
2365
2366 fn upload_path_atlas(&mut self, width: u32, height: u32, pixels: Vec<u8>) {
2368 if width == 0 || height == 0 {
2369 return;
2370 }
2371
2372 let needs_recreate = self
2373 .path_atlas_texture
2374 .as_ref()
2375 .is_none_or(|t| t.width != width || t.height != height);
2376
2377 if needs_recreate {
2378 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
2379 label: Some("path_atlas"),
2380 size: wgpu::Extent3d {
2381 width,
2382 height,
2383 depth_or_array_layers: 1,
2384 },
2385 mip_level_count: 1,
2386 sample_count: 1,
2387 dimension: wgpu::TextureDimension::D2,
2388 format: wgpu::TextureFormat::Rgba8UnormSrgb,
2389 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2390 view_formats: &[],
2391 });
2392
2393 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
2394 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
2395 mag_filter: wgpu::FilterMode::Linear,
2396 min_filter: wgpu::FilterMode::Linear,
2397 ..Default::default()
2398 });
2399
2400 let bind_group_layout = self.quad_pipeline.get_bind_group_layout(0);
2401 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2402 label: Some("path_atlas_bind_group"),
2403 layout: &bind_group_layout,
2404 entries: &[
2405 wgpu::BindGroupEntry {
2406 binding: 0,
2407 resource: wgpu::BindingResource::TextureView(&view),
2408 },
2409 wgpu::BindGroupEntry {
2410 binding: 1,
2411 resource: wgpu::BindingResource::Sampler(&sampler),
2412 },
2413 ],
2414 });
2415
2416 self.path_atlas_texture = Some(AtlasTexture {
2417 texture,
2418 bind_group,
2419 width,
2420 height,
2421 });
2422 }
2423
2424 if let Some(atlas) = &self.path_atlas_texture {
2425 self.queue.write_texture(
2426 wgpu::TexelCopyTextureInfo {
2427 texture: &atlas.texture,
2428 mip_level: 0,
2429 origin: wgpu::Origin3d::ZERO,
2430 aspect: wgpu::TextureAspect::All,
2431 },
2432 &pixels,
2433 wgpu::TexelCopyBufferLayout {
2434 offset: 0,
2435 bytes_per_row: Some(width * 4),
2436 rows_per_image: Some(height),
2437 },
2438 wgpu::Extent3d {
2439 width,
2440 height,
2441 depth_or_array_layers: 1,
2442 },
2443 );
2444 }
2445 }
2446
2447 pub fn device(&self) -> &wgpu::Device {
2448 &self.device
2449 }
2450
2451 pub fn queue(&self) -> &wgpu::Queue {
2452 &self.queue
2453 }
2454
2455 pub fn register_image(&mut self, name: &str, width: u32, height: u32, pixels: &[u8]) {
2457 let layout = self.quad_pipeline.get_bind_group_layout(0);
2458 self.image_manager.register_image(
2459 name,
2460 width,
2461 height,
2462 pixels,
2463 &self.device,
2464 &self.queue,
2465 &layout,
2466 );
2467 }
2468
2469 pub fn remove_image(&mut self, name: &str) {
2471 self.image_manager.remove(name);
2472 }
2473}
2474
2475fn path_quad_verts(
2477 entry: &teksilo_canvas::PathEntry,
2478 placement: &crate::path_atlas::PathPlacement,
2479 atlas_width: u32,
2480 atlas_height: u32,
2481 opacity: f32,
2482 transform: &Transform2D,
2483) -> [QuadVertex; 4] {
2484 let region = &placement.region;
2489 let [sx, sy, sw, sh] = placement.device_rect;
2490
2491 let aw = atlas_width.max(1) as f32;
2492 let ah = atlas_height.max(1) as f32;
2493 let u0 = region.x as f32 / aw;
2494 let v0 = region.y as f32 / ah;
2495 let u1 = (region.x + region.w) as f32 / aw;
2496 let v1 = (region.y + region.h) as f32 / ah;
2497
2498 let lin = crate::vertex::srgb_to_linear_rgba(entry.color);
2505 let color = [lin[0], lin[1], lin[2], entry.color[3] * opacity];
2506
2507 let positions = [
2508 apply_transform_pixel([sx, sy], transform),
2509 apply_transform_pixel([sx + sw, sy], transform),
2510 apply_transform_pixel([sx + sw, sy + sh], transform),
2511 apply_transform_pixel([sx, sy + sh], transform),
2512 ];
2513 let uvs = [[u0, v0], [u1, v0], [u1, v1], [u0, v1]];
2514
2515 [
2518 QuadVertex {
2519 position: positions[0],
2520 tex_coord: uvs[0],
2521 color,
2522 flags: 0,
2523 _pad: 0,
2524 },
2525 QuadVertex {
2526 position: positions[1],
2527 tex_coord: uvs[1],
2528 color,
2529 flags: 0,
2530 _pad: 0,
2531 },
2532 QuadVertex {
2533 position: positions[2],
2534 tex_coord: uvs[2],
2535 color,
2536 flags: 0,
2537 _pad: 0,
2538 },
2539 QuadVertex {
2540 position: positions[3],
2541 tex_coord: uvs[3],
2542 color,
2543 flags: 0,
2544 _pad: 0,
2545 },
2546 ]
2547}
2548
2549fn path_gradient_quad_verts(
2558 entry: &teksilo_canvas::PathEntry,
2559 placement: &crate::path_atlas::PathPlacement,
2560 scale_factor: f32,
2561 atlas_width: u32,
2562 atlas_height: u32,
2563 current_opacity: f32,
2564 transform: &Transform2D,
2565) -> [crate::vertex::PathGradientVertex; 4] {
2566 crate::vertex::PathGradientVertex::from_path_entry(
2567 entry,
2568 placement,
2569 scale_factor,
2570 atlas_width,
2571 atlas_height,
2572 current_opacity,
2573 transform,
2574 )
2575}
2576
2577fn pixel_to_ndc(pixel: [f32; 2], viewport_width: u32, viewport_height: u32) -> [f32; 2] {
2578 let x = (pixel[0] / viewport_width as f32) * 2.0 - 1.0;
2579 let y = 1.0 - (pixel[1] / viewport_height as f32) * 2.0; [x, y]
2581}
2582
2583fn apply_transform_pixel(pixel: [f32; 2], transform: &Transform2D) -> [f32; 2] {
2585 let [a, b, c, d, tx, ty] = transform.m;
2586 [
2587 a * pixel[0] + c * pixel[1] + tx,
2588 b * pixel[0] + d * pixel[1] + ty,
2589 ]
2590}
2591
2592struct KawaseResult {
2598 texture: crate::blur::AcquiredTexture,
2599 used_w: u32,
2600 used_h: u32,
2601 bucket_w: u32,
2602 bucket_h: u32,
2603}
2604
2605#[allow(clippy::too_many_arguments)]
2611fn run_kawase_chain(
2612 device: &wgpu::Device,
2613 queue: &wgpu::Queue,
2614 encoder: &mut wgpu::CommandEncoder,
2615 pool: &mut crate::blur::BlurPool,
2616 pipelines: &mut crate::blur::BlurPipelines,
2617 source: crate::blur::AcquiredTexture,
2618 used_w: u32,
2619 used_h: u32,
2620 bucket_w: u32,
2621 bucket_h: u32,
2622 radius_device_px: f32,
2623) -> KawaseResult {
2624 let levels = crate::blur::kawase_levels(radius_device_px);
2625
2626 let mut current = (source, used_w, used_h, bucket_w, bucket_h);
2630
2631 let mut chain: Vec<(crate::blur::AcquiredTexture, u32, u32, u32, u32)> =
2635 Vec::with_capacity(levels as usize + 1);
2636 chain.push(current);
2637
2638 const KERNEL_OFFSET: f32 = 0.5;
2643
2644 for _ in 0..levels {
2646 let (src_handle, src_used_w, src_used_h, src_bucket_w, src_bucket_h) = current;
2647 let dst_used_w = (src_used_w / 2).max(1);
2648 let dst_used_h = (src_used_h / 2).max(1);
2649 let dst = pool.acquire(device, dst_used_w, dst_used_h);
2650 let (dst_bucket_w, dst_bucket_h) = pool.dimensions(dst);
2651
2652 let params = crate::blur::BlurParams {
2654 offset: crate::blur::kawase_offset(src_bucket_w, src_bucket_h, KERNEL_OFFSET),
2655 };
2656 let params_offset = pipelines.params.push(queue, ¶ms);
2657 let bind_group = pool.make_bind_group(device, src_handle, pipelines.params.buffer());
2658
2659 crate::fullscreen::run_fullscreen_pass(
2660 encoder,
2661 &pipelines.down,
2662 &bind_group,
2663 &[params_offset],
2664 pool.view(dst),
2665 dst_used_w,
2666 dst_used_h,
2667 wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2668 None,
2669 "kawase_down_pass",
2670 );
2671
2672 current = (dst, dst_used_w, dst_used_h, dst_bucket_w, dst_bucket_h);
2673 chain.push(current);
2674 }
2675
2676 for level in (0..levels).rev() {
2680 let (src_handle, _src_used_w, _src_used_h, src_bucket_w, src_bucket_h) = current;
2681 let target = chain[level as usize];
2682 let dst_used_w = target.1;
2683 let dst_used_h = target.2;
2684 let dst = pool.acquire(device, dst_used_w, dst_used_h);
2685 let (dst_bucket_w, dst_bucket_h) = pool.dimensions(dst);
2686
2687 let params = crate::blur::BlurParams {
2688 offset: crate::blur::kawase_offset(src_bucket_w, src_bucket_h, KERNEL_OFFSET),
2689 };
2690 let params_offset = pipelines.params.push(queue, ¶ms);
2691 let bind_group = pool.make_bind_group(device, src_handle, pipelines.params.buffer());
2692
2693 crate::fullscreen::run_fullscreen_pass(
2694 encoder,
2695 &pipelines.up,
2696 &bind_group,
2697 &[params_offset],
2698 pool.view(dst),
2699 dst_used_w,
2700 dst_used_h,
2701 wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2702 None,
2703 "kawase_up_pass",
2704 );
2705
2706 current = (dst, dst_used_w, dst_used_h, dst_bucket_w, dst_bucket_h);
2707 }
2708
2709 KawaseResult {
2710 texture: current.0,
2711 used_w: current.1,
2712 used_h: current.2,
2713 bucket_w: current.3,
2714 bucket_h: current.4,
2715 }
2716}
2717
2718#[allow(clippy::too_many_arguments)]
2728fn composite_blur_quad(
2729 device: &wgpu::Device,
2730 queue: &wgpu::Queue,
2731 pass: &mut wgpu::RenderPass<'_>,
2732 pool: &crate::blur::BlurPool,
2733 quad_pipeline: &wgpu::RenderPipeline,
2734 quad_bind_group_layout: &wgpu::BindGroupLayout,
2735 sampler: &wgpu::Sampler,
2736 quad_stream: &crate::stream_buffer::StreamBuffer,
2737 index_binding: Option<(&wgpu::Buffer, u64, u64)>,
2738 blurred: crate::blur::AcquiredTexture,
2739 used_w: u32,
2740 used_h: u32,
2741 bucket_w: u32,
2742 bucket_h: u32,
2743 bounds: teksilo_canvas::Rect,
2744 scale_factor: f32,
2745 viewport_width: u32,
2746 viewport_height: u32,
2747) {
2748 let view = pool.view(blurred);
2749 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2750 label: Some("blur_composite_bind_group"),
2751 layout: quad_bind_group_layout,
2752 entries: &[
2753 wgpu::BindGroupEntry {
2754 binding: 0,
2755 resource: wgpu::BindingResource::TextureView(view),
2756 },
2757 wgpu::BindGroupEntry {
2758 binding: 1,
2759 resource: wgpu::BindingResource::Sampler(sampler),
2760 },
2761 ],
2762 });
2763
2764 let sx = bounds.x * scale_factor;
2766 let sy = bounds.y * scale_factor;
2767 let sw = bounds.width * scale_factor;
2768 let sh = bounds.height * scale_factor;
2769
2770 let u_max = used_w as f32 / bucket_w as f32;
2774 let v_max = used_h as f32 / bucket_h as f32;
2775
2776 let flags = 1u32;
2778 let color = [1.0, 1.0, 1.0, 1.0];
2779
2780 let p_tl = pixel_to_ndc([sx, sy], viewport_width, viewport_height);
2781 let p_tr = pixel_to_ndc([sx + sw, sy], viewport_width, viewport_height);
2782 let p_br = pixel_to_ndc([sx + sw, sy + sh], viewport_width, viewport_height);
2783 let p_bl = pixel_to_ndc([sx, sy + sh], viewport_width, viewport_height);
2784
2785 let verts: [QuadVertex; 4] = [
2786 QuadVertex {
2787 position: p_tl,
2788 tex_coord: [0.0, 0.0],
2789 color,
2790 flags,
2791 _pad: 0,
2792 },
2793 QuadVertex {
2794 position: p_tr,
2795 tex_coord: [u_max, 0.0],
2796 color,
2797 flags,
2798 _pad: 0,
2799 },
2800 QuadVertex {
2801 position: p_br,
2802 tex_coord: [u_max, v_max],
2803 color,
2804 flags,
2805 _pad: 0,
2806 },
2807 QuadVertex {
2808 position: p_bl,
2809 tex_coord: [0.0, v_max],
2810 color,
2811 flags,
2812 _pad: 0,
2813 },
2814 ];
2815
2816 let _ = device; let Some((vb, v_off, v_len)) = quad_stream.write(queue, bytemuck::cast_slice(&verts)) else {
2822 return;
2823 };
2824 let Some((ib, _, _)) = index_binding else {
2825 return;
2826 };
2827 let composite_index_bytes: u64 = 6 * std::mem::size_of::<u32>() as u64;
2828
2829 pass.set_pipeline(quad_pipeline);
2830 pass.set_bind_group(0, &bind_group, &[]);
2831 pass.set_viewport(
2832 0.0,
2833 0.0,
2834 viewport_width as f32,
2835 viewport_height as f32,
2836 0.0,
2837 1.0,
2838 );
2839 pass.set_vertex_buffer(0, vb.slice(v_off..v_off + v_len));
2840 pass.set_index_buffer(
2841 ib.slice(0..composite_index_bytes),
2842 wgpu::IndexFormat::Uint32,
2843 );
2844 pass.draw_indexed(0..6, 0, 0..1);
2845}
2846
2847fn create_rect_pipeline(
2850 device: &wgpu::Device,
2851 format: wgpu::TextureFormat,
2852) -> wgpu::RenderPipeline {
2853 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2854 label: Some("rect_shader"),
2855 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/rect.wgsl").into()),
2856 });
2857
2858 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2859 label: Some("rect_pipeline_layout"),
2860 bind_group_layouts: &[],
2861 immediate_size: 0,
2862 });
2863
2864 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2865 label: Some("rect_pipeline"),
2866 layout: Some(&layout),
2867 vertex: wgpu::VertexState {
2868 module: &shader,
2869 entry_point: Some("vs_main"),
2870 buffers: &[Some(wgpu::VertexBufferLayout {
2871 array_stride: std::mem::size_of::<RectVertex>() as u64,
2872 step_mode: wgpu::VertexStepMode::Vertex,
2873 attributes: &[
2874 wgpu::VertexAttribute {
2875 offset: 0,
2876 shader_location: 0,
2877 format: wgpu::VertexFormat::Float32x2,
2878 },
2879 wgpu::VertexAttribute {
2880 offset: 8,
2881 shader_location: 1,
2882 format: wgpu::VertexFormat::Float32x4,
2883 },
2884 ],
2885 })],
2886 compilation_options: Default::default(),
2887 },
2888 fragment: Some(wgpu::FragmentState {
2889 module: &shader,
2890 entry_point: Some("fs_main"),
2891 targets: &[Some(wgpu::ColorTargetState {
2892 format,
2893 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
2894 write_mask: wgpu::ColorWrites::ALL,
2895 })],
2896 compilation_options: Default::default(),
2897 }),
2898 primitive: wgpu::PrimitiveState {
2899 topology: wgpu::PrimitiveTopology::TriangleList,
2900 ..Default::default()
2901 },
2902 depth_stencil: None,
2903 multisample: wgpu::MultisampleState::default(),
2904 multiview_mask: None,
2905 cache: None,
2906 })
2907}
2908
2909fn create_sdf_pipeline(device: &wgpu::Device, format: wgpu::TextureFormat) -> wgpu::RenderPipeline {
2910 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2911 label: Some("sdf_shader"),
2912 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/sdf.wgsl").into()),
2913 });
2914
2915 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2916 label: Some("sdf_pipeline_layout"),
2917 bind_group_layouts: &[],
2918 immediate_size: 0,
2919 });
2920
2921 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2922 label: Some("sdf_pipeline"),
2923 layout: Some(&layout),
2924 vertex: wgpu::VertexState {
2925 module: &shader,
2926 entry_point: Some("vs_main"),
2927 buffers: &[Some(wgpu::VertexBufferLayout {
2928 array_stride: std::mem::size_of::<SdfVertex>() as u64,
2929 step_mode: wgpu::VertexStepMode::Vertex,
2930 attributes: &[
2931 wgpu::VertexAttribute {
2932 offset: 0,
2933 shader_location: 0,
2934 format: wgpu::VertexFormat::Float32x2, },
2936 wgpu::VertexAttribute {
2937 offset: 8,
2938 shader_location: 1,
2939 format: wgpu::VertexFormat::Float32x2, },
2941 wgpu::VertexAttribute {
2942 offset: 16,
2943 shader_location: 2,
2944 format: wgpu::VertexFormat::Float32x4, },
2946 wgpu::VertexAttribute {
2947 offset: 32,
2948 shader_location: 3,
2949 format: wgpu::VertexFormat::Float32x4, },
2951 wgpu::VertexAttribute {
2952 offset: 48,
2953 shader_location: 4,
2954 format: wgpu::VertexFormat::Float32x4, },
2956 wgpu::VertexAttribute {
2957 offset: 64,
2958 shader_location: 5,
2959 format: wgpu::VertexFormat::Float32x4, },
2961 wgpu::VertexAttribute {
2962 offset: 80,
2963 shader_location: 6,
2964 format: wgpu::VertexFormat::Float32x4, },
2966 wgpu::VertexAttribute {
2967 offset: 96,
2968 shader_location: 7,
2969 format: wgpu::VertexFormat::Float32x4, },
2971 wgpu::VertexAttribute {
2972 offset: 112,
2973 shader_location: 8,
2974 format: wgpu::VertexFormat::Float32x4, },
2976 wgpu::VertexAttribute {
2977 offset: 128,
2978 shader_location: 9,
2979 format: wgpu::VertexFormat::Float32x4, },
2981 wgpu::VertexAttribute {
2982 offset: 144,
2983 shader_location: 10,
2984 format: wgpu::VertexFormat::Float32x4, },
2986 ],
2987 })],
2988 compilation_options: Default::default(),
2989 },
2990 fragment: Some(wgpu::FragmentState {
2991 module: &shader,
2992 entry_point: Some("fs_main"),
2993 targets: &[Some(wgpu::ColorTargetState {
2994 format,
2995 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
2996 write_mask: wgpu::ColorWrites::ALL,
2997 })],
2998 compilation_options: Default::default(),
2999 }),
3000 primitive: wgpu::PrimitiveState {
3001 topology: wgpu::PrimitiveTopology::TriangleList,
3002 ..Default::default()
3003 },
3004 depth_stencil: None,
3005 multisample: wgpu::MultisampleState::default(),
3006 multiview_mask: None,
3007 cache: None,
3008 })
3009}
3010
3011fn create_quad_pipeline(
3012 device: &wgpu::Device,
3013 format: wgpu::TextureFormat,
3014) -> wgpu::RenderPipeline {
3015 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
3016 label: Some("quad_shader"),
3017 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/quad.wgsl").into()),
3018 });
3019
3020 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3021 label: Some("quad_bind_group_layout"),
3022 entries: &[
3023 wgpu::BindGroupLayoutEntry {
3024 binding: 0,
3025 visibility: wgpu::ShaderStages::FRAGMENT,
3026 ty: wgpu::BindingType::Texture {
3027 sample_type: wgpu::TextureSampleType::Float { filterable: true },
3028 view_dimension: wgpu::TextureViewDimension::D2,
3029 multisampled: false,
3030 },
3031 count: None,
3032 },
3033 wgpu::BindGroupLayoutEntry {
3034 binding: 1,
3035 visibility: wgpu::ShaderStages::FRAGMENT,
3036 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
3037 count: None,
3038 },
3039 ],
3040 });
3041
3042 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
3043 label: Some("quad_pipeline_layout"),
3044 bind_group_layouts: &[Some(&bind_group_layout)],
3045 immediate_size: 0,
3046 });
3047
3048 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
3049 label: Some("quad_pipeline"),
3050 layout: Some(&layout),
3051 vertex: wgpu::VertexState {
3052 module: &shader,
3053 entry_point: Some("vs_main"),
3054 buffers: &[Some(wgpu::VertexBufferLayout {
3055 array_stride: std::mem::size_of::<QuadVertex>() as u64,
3056 step_mode: wgpu::VertexStepMode::Vertex,
3057 attributes: &[
3058 wgpu::VertexAttribute {
3059 offset: 0,
3060 shader_location: 0,
3061 format: wgpu::VertexFormat::Float32x2, },
3063 wgpu::VertexAttribute {
3064 offset: 8,
3065 shader_location: 1,
3066 format: wgpu::VertexFormat::Float32x2, },
3068 wgpu::VertexAttribute {
3069 offset: 16,
3070 shader_location: 2,
3071 format: wgpu::VertexFormat::Float32x4, },
3073 wgpu::VertexAttribute {
3074 offset: 32,
3075 shader_location: 3,
3076 format: wgpu::VertexFormat::Uint32, },
3078 ],
3079 })],
3080 compilation_options: Default::default(),
3081 },
3082 fragment: Some(wgpu::FragmentState {
3083 module: &shader,
3084 entry_point: Some("fs_main"),
3085 targets: &[Some(wgpu::ColorTargetState {
3086 format,
3087 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
3088 write_mask: wgpu::ColorWrites::ALL,
3089 })],
3090 compilation_options: Default::default(),
3091 }),
3092 primitive: wgpu::PrimitiveState {
3093 topology: wgpu::PrimitiveTopology::TriangleList,
3094 ..Default::default()
3095 },
3096 depth_stencil: None,
3097 multisample: wgpu::MultisampleState::default(),
3098 multiview_mask: None,
3099 cache: None,
3100 })
3101}
3102
3103fn create_path_gradient_pipeline(
3109 device: &wgpu::Device,
3110 format: wgpu::TextureFormat,
3111 texture_bind_group_layout: &wgpu::BindGroupLayout,
3112) -> wgpu::RenderPipeline {
3113 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
3114 label: Some("path_gradient_shader"),
3115 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/path_gradient.wgsl").into()),
3116 });
3117
3118 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
3119 label: Some("path_gradient_pipeline_layout"),
3120 bind_group_layouts: &[Some(texture_bind_group_layout)],
3121 immediate_size: 0,
3122 });
3123
3124 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
3125 label: Some("path_gradient_pipeline"),
3126 layout: Some(&layout),
3127 vertex: wgpu::VertexState {
3128 module: &shader,
3129 entry_point: Some("vs_main"),
3130 buffers: &[Some(wgpu::VertexBufferLayout {
3131 array_stride: std::mem::size_of::<crate::vertex::PathGradientVertex>() as u64,
3132 step_mode: wgpu::VertexStepMode::Vertex,
3133 attributes: &[
3134 wgpu::VertexAttribute {
3135 offset: 0,
3136 shader_location: 0,
3137 format: wgpu::VertexFormat::Float32x2, },
3139 wgpu::VertexAttribute {
3140 offset: 8,
3141 shader_location: 1,
3142 format: wgpu::VertexFormat::Float32x2, },
3144 wgpu::VertexAttribute {
3145 offset: 16,
3146 shader_location: 2,
3147 format: wgpu::VertexFormat::Float32x2, },
3149 wgpu::VertexAttribute {
3150 offset: 24,
3151 shader_location: 3,
3152 format: wgpu::VertexFormat::Uint32, },
3154 wgpu::VertexAttribute {
3156 offset: 32,
3157 shader_location: 4,
3158 format: wgpu::VertexFormat::Float32x4, },
3160 wgpu::VertexAttribute {
3161 offset: 48,
3162 shader_location: 5,
3163 format: wgpu::VertexFormat::Float32x4, },
3165 wgpu::VertexAttribute {
3166 offset: 64,
3167 shader_location: 6,
3168 format: wgpu::VertexFormat::Float32x4, },
3170 wgpu::VertexAttribute {
3171 offset: 80,
3172 shader_location: 7,
3173 format: wgpu::VertexFormat::Float32x4, },
3175 wgpu::VertexAttribute {
3176 offset: 96,
3177 shader_location: 8,
3178 format: wgpu::VertexFormat::Float32x4, },
3180 wgpu::VertexAttribute {
3181 offset: 112,
3182 shader_location: 9,
3183 format: wgpu::VertexFormat::Float32x4, },
3185 ],
3186 })],
3187 compilation_options: Default::default(),
3188 },
3189 fragment: Some(wgpu::FragmentState {
3190 module: &shader,
3191 entry_point: Some("fs_main"),
3192 targets: &[Some(wgpu::ColorTargetState {
3193 format,
3194 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
3195 write_mask: wgpu::ColorWrites::ALL,
3196 })],
3197 compilation_options: Default::default(),
3198 }),
3199 primitive: wgpu::PrimitiveState {
3200 topology: wgpu::PrimitiveTopology::TriangleList,
3201 ..Default::default()
3202 },
3203 depth_stencil: None,
3204 multisample: wgpu::MultisampleState::default(),
3205 multiview_mask: None,
3206 cache: None,
3207 })
3208}
3209
3210fn create_shadow_pipeline(
3211 device: &wgpu::Device,
3212 format: wgpu::TextureFormat,
3213) -> wgpu::RenderPipeline {
3214 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
3215 label: Some("shadow_shader"),
3216 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/shadow.wgsl").into()),
3217 });
3218
3219 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
3220 label: Some("shadow_pipeline_layout"),
3221 bind_group_layouts: &[],
3222 immediate_size: 0,
3223 });
3224
3225 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
3226 label: Some("shadow_pipeline"),
3227 layout: Some(&layout),
3228 vertex: wgpu::VertexState {
3229 module: &shader,
3230 entry_point: Some("vs_main"),
3231 buffers: &[Some(wgpu::VertexBufferLayout {
3232 array_stride: std::mem::size_of::<ShadowVertex>() as u64,
3233 step_mode: wgpu::VertexStepMode::Vertex,
3234 attributes: &[
3235 wgpu::VertexAttribute {
3236 offset: 0,
3237 shader_location: 0,
3238 format: wgpu::VertexFormat::Float32x2, },
3240 wgpu::VertexAttribute {
3241 offset: 8,
3242 shader_location: 1,
3243 format: wgpu::VertexFormat::Float32x2, },
3245 wgpu::VertexAttribute {
3246 offset: 16,
3247 shader_location: 2,
3248 format: wgpu::VertexFormat::Float32x4, },
3250 wgpu::VertexAttribute {
3251 offset: 32,
3252 shader_location: 3,
3253 format: wgpu::VertexFormat::Float32x4, },
3255 wgpu::VertexAttribute {
3256 offset: 48,
3257 shader_location: 4,
3258 format: wgpu::VertexFormat::Float32x4, },
3260 wgpu::VertexAttribute {
3261 offset: 64,
3262 shader_location: 5,
3263 format: wgpu::VertexFormat::Float32x4, },
3265 ],
3266 })],
3267 compilation_options: Default::default(),
3268 },
3269 fragment: Some(wgpu::FragmentState {
3270 module: &shader,
3271 entry_point: Some("fs_main"),
3272 targets: &[Some(wgpu::ColorTargetState {
3273 format,
3274 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
3275 write_mask: wgpu::ColorWrites::ALL,
3276 })],
3277 compilation_options: Default::default(),
3278 }),
3279 primitive: wgpu::PrimitiveState {
3280 topology: wgpu::PrimitiveTopology::TriangleList,
3281 ..Default::default()
3282 },
3283 depth_stencil: None,
3284 multisample: wgpu::MultisampleState::default(),
3285 multiview_mask: None,
3286 cache: None,
3287 })
3288}
3289
3290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3301pub(crate) struct StreamQuadCounts {
3302 pub rect: usize,
3303 pub sdf: usize,
3304 pub quad: usize,
3305 pub shadow: usize,
3306 pub anim_proc: usize,
3307 pub path_gradient: usize,
3310}
3311
3312impl StreamQuadCounts {
3313 pub fn max(&self) -> usize {
3316 self.rect
3317 .max(self.sdf)
3318 .max(self.quad)
3319 .max(self.shadow)
3320 .max(self.anim_proc)
3321 .max(self.path_gradient)
3322 }
3323}
3324
3325pub(crate) fn stream_quad_counts(frame: &RenderFrame) -> StreamQuadCounts {
3347 let composite_quads = frame
3348 .draw_order
3349 .iter()
3350 .filter(|c| matches!(c, teksilo_canvas::DrawCommand::BeginBlurredSubtree { .. }))
3351 .count();
3352 let gradient_paths = frame
3353 .paths
3354 .iter()
3355 .filter(|p| !matches!(p.paint_data, teksilo_canvas::PaintData::Solid))
3356 .count();
3357 let solid_paths = frame.paths.len() - gradient_paths;
3358 StreamQuadCounts {
3359 rect: frame.decorations.len() + frame.cosmetic_lines.len(),
3360 sdf: frame.shapes.len(),
3361 quad: frame.glyphs.len()
3362 + solid_paths
3363 + frame.images.len()
3364 + frame.live_images.len()
3365 + composite_quads,
3366 shadow: frame.shadows.len(),
3367 anim_proc: frame.animated_quads.len(),
3368 path_gradient: gradient_paths,
3369 }
3370}
3371
3372fn create_anim_proc_pipeline(
3378 device: &wgpu::Device,
3379 format: wgpu::TextureFormat,
3380) -> (
3381 wgpu::RenderPipeline,
3382 wgpu::Buffer,
3383 wgpu::BindGroup,
3384 wgpu::BindGroupLayout,
3385) {
3386 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
3387 label: Some("anim_procedural_shader"),
3388 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/anim_procedural.wgsl").into()),
3389 });
3390
3391 let buffer_size = (MAX_ANIM_SLOTS * std::mem::size_of::<teksilo_canvas::AnimParams>()) as u64;
3392 let anim_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3393 label: Some("anim_uniform_buffer"),
3394 size: buffer_size,
3395 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3396 mapped_at_creation: false,
3397 });
3398
3399 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3400 label: Some("anim_uniform_bind_group_layout"),
3401 entries: &[wgpu::BindGroupLayoutEntry {
3402 binding: 0,
3403 visibility: wgpu::ShaderStages::FRAGMENT,
3404 ty: wgpu::BindingType::Buffer {
3405 ty: wgpu::BufferBindingType::Uniform,
3406 has_dynamic_offset: false,
3407 min_binding_size: None,
3408 },
3409 count: None,
3410 }],
3411 });
3412
3413 let anim_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3414 label: Some("anim_uniform_bind_group"),
3415 layout: &bind_group_layout,
3416 entries: &[wgpu::BindGroupEntry {
3417 binding: 0,
3418 resource: anim_uniform_buffer.as_entire_binding(),
3419 }],
3420 });
3421
3422 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
3423 label: Some("anim_proc_pipeline_layout"),
3424 bind_group_layouts: &[Some(&bind_group_layout)],
3425 immediate_size: 0,
3426 });
3427
3428 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
3429 label: Some("anim_proc_pipeline"),
3430 layout: Some(&pipeline_layout),
3431 vertex: wgpu::VertexState {
3432 module: &shader,
3433 entry_point: Some("vs_main"),
3434 buffers: &[Some(anim_quad_vertex_layout())],
3435 compilation_options: Default::default(),
3436 },
3437 fragment: Some(wgpu::FragmentState {
3438 module: &shader,
3439 entry_point: Some("fs_main"),
3440 targets: &[Some(wgpu::ColorTargetState {
3441 format,
3442 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
3443 write_mask: wgpu::ColorWrites::ALL,
3444 })],
3445 compilation_options: Default::default(),
3446 }),
3447 primitive: wgpu::PrimitiveState {
3448 topology: wgpu::PrimitiveTopology::TriangleList,
3449 ..Default::default()
3450 },
3451 depth_stencil: None,
3452 multisample: wgpu::MultisampleState::default(),
3453 multiview_mask: None,
3454 cache: None,
3455 });
3456
3457 (
3458 pipeline,
3459 anim_uniform_buffer,
3460 anim_uniform_bind_group,
3461 bind_group_layout,
3462 )
3463}
3464
3465fn create_anim_sprite_pipeline(
3472 device: &wgpu::Device,
3473 format: wgpu::TextureFormat,
3474 uniform_layout: &wgpu::BindGroupLayout,
3475 texture_layout: &wgpu::BindGroupLayout,
3476) -> wgpu::RenderPipeline {
3477 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
3478 label: Some("anim_sprite_shader"),
3479 source: wgpu::ShaderSource::Wgsl(include_str!("shaders/anim_sprite.wgsl").into()),
3480 });
3481
3482 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
3483 label: Some("anim_sprite_pipeline_layout"),
3484 bind_group_layouts: &[Some(uniform_layout), Some(texture_layout)],
3485 immediate_size: 0,
3486 });
3487
3488 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
3489 label: Some("anim_sprite_pipeline"),
3490 layout: Some(&pipeline_layout),
3491 vertex: wgpu::VertexState {
3492 module: &shader,
3493 entry_point: Some("vs_main"),
3494 buffers: &[Some(anim_quad_vertex_layout())],
3495 compilation_options: Default::default(),
3496 },
3497 fragment: Some(wgpu::FragmentState {
3498 module: &shader,
3499 entry_point: Some("fs_main"),
3500 targets: &[Some(wgpu::ColorTargetState {
3501 format,
3502 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
3503 write_mask: wgpu::ColorWrites::ALL,
3504 })],
3505 compilation_options: Default::default(),
3506 }),
3507 primitive: wgpu::PrimitiveState {
3508 topology: wgpu::PrimitiveTopology::TriangleList,
3509 ..Default::default()
3510 },
3511 depth_stencil: None,
3512 multisample: wgpu::MultisampleState::default(),
3513 multiview_mask: None,
3514 cache: None,
3515 })
3516}
3517
3518fn anim_quad_vertex_layout() -> wgpu::VertexBufferLayout<'static> {
3520 const ATTRS: [wgpu::VertexAttribute; 3] = [
3521 wgpu::VertexAttribute {
3522 offset: 0,
3523 shader_location: 0,
3524 format: wgpu::VertexFormat::Float32x2,
3525 },
3526 wgpu::VertexAttribute {
3527 offset: 8,
3528 shader_location: 1,
3529 format: wgpu::VertexFormat::Float32x2,
3530 },
3531 wgpu::VertexAttribute {
3532 offset: 16,
3533 shader_location: 2,
3534 format: wgpu::VertexFormat::Uint32,
3535 },
3536 ];
3537 wgpu::VertexBufferLayout {
3538 array_stride: std::mem::size_of::<AnimQuadVertex>() as u64,
3539 step_mode: wgpu::VertexStepMode::Vertex,
3540 attributes: &ATTRS,
3541 }
3542}
3543
3544#[cfg(test)]
3545mod tests {
3546 use teksilo_canvas::RenderFrame;
3547 use teksilo_canvas::render_frame::{DrawCommand, GlyphQuad, PaintData, ShapeKind, ShapeQuad};
3548
3549 use super::*;
3550
3551 #[test]
3552 fn stream_quad_counts_includes_sprite_anim_quads() {
3553 use teksilo_canvas::render_frame::{AnimatedQuadClass, AnimatedQuadDraw};
3560
3561 let mut frame = RenderFrame::new();
3562 for slot in 0..3 {
3563 frame.animated_quads.push(AnimatedQuadDraw {
3564 screen: [0.0, 0.0, 10.0, 10.0],
3565 slot,
3566 class: AnimatedQuadClass::Procedural,
3567 });
3568 }
3569 for slot in 3..5 {
3570 frame.animated_quads.push(AnimatedQuadDraw {
3571 screen: [0.0, 0.0, 10.0, 10.0],
3572 slot,
3573 class: AnimatedQuadClass::Sprite {
3574 image_name: "icon".to_string(),
3575 },
3576 });
3577 }
3578 frame.glyphs.push(GlyphQuad {
3579 screen: [0.0, 0.0, 8.0, 8.0],
3580 atlas: [0.0, 0.0, 2.0, 2.0],
3581 color: [1.0; 4],
3582 is_color: false,
3583 });
3584
3585 let counts = stream_quad_counts(&frame);
3586 assert_eq!(
3587 counts.anim_proc, 5,
3588 "anim_proc stream must be sized for BOTH Procedural and Sprite quads"
3589 );
3590 assert_eq!(counts.quad, 1);
3591 assert_eq!(counts.rect, 0);
3592 assert_eq!(counts.sdf, 0);
3593 assert_eq!(counts.shadow, 0);
3594 assert_eq!(counts.max(), 5, "index buffer sizes to the largest stream");
3595 }
3596
3597 #[test]
3598 fn stream_quad_counts_splits_solid_and_gradient_paths() {
3599 use teksilo_canvas::render_frame::PathEntry;
3605 use teksilo_canvas::{FillRule, GradientStop, Path, StrokeStyle};
3606 use teksilo_tokens::Color;
3607
3608 let mut frame = RenderFrame::new();
3609 frame.paths.push(PathEntry {
3610 path: Path::new(),
3611 color: [1.0, 0.0, 0.0, 1.0],
3612 stroke_style: StrokeStyle::solid(0.0),
3613 fill_rule: FillRule::Winding,
3614 bounds: [0.0, 0.0, 10.0, 10.0],
3615 paint_data: PaintData::Solid,
3616 });
3617 frame.paths.push(PathEntry {
3618 path: Path::new(),
3619 color: [1.0, 1.0, 1.0, 1.0],
3620 stroke_style: StrokeStyle::solid(0.0),
3621 fill_rule: FillRule::Winding,
3622 bounds: [0.0, 0.0, 20.0, 20.0],
3623 paint_data: PaintData::LinearGradient {
3624 start: [0.0, 0.0],
3625 end: [20.0, 0.0],
3626 stops: vec![
3627 GradientStop {
3628 offset: 0.0,
3629 color: Color::RED,
3630 },
3631 GradientStop {
3632 offset: 1.0,
3633 color: Color::BLUE,
3634 },
3635 ],
3636 },
3637 });
3638
3639 let counts = stream_quad_counts(&frame);
3640 assert_eq!(counts.quad, 1, "the solid path counts toward quad");
3641 assert_eq!(
3642 counts.path_gradient, 1,
3643 "the gradient path counts toward path_gradient, not quad"
3644 );
3645 assert_eq!(counts.rect, 0);
3646 assert_eq!(counts.sdf, 0);
3647 assert_eq!(counts.shadow, 0);
3648 assert_eq!(counts.anim_proc, 0);
3649 assert_eq!(counts.max(), 1);
3650 }
3651
3652 #[test]
3653 fn gradient_path_renders_nonflat_on_gpu() {
3654 use teksilo_canvas::render_frame::PathEntry;
3661 use teksilo_canvas::{FillRule, GradientStop, Path, Rect, StrokeStyle};
3662 use teksilo_tokens::Color;
3663
3664 let Some((mut renderer, device, queue)) = pollster::block_on(
3665 crate::test_support::require_test_renderer("teksilo_render_gradient_path_device"),
3666 ) else {
3667 return; };
3669
3670 let path = Path::rect(Rect::new(1.0, 1.0, 30.0, 30.0));
3672 let bounds = path.bounds();
3673 let mut frame = RenderFrame::new();
3674 frame.paths.push(PathEntry {
3675 path,
3676 color: [1.0, 1.0, 1.0, 1.0],
3677 stroke_style: StrokeStyle::solid(0.0),
3678 fill_rule: FillRule::Winding,
3679 bounds: [bounds.x, bounds.y, bounds.width, bounds.height],
3680 paint_data: PaintData::LinearGradient {
3681 start: [bounds.x, bounds.y],
3682 end: [bounds.x + bounds.width, bounds.y],
3683 stops: vec![
3684 GradientStop {
3685 offset: 0.0,
3686 color: Color::RED,
3687 },
3688 GradientStop {
3689 offset: 1.0,
3690 color: Color::BLUE,
3691 },
3692 ],
3693 },
3694 });
3695 frame.draw_order.push(DrawCommand::Path(0));
3696
3697 let texture = device.create_texture(&wgpu::TextureDescriptor {
3698 label: Some("teksilo_render_gradient_path_target"),
3699 size: wgpu::Extent3d {
3700 width: 32,
3701 height: 32,
3702 depth_or_array_layers: 1,
3703 },
3704 mip_level_count: 1,
3705 sample_count: 1,
3706 dimension: wgpu::TextureDimension::D2,
3707 format: wgpu::TextureFormat::Rgba8UnormSrgb,
3708 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
3709 view_formats: &[],
3710 });
3711 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3712
3713 renderer.render(&frame, &view, 1.0, 32, 32, [0.0, 0.0, 0.0, 0.0]);
3717
3718 let pixels = crate::test_support::read_texture_rgba(&device, &queue, &texture, 32, 32);
3719 let px = |x: usize, y: usize| {
3720 let i = (y * 32 + x) * 4;
3721 [pixels[i], pixels[i + 1], pixels[i + 2], pixels[i + 3]]
3722 };
3723 let left = px(4, 16);
3725 let right = px(27, 16);
3726
3727 assert!(
3728 left[3] > 200 && right[3] > 200,
3729 "gradient square not covered (coverage-mask atlas broken): left={left:?} right={right:?}"
3730 );
3731 assert!(
3734 left[0] as i32 > left[2] as i32 + 40,
3735 "left edge must be red-dominant, got {left:?}"
3736 );
3737 assert!(
3738 right[2] as i32 > right[0] as i32 + 40,
3739 "right edge must be blue-dominant, got {right:?}"
3740 );
3741 assert!(
3742 (left[0] as i32 - right[0] as i32).abs() > 60,
3743 "gradient looks flat (shader not sampling the gradient): left={left:?} right={right:?}"
3744 );
3745 }
3746
3747 #[test]
3748 fn gradient_path_partial_alpha_preserved() {
3749 use teksilo_canvas::render_frame::PathEntry;
3756 use teksilo_canvas::{FillRule, GradientStop, Path, Rect, StrokeStyle};
3757 use teksilo_tokens::Color;
3758
3759 let Some((mut renderer, device, queue)) = pollster::block_on(
3760 crate::test_support::require_test_renderer("teksilo_render_partial_alpha_device"),
3761 ) else {
3762 return;
3763 };
3764
3765 let path = Path::rect(Rect::new(0.0, 0.0, 32.0, 32.0));
3766 let bounds = path.bounds();
3767 let mut frame = RenderFrame::new();
3768 frame.paths.push(PathEntry {
3769 path,
3770 color: [1.0, 1.0, 1.0, 1.0],
3771 stroke_style: StrokeStyle::solid(0.0),
3772 fill_rule: FillRule::Winding,
3773 bounds: [bounds.x, bounds.y, bounds.width, bounds.height],
3774 paint_data: PaintData::LinearGradient {
3775 start: [0.0, 0.0],
3776 end: [32.0, 0.0],
3777 stops: vec![
3778 GradientStop {
3779 offset: 0.0,
3780 color: Color::from_rgba(0.0, 0.62, 0.45, 1.0),
3781 },
3782 GradientStop {
3783 offset: 1.0,
3784 color: Color::from_rgba(0.0, 0.62, 0.45, 0.4),
3785 },
3786 ],
3787 },
3788 });
3789 frame.draw_order.push(DrawCommand::Path(0));
3790
3791 let texture = device.create_texture(&wgpu::TextureDescriptor {
3792 label: Some("partial_alpha_target"),
3793 size: wgpu::Extent3d {
3794 width: 32,
3795 height: 32,
3796 depth_or_array_layers: 1,
3797 },
3798 mip_level_count: 1,
3799 sample_count: 1,
3800 dimension: wgpu::TextureDimension::D2,
3801 format: wgpu::TextureFormat::Rgba8UnormSrgb,
3802 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
3803 view_formats: &[],
3804 });
3805 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3806 renderer.render(&frame, &view, 1.0, 32, 32, [0.0, 0.0, 0.0, 0.0]);
3807
3808 let px = crate::test_support::read_texture_rgba(&device, &queue, &texture, 32, 32);
3809 let alpha = |x: usize| px[(16 * 32 + x) * 4 + 3];
3810 let (left, right) = (alpha(2), alpha(29));
3811 assert!(
3813 left >= 240,
3814 "opaque (a=1.0) end must stay opaque, got {left} (/255)"
3815 );
3816 assert!(
3817 (90..=115).contains(&right),
3818 "a=0.4 stop must read ~102/255, got {right} — a value near ~61 means the \
3819 pipeline under-renders gradient stop alpha (washed-out fills)"
3820 );
3821 }
3822
3823 #[test]
3824 fn glyph_quad_renders_over_shape_in_offscreen_target() {
3825 let Some((mut renderer, device, queue)) = pollster::block_on(
3826 crate::test_support::require_test_renderer("teksilo_render_test_device"),
3827 ) else {
3828 return;
3829 };
3830
3831 renderer.upload_atlas(
3832 2,
3833 2,
3834 &[
3835 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
3836 ],
3837 );
3838
3839 let mut frame = RenderFrame::new();
3840 frame.shapes.push(ShapeQuad {
3841 screen: [4.0, 4.0, 24.0, 24.0],
3842 color: [0.2, 0.6, 0.9, 1.0],
3843 shape: ShapeKind::RoundedRect,
3844 stroke_width: 0.0,
3845 stroke_space: teksilo_canvas::StrokeSpace::Logical,
3846 corner_radii: [0.0; 4],
3847 paint_data: PaintData::Solid,
3848 });
3849 frame.draw_order.push(DrawCommand::Shape(0));
3850
3851 frame.glyphs.push(GlyphQuad {
3852 screen: [10.0, 10.0, 8.0, 8.0],
3853 atlas: [0.0, 0.0, 2.0, 2.0],
3854 color: [1.0, 1.0, 1.0, 1.0],
3855 is_color: false,
3856 });
3857 frame.draw_order.push(DrawCommand::Glyph(0));
3858
3859 let texture = device.create_texture(&wgpu::TextureDescriptor {
3860 label: Some("teksilo_render_test_target"),
3861 size: wgpu::Extent3d {
3862 width: 32,
3863 height: 32,
3864 depth_or_array_layers: 1,
3865 },
3866 mip_level_count: 1,
3867 sample_count: 1,
3868 dimension: wgpu::TextureDimension::D2,
3869 format: wgpu::TextureFormat::Rgba8UnormSrgb,
3870 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
3871 view_formats: &[],
3872 });
3873 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3874
3875 renderer.render(&frame, &view, 1.0, 32, 32, [0.0, 0.0, 0.0, 0.0]);
3876
3877 let pixels = crate::test_support::read_texture_rgba(&device, &queue, &texture, 32, 32);
3878 let center = ((14 * 32 + 14) * 4) as usize;
3879 let blue_only = [
3880 pixels[center],
3881 pixels[center + 1],
3882 pixels[center + 2],
3883 pixels[center + 3],
3884 ];
3885
3886 assert!(
3887 blue_only[0] > 200 && blue_only[1] > 200 && blue_only[2] > 200,
3888 "expected glyph pixel to be visible over shape, got {:?}",
3889 blue_only
3890 );
3891 }
3892
3893 #[test]
3894 fn fractional_origin_glyph_renders_pixel_exact() {
3895 let Some((mut renderer, device, queue)) = pollster::block_on(
3904 crate::test_support::require_test_renderer("teksilo_render_snap_test_device"),
3905 ) else {
3906 return;
3907 };
3908
3909 let mut atlas = [0u8; 4 * 4 * 4];
3912 for y in 0..3 {
3913 for x in 0..3 {
3914 let i = (y * 4 + x) * 4;
3915 atlas[i..i + 4].copy_from_slice(&[255, 255, 255, 255]);
3916 }
3917 }
3918 renderer.upload_atlas(4, 4, &atlas);
3919
3920 let mut frame = RenderFrame::new();
3921 frame.glyphs.push(GlyphQuad {
3923 screen: [10.4, 10.6, 3.0, 3.0],
3924 atlas: [0.0, 0.0, 3.0, 3.0],
3925 color: [1.0, 1.0, 1.0, 1.0],
3926 is_color: false,
3927 });
3928 frame.draw_order.push(DrawCommand::Glyph(0));
3929
3930 let texture = device.create_texture(&wgpu::TextureDescriptor {
3931 label: Some("teksilo_render_snap_test_target"),
3932 size: wgpu::Extent3d {
3933 width: 32,
3934 height: 32,
3935 depth_or_array_layers: 1,
3936 },
3937 mip_level_count: 1,
3938 sample_count: 1,
3939 dimension: wgpu::TextureDimension::D2,
3940 format: wgpu::TextureFormat::Rgba8UnormSrgb,
3941 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
3942 view_formats: &[],
3943 });
3944 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3945
3946 renderer.render(&frame, &view, 1.0, 32, 32, [0.0, 0.0, 0.0, 0.0]);
3947
3948 let pixels = crate::test_support::read_texture_rgba(&device, &queue, &texture, 32, 32);
3949 let alpha = |x: usize, y: usize| pixels[(y * 32 + x) * 4 + 3];
3950
3951 for y in 11..14 {
3955 for x in 10..13 {
3956 assert_eq!(
3957 alpha(x, y),
3958 255,
3959 "interior pixel ({x},{y}) must be fully opaque — \
3960 bilinear edge feathering means the snap did not fire"
3961 );
3962 }
3963 }
3964 for y in 10..15 {
3967 for x in 9..14 {
3968 let inside = (10..13).contains(&x) && (11..14).contains(&y);
3969 if !inside {
3970 assert_eq!(
3971 alpha(x, y),
3972 0,
3973 "ring pixel ({x},{y}) must be untouched — \
3974 the snapped quad must not bleed past its bitmap"
3975 );
3976 }
3977 }
3978 }
3979 }
3980
3981 #[test]
3994 fn fractional_origin_path_renders_pixel_exact() {
3995 let Some((mut renderer, device, queue)) = pollster::block_on(
3996 crate::test_support::require_test_renderer("teksilo_render_path_snap_test_device"),
3997 ) else {
3998 return;
3999 };
4000
4001 let mut path = teksilo_canvas::Path::new();
4004 path.move_to(teksilo_canvas::Point::new(8.5, 4.0));
4005 path.line_to(teksilo_canvas::Point::new(8.5, 12.0));
4006 let stroke_style = teksilo_canvas::StrokeStyle::solid(1.0);
4007 let bounds = path.bounds().expand(stroke_style.width);
4008 assert_eq!(bounds.x, 7.5, "the half-pixel origin this test is about");
4009
4010 let mut frame = RenderFrame::new();
4011 frame.paths.push(teksilo_canvas::PathEntry {
4012 path,
4013 color: [1.0, 1.0, 1.0, 1.0],
4014 stroke_style,
4015 fill_rule: teksilo_canvas::FillRule::Winding,
4016 bounds: bounds.to_array(),
4017 paint_data: teksilo_canvas::PaintData::Solid,
4018 });
4019 frame.draw_order.push(DrawCommand::Path(0));
4020
4021 let texture = device.create_texture(&wgpu::TextureDescriptor {
4022 label: Some("teksilo_render_path_snap_test_target"),
4023 size: wgpu::Extent3d {
4024 width: 32,
4025 height: 32,
4026 depth_or_array_layers: 1,
4027 },
4028 mip_level_count: 1,
4029 sample_count: 1,
4030 dimension: wgpu::TextureDimension::D2,
4031 format: wgpu::TextureFormat::Rgba8UnormSrgb,
4032 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
4033 view_formats: &[],
4034 });
4035 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
4036
4037 renderer.render(&frame, &view, 1.0, 32, 32, [0.0, 0.0, 0.0, 0.0]);
4038
4039 let pixels = crate::test_support::read_texture_rgba(&device, &queue, &texture, 32, 32);
4040 let alpha = |x: usize, y: usize| pixels[(y * 32 + x) * 4 + 3];
4041
4042 for y in 5..11 {
4043 assert_eq!(
4044 alpha(8, y),
4045 255,
4046 "the hairline's own column must be fully inked at y={y} — \
4047 anything less means the quad was resampled off the pixel grid"
4048 );
4049 for x in [6, 7, 9, 10] {
4050 assert_eq!(
4051 alpha(x, y),
4052 0,
4053 "({x},{y}) must be untouched — a 1 px stroke that leaks \
4054 into its neighbours is the blur this snap removes"
4055 );
4056 }
4057 }
4058 }
4059
4060 fn render_to_pixels(
4063 renderer: &mut Renderer,
4064 device: &wgpu::Device,
4065 queue: &wgpu::Queue,
4066 frame: &RenderFrame,
4067 ) -> Vec<u8> {
4068 let texture = device.create_texture(&wgpu::TextureDescriptor {
4069 label: Some("teksilo_render_blur_isolation_target"),
4070 size: wgpu::Extent3d {
4071 width: 64,
4072 height: 32,
4073 depth_or_array_layers: 1,
4074 },
4075 mip_level_count: 1,
4076 sample_count: 1,
4077 dimension: wgpu::TextureDimension::D2,
4078 format: wgpu::TextureFormat::Rgba8UnormSrgb,
4079 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
4080 view_formats: &[],
4081 });
4082 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
4083 renderer.render(frame, &view, 1.0, 64, 32, [0.0, 0.0, 0.0, 0.0]);
4084 crate::test_support::read_texture_rgba(device, queue, &texture, 64, 32)
4085 }
4086
4087 fn push_blurred_square(
4089 frame: &mut RenderFrame,
4090 bounds: teksilo_canvas::Rect,
4091 radius: f32,
4092 square: teksilo_canvas::Rect,
4093 ) {
4094 frame
4095 .draw_order
4096 .push(DrawCommand::BeginBlurredSubtree { bounds, radius });
4097 let idx = frame.decorations.len();
4098 frame.decorations.push(teksilo_canvas::DecorationRect {
4099 rect: square.to_array(),
4100 color: [1.0, 1.0, 1.0, 1.0],
4101 kind: teksilo_canvas::DecorationKind::WidgetBackground,
4102 });
4103 frame.draw_order.push(DrawCommand::Decoration(idx));
4104 frame.draw_order.push(DrawCommand::EndBlurredSubtree);
4105 }
4106
4107 #[test]
4108 fn a_blur_scope_does_not_take_the_kernel_of_a_later_one() {
4109 use teksilo_canvas::Rect;
4110 let Some((mut renderer, device, queue)) = pollster::block_on(
4111 crate::test_support::require_test_renderer("teksilo_render_blur_isolation_device"),
4112 ) else {
4113 return; };
4115
4116 let a_bounds = Rect::new(0.0, 0.0, 32.0, 32.0);
4118 let a_square = Rect::new(12.0, 12.0, 8.0, 8.0);
4119 let mut alone = RenderFrame::new();
4120 push_blurred_square(&mut alone, a_bounds, 6.0, a_square);
4121
4122 let mut followed = RenderFrame::new();
4126 push_blurred_square(&mut followed, a_bounds, 6.0, a_square);
4127 push_blurred_square(
4128 &mut followed,
4129 Rect::new(40.0, 8.0, 16.0, 16.0),
4130 2.0,
4131 Rect::new(44.0, 12.0, 8.0, 8.0),
4132 );
4133
4134 let a_alone = render_to_pixels(&mut renderer, &device, &queue, &alone);
4135 let a_followed = render_to_pixels(&mut renderer, &device, &queue, &followed);
4136 let region = |pixels: &[u8]| -> Vec<u8> {
4137 (0..32)
4138 .flat_map(|y| pixels[y * 64 * 4..(y * 64 + 32) * 4].to_vec())
4139 .collect()
4140 };
4141 assert!(
4142 region(&a_alone).iter().any(|&b| b != 0),
4143 "scope A must have drawn something"
4144 );
4145 assert_eq!(
4146 region(&a_alone),
4147 region(&a_followed),
4148 "scope A must blur the same whatever blur scope follows it in the frame"
4149 );
4150 }
4151
4152 #[test]
4153 fn each_kawase_pass_gets_its_own_parameter_slot() {
4154 let Some((_renderer, device, queue)) = pollster::block_on(
4155 crate::test_support::require_test_renderer("teksilo_render_blur_slots_device"),
4156 ) else {
4157 return; };
4159 let mut arena = crate::blur::KawaseParamArena::new(&device);
4160 let passes = crate::blur::PARAM_SLOTS_PER_SCOPE + 3;
4162 arena.begin_frame(&device, passes);
4163 let offsets: Vec<u32> = (0..passes)
4164 .map(|i| {
4165 let v = i as f32;
4166 arena.push(
4167 &queue,
4168 &crate::blur::BlurParams {
4169 offset: [v, v + 0.5, 0.0, 0.0],
4170 },
4171 )
4172 })
4173 .collect();
4174 let stride = arena.slot_stride();
4175 for (i, offset) in offsets.iter().enumerate() {
4176 assert_eq!(u64::from(*offset), stride * i as u64);
4177 assert_eq!(
4178 u64::from(*offset) % u64::from(device.limits().min_uniform_buffer_offset_alignment),
4179 0,
4180 "dynamic offsets must respect the device alignment"
4181 );
4182 }
4183
4184 let size = arena.buffer().size();
4185 let readback = device.create_buffer(&wgpu::BufferDescriptor {
4186 label: Some("blur_params_readback"),
4187 size,
4188 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
4189 mapped_at_creation: false,
4190 });
4191 let mut encoder =
4192 device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
4193 encoder.copy_buffer_to_buffer(arena.buffer(), 0, &readback, 0, size);
4194 queue.submit(Some(encoder.finish()));
4195 readback.slice(..).map_async(wgpu::MapMode::Read, |_| {});
4196 device
4197 .poll(wgpu::PollType::Wait {
4198 submission_index: None,
4199 timeout: None,
4200 })
4201 .expect("poll");
4202 let bytes = readback
4203 .slice(..)
4204 .get_mapped_range()
4205 .expect("mapped readback")
4206 .to_vec();
4207 for (i, offset) in offsets.iter().enumerate() {
4208 let at = *offset as usize;
4209 let slot: &[f32] = bytemuck::cast_slice(&bytes[at..at + 16]);
4210 assert_eq!(slot, &[i as f32, i as f32 + 0.5, 0.0, 0.0], "slot {i}");
4211 }
4212 }
4213
4214 fn render_scaled(
4217 renderer: &mut Renderer,
4218 device: &wgpu::Device,
4219 queue: &wgpu::Queue,
4220 frame: &RenderFrame,
4221 scale: f32,
4222 (w, h): (u32, u32),
4223 ) -> impl Fn(u32, u32) -> [u8; 4] {
4224 let texture = device.create_texture(&wgpu::TextureDescriptor {
4225 label: Some("teksilo_render_clip_target"),
4226 size: wgpu::Extent3d {
4227 width: w,
4228 height: h,
4229 depth_or_array_layers: 1,
4230 },
4231 mip_level_count: 1,
4232 sample_count: 1,
4233 dimension: wgpu::TextureDimension::D2,
4234 format: wgpu::TextureFormat::Rgba8UnormSrgb,
4235 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
4236 view_formats: &[],
4237 });
4238 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
4239 renderer.render(frame, &view, scale, w, h, [0.0, 0.0, 0.0, 0.0]);
4240 let pixels = crate::test_support::read_texture_rgba(device, queue, &texture, w, h);
4241 move |x, y| {
4242 let i = ((y * w + x) * 4) as usize;
4243 [pixels[i], pixels[i + 1], pixels[i + 2], pixels[i + 3]]
4244 }
4245 }
4246
4247 fn push_white_rect(frame: &mut RenderFrame, rect: teksilo_canvas::Rect) {
4248 let idx = frame.decorations.len();
4249 frame.decorations.push(teksilo_canvas::DecorationRect {
4250 rect: rect.to_array(),
4251 color: [1.0, 1.0, 1.0, 1.0],
4252 kind: teksilo_canvas::DecorationKind::WidgetBackground,
4253 });
4254 frame.draw_order.push(DrawCommand::Decoration(idx));
4255 }
4256
4257 #[test]
4258 fn a_clip_under_a_translation_lands_where_the_content_does_at_fractional_scale() {
4259 use teksilo_canvas::Rect;
4260 let Some((mut renderer, device, queue)) = pollster::block_on(
4261 crate::test_support::require_test_renderer("teksilo_render_clip_translate_device"),
4262 ) else {
4263 return; };
4265 let mut frame = RenderFrame::new();
4269 frame
4270 .draw_order
4271 .push(DrawCommand::PushTransform(Transform2D::translate(
4272 10.0, 0.0,
4273 )));
4274 frame
4275 .draw_order
4276 .push(DrawCommand::SetClip(Rect::new(0.0, 0.0, 20.0, 20.0)));
4277 push_white_rect(&mut frame, Rect::new(0.0, 0.0, 40.0, 20.0));
4278 frame.draw_order.push(DrawCommand::ClearClip);
4279 frame.draw_order.push(DrawCommand::PopTransform);
4280
4281 let px = render_scaled(&mut renderer, &device, &queue, &frame, 1.5, (96, 40));
4282 assert_eq!(px(17, 10)[3], 255, "inside the clip, near its leading edge");
4283 assert_eq!(
4284 px(43, 10)[3],
4285 255,
4286 "inside the clip, near its trailing edge"
4287 );
4288 assert_eq!(px(47, 10)[3], 0, "past the clip's trailing edge");
4289 assert_eq!(px(12, 10)[3], 0, "before the content starts");
4290 }
4291
4292 #[test]
4293 fn a_clip_still_applies_after_a_blur_scope_reopens_the_pass() {
4294 use teksilo_canvas::Rect;
4295 let Some((mut renderer, device, queue)) = pollster::block_on(
4296 crate::test_support::require_test_renderer("teksilo_render_clip_after_blur_device"),
4297 ) else {
4298 return; };
4300 let mut frame = RenderFrame::new();
4304 frame
4305 .draw_order
4306 .push(DrawCommand::SetClip(Rect::new(0.0, 0.0, 20.0, 32.0)));
4307 push_blurred_square(
4308 &mut frame,
4309 Rect::new(0.0, 0.0, 16.0, 16.0),
4310 2.0,
4311 Rect::new(4.0, 4.0, 8.0, 8.0),
4312 );
4313 push_white_rect(&mut frame, Rect::new(0.0, 20.0, 64.0, 12.0));
4314 frame.draw_order.push(DrawCommand::ClearClip);
4315
4316 let px = render_scaled(&mut renderer, &device, &queue, &frame, 1.0, (64, 32));
4317 assert_eq!(px(10, 26)[3], 255, "inside the clip");
4318 assert_eq!(px(40, 26)[3], 0, "outside the clip");
4319 }
4320
4321 #[test]
4322 fn a_clip_set_before_a_blur_scope_does_not_clip_inside_its_intermediate() {
4323 use teksilo_canvas::Rect;
4324 let Some((mut renderer, device, queue)) = pollster::block_on(
4325 crate::test_support::require_test_renderer("teksilo_render_clip_into_blur_device"),
4326 ) else {
4327 return; };
4329 let mut frame = RenderFrame::new();
4335 frame
4336 .draw_order
4337 .push(DrawCommand::SetClip(Rect::new(30.0, 0.0, 34.0, 32.0)));
4338 push_blurred_square(
4339 &mut frame,
4340 Rect::new(30.0, 0.0, 16.0, 16.0),
4341 2.0,
4342 Rect::new(32.0, 2.0, 12.0, 12.0),
4343 );
4344 frame.draw_order.push(DrawCommand::ClearClip);
4345
4346 let px = render_scaled(&mut renderer, &device, &queue, &frame, 1.0, (64, 32));
4347 assert!(
4348 px(38, 8)[3] > 128,
4349 "the blurred square shows at the centre of the scope, got {:?}",
4350 px(38, 8)
4351 );
4352 }
4353
4354 #[test]
4355 fn a_clip_opened_inside_a_blur_scope_stays_in_the_intermediates_space() {
4356 use teksilo_canvas::Rect;
4357 let Some((mut renderer, device, queue)) = pollster::block_on(
4358 crate::test_support::require_test_renderer("teksilo_render_clip_inside_blur_device"),
4359 ) else {
4360 return; };
4362 let mut frame = RenderFrame::new();
4370 frame
4371 .draw_order
4372 .push(DrawCommand::SetClip(Rect::new(30.0, 0.0, 34.0, 32.0)));
4373 let scope = Rect::new(30.0, 0.0, 16.0, 16.0);
4374 frame.draw_order.push(DrawCommand::BeginBlurredSubtree {
4375 bounds: scope,
4376 radius: 2.0,
4377 });
4378 frame.draw_order.push(DrawCommand::SetClip(scope));
4379 push_white_rect(&mut frame, Rect::new(32.0, 2.0, 12.0, 12.0));
4380 frame.draw_order.push(DrawCommand::ClearClip);
4381 push_white_rect(&mut frame, Rect::new(32.0, 2.0, 12.0, 12.0));
4382 frame.draw_order.push(DrawCommand::EndBlurredSubtree);
4383 frame.draw_order.push(DrawCommand::ClearClip);
4384
4385 let px = render_scaled(&mut renderer, &device, &queue, &frame, 1.0, (64, 32));
4386 assert!(
4387 px(38, 8)[3] > 128,
4388 "the square drawn inside the scope shows, got {:?}",
4389 px(38, 8)
4390 );
4391 }
4392
4393 #[test]
4394 fn a_clip_that_starts_before_the_target_keeps_its_far_edge() {
4395 use teksilo_canvas::Rect;
4396 let Some((mut renderer, device, queue)) = pollster::block_on(
4397 crate::test_support::require_test_renderer("teksilo_render_clip_negative_device"),
4398 ) else {
4399 return; };
4401 let mut frame = RenderFrame::new();
4404 frame
4405 .draw_order
4406 .push(DrawCommand::SetClip(Rect::new(-10.0, 0.0, 30.0, 20.0)));
4407 push_white_rect(&mut frame, Rect::new(0.0, 0.0, 64.0, 20.0));
4408 frame.draw_order.push(DrawCommand::ClearClip);
4409
4410 let px = render_scaled(&mut renderer, &device, &queue, &frame, 1.0, (64, 32));
4411 assert_eq!(px(18, 10)[3], 255, "inside the clip");
4412 assert_eq!(px(24, 10)[3], 0, "past the clip's far edge");
4413 }
4414}