1use std::collections::HashMap;
8
9use vello_common::paint::ImageSource;
10use vello_hybrid::{
11 RenderSize, RenderTargetConfig, Renderer as HRenderer, Resources, Scene, TextureBindings,
12};
13
14use super::{
15 dimension, image_key, recorded_images, unpremultiply, HybridScene, Pass, Writer,
16 PICK_ALIASING_THRESHOLD,
17};
18use crate::backend::{BackendError, Renderer, WgpuRenderer};
19use crate::color::Color;
20use crate::geometry::Affine;
21use crate::pick;
22
23struct Target {
28 texture: wgpu::Texture,
29 view: wgpu::TextureView,
30 readback: wgpu::Buffer,
31 width: u32,
32 height: u32,
33 padded_bytes_per_row: u32,
35 format: wgpu::TextureFormat,
36}
37
38impl Target {
39 fn new(device: &wgpu::Device, width: u32, height: u32, format: wgpu::TextureFormat) -> Self {
44 let bytes_per_row = width * 4;
45 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
46 let padded_bytes_per_row = bytes_per_row.div_ceil(align) * align;
47
48 let texture = device.create_texture(&wgpu::TextureDescriptor {
49 label: Some("hephaestus.hybrid.target"),
50 size: wgpu::Extent3d {
51 width,
52 height,
53 depth_or_array_layers: 1,
54 },
55 mip_level_count: 1,
56 sample_count: 1,
57 dimension: wgpu::TextureDimension::D2,
58 format,
59 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
60 view_formats: &[],
61 });
62 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
63 let readback = device.create_buffer(&wgpu::BufferDescriptor {
64 label: Some("hephaestus.hybrid.readback"),
65 size: u64::from(padded_bytes_per_row) * u64::from(height),
66 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
67 mapped_at_creation: false,
68 });
69 Self {
70 texture,
71 view,
72 readback,
73 width,
74 height,
75 padded_bytes_per_row,
76 format,
77 }
78 }
79}
80
81struct SizeBound {
87 renderer: HRenderer,
88 resources: Resources,
89 display: Scene,
90 pick: Option<Scene>,
91 images: HashMap<u64, ImageSource>,
92 width: u32,
93 height: u32,
94 format: wgpu::TextureFormat,
95}
96
97struct PendingPick {
105 slot: std::sync::Arc<std::sync::Mutex<Option<Result<(), wgpu::BufferAsyncError>>>>,
106 width: u32,
107 height: u32,
108}
109
110pub struct HybridRenderer {
117 device: wgpu::Device,
118 queue: wgpu::Queue,
119 scene: HybridScene,
120 picking: bool,
121 sized: Option<SizeBound>,
122 target: Option<Target>,
123 pick_target: Option<Target>,
124 hitmap: Option<Vec<u32>>,
126 hitmap_dims: Option<(u32, u32)>,
127 pick_pending: Option<PendingPick>,
128 target_format: wgpu::TextureFormat,
131 refresh_pick: bool,
134}
135
136impl HybridRenderer {
137 pub fn new() -> Result<Self, BackendError> {
140 pollster::block_on(Self::new_async(false))
141 }
142
143 pub fn with_picking() -> Result<Self, BackendError> {
146 pollster::block_on(Self::new_async(true))
147 }
148
149 pub fn with_device(device: &wgpu::Device, queue: &wgpu::Queue) -> Result<Self, BackendError> {
155 Ok(Self::build(device.clone(), queue.clone(), false))
156 }
157
158 pub fn with_device_and_picking(
160 device: &wgpu::Device,
161 queue: &wgpu::Queue,
162 ) -> Result<Self, BackendError> {
163 Ok(Self::build(device.clone(), queue.clone(), true))
164 }
165
166 async fn new_async(picking: bool) -> Result<Self, BackendError> {
167 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
168 desc.backends =
173 wgpu::Backends::from_env().unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL);
174 let instance = wgpu::Instance::new(desc);
175 let adapter = instance
176 .request_adapter(&wgpu::RequestAdapterOptions {
177 power_preference: wgpu::PowerPreference::HighPerformance,
178 compatible_surface: None,
179 force_fallback_adapter: false,
180 })
181 .await
182 .map_err(|_| BackendError::NoAdapter)?;
183 let (device, queue) = adapter
184 .request_device(&wgpu::DeviceDescriptor {
185 label: Some("hephaestus.hybrid.device"),
186 required_features: wgpu::Features::empty(),
187 required_limits: wgpu::Limits::default(),
188 memory_hints: wgpu::MemoryHints::default(),
189 trace: wgpu::Trace::Off,
190 experimental_features: wgpu::ExperimentalFeatures::default(),
191 })
192 .await
193 .map_err(|e| BackendError::DeviceRequest(e.to_string()))?;
194 Ok(Self::build(device, queue, picking))
195 }
196
197 fn build(device: wgpu::Device, queue: wgpu::Queue, picking: bool) -> Self {
198 Self {
199 device,
200 queue,
201 scene: HybridScene::new(),
202 picking,
203 sized: None,
204 target: None,
205 pick_target: None,
206 hitmap: None,
207 hitmap_dims: None,
208 pick_pending: None,
209 target_format: wgpu::TextureFormat::Rgba8Unorm,
210 refresh_pick: true,
211 }
212 }
213
214 pub fn pick_at(&self, x: u32, y: u32) -> Option<u32> {
221 let (w, h) = self.hitmap_dims?;
222 if x >= w || y >= h {
223 return None;
224 }
225 let hitmap = self.hitmap.as_ref()?;
226 pick::decode(hitmap[(y * w + x) as usize])
227 }
228
229 pub fn set_refresh_pick(&mut self, refresh: bool) {
244 self.refresh_pick = refresh;
245 }
246
247 pub fn refreshes_pick(&self) -> bool {
249 self.picking && self.refresh_pick
250 }
251
252 pub fn set_target_format(&mut self, format: wgpu::TextureFormat) {
263 self.target_format = format;
264 }
265
266 pub fn hitmap(&self) -> Option<&[u32]> {
271 self.hitmap.as_deref()
272 }
273
274 fn ensure_sized(
277 &mut self,
278 width: u32,
279 height: u32,
280 format: wgpu::TextureFormat,
281 ) -> Result<(), BackendError> {
282 if self
283 .sized
284 .as_ref()
285 .is_some_and(|s| s.width == width && s.height == height && s.format == format)
286 {
287 return Ok(());
288 }
289 let (w16, h16) = (dimension(width)?, dimension(height)?);
290 let (renderer, resources) = HRenderer::new(
291 &self.device,
292 &RenderTargetConfig {
293 format,
294 width,
295 height,
296 },
297 );
298 self.sized = Some(SizeBound {
299 renderer,
300 resources,
301 display: Scene::new(w16, h16),
302 pick: self.picking.then(|| Scene::new(w16, h16)),
303 images: HashMap::new(),
304 width,
305 height,
306 format,
307 });
308 Ok(())
309 }
310}
311
312impl HybridRenderer {
314 fn upload_images(&mut self, encoder: &mut wgpu::CommandEncoder) -> Result<(), BackendError> {
317 let images = recorded_images(&self.scene.ops);
318 if images.is_empty() {
319 return Ok(());
320 }
321 let sized = self.sized.as_mut().expect("sized state ensured");
322 for image in images {
323 let key = image_key(image);
324 if sized.images.contains_key(&key) {
325 continue;
326 }
327 let ImageSource::Pixmap(pixmap) = ImageSource::from_peniko_image_data(image) else {
330 return Err(BackendError::Other(
331 "image conversion did not yield pixel data".into(),
332 ));
333 };
334 let transparency = pixmap.may_have_transparency();
335 let id = sized.renderer.upload_image(
336 &mut sized.resources,
337 &self.device,
338 &self.queue,
339 encoder,
340 &pixmap,
341 );
342 sized.images.insert(
343 key,
344 ImageSource::opaque_id_with_transparency_hint(id, transparency),
345 );
346 }
347 Ok(())
348 }
349
350 fn replay(&mut self, background: Color, width: u32, height: u32, refresh_pick: bool) {
353 let sized = self.sized.as_mut().expect("sized state ensured");
354 let frame = crate::geometry::Rect::new(0.0, 0.0, width.into(), height.into());
355
356 sized.display.reset();
357 sized.display.set_aliasing_threshold(None);
358 sized.display.set_transform(Affine::IDENTITY);
361 sized.display.set_paint(background);
362 sized.display.fill_rect(&frame);
363
364 let mut writer = Writer {
365 scene: &mut sized.display,
366 resources: &mut sized.resources,
367 pass: Pass::Display,
368 images: &sized.images,
369 };
370 self.scene.ops.replay(&mut writer);
371
372 if !refresh_pick {
373 return;
374 }
375 if let Some(pick) = sized.pick.as_mut() {
376 pick.reset();
377 pick.set_aliasing_threshold(Some(PICK_ALIASING_THRESHOLD));
381 let mut writer = Writer {
384 scene: pick,
385 resources: &mut sized.resources,
386 pass: Pass::Pick,
387 images: &sized.images,
388 };
389 self.scene.ops.replay(&mut writer);
390 }
391 }
392}
393
394impl HybridRenderer {
395 fn ensure_pick_target(&mut self, width: u32, height: u32) {
398 let format = self
402 .sized
403 .as_ref()
404 .map_or(wgpu::TextureFormat::Rgba8Unorm, |s| s.format);
405 if self
406 .pick_target
407 .as_ref()
408 .is_none_or(|t| t.width != width || t.height != height || t.format != format)
409 {
410 self.pick_target = Some(Target::new(&self.device, width, height, format));
411 }
412 }
413
414 fn rasterise(
416 &mut self,
417 encoder: &mut wgpu::CommandEncoder,
418 pass: Pass,
419 view: &wgpu::TextureView,
420 width: u32,
421 height: u32,
422 ) -> Result<(), BackendError> {
423 let sized = self.sized.as_mut().expect("sized state ensured");
424 let scene = match pass {
425 Pass::Display => &sized.display,
426 Pass::Pick => sized.pick.as_ref().expect("pick scene present"),
427 };
428 sized
429 .renderer
430 .render(
431 scene,
432 &mut sized.resources,
433 &self.device,
434 &self.queue,
435 encoder,
436 &RenderSize { width, height },
437 view,
438 &TextureBindings::new(),
439 )
440 .map_err(|e| match pass {
441 Pass::Display => BackendError::Other(format!("hybrid render: {e}")),
442 Pass::Pick => BackendError::Other(format!("hybrid pick render: {e}")),
443 })
444 }
445
446 fn read_hitmap(&mut self, width: u32, height: u32) {
450 let pick_target = self.pick_target.as_ref().expect("pick target ensured");
451 let row_bytes = (width as usize) * 4;
452 let row_px = width as usize;
453 let hitmap = self.hitmap.get_or_insert_with(Vec::new);
454 hitmap.clear();
455 hitmap.resize(row_px * height as usize, 0);
456 let swizzle = matches!(
461 pick_target.format,
462 wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
463 );
464 {
465 let data = pick_target.readback.slice(..).get_mapped_range();
466 let padded = pick_target.padded_bytes_per_row as usize;
467 for y in 0..height as usize {
468 let dst: &mut [u8] =
469 bytemuck::cast_slice_mut(&mut hitmap[y * row_px..(y + 1) * row_px]);
470 dst.copy_from_slice(&data[y * padded..y * padded + row_bytes]);
471 if swizzle {
472 for px in dst.chunks_exact_mut(4) {
473 px.swap(0, 2);
474 }
475 }
476 }
477 }
478 pick_target.readback.unmap();
479 self.hitmap_dims = Some((width, height));
480 }
481
482 fn settle_pending_pick(&mut self) -> Result<(), BackendError> {
491 if self.pick_pending.is_some() {
492 let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
493 self.try_finish_pick()?;
494 }
495 Ok(())
496 }
497
498 fn submit_pick_blocking(&mut self, width: u32, height: u32) -> Result<(), BackendError> {
506 let mut encoder = self
507 .device
508 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
509 label: Some("hephaestus.hybrid.pick"),
510 });
511 let pick_view = self
512 .pick_target
513 .as_ref()
514 .expect("pick target ensured")
515 .view
516 .clone();
517 self.rasterise(&mut encoder, Pass::Pick, &pick_view, width, height)?;
518 {
519 let pick_target = self.pick_target.as_ref().expect("pick target ensured");
520 copy_to_readback(&mut encoder, pick_target, width, height);
521 }
522 self.queue.submit(std::iter::once(encoder.finish()));
523
524 let pick_target = self.pick_target.as_ref().expect("pick target ensured");
525 let (tx, rx) = futures_intrusive::channel::shared::oneshot_channel();
526 pick_target
527 .readback
528 .slice(..)
529 .map_async(wgpu::MapMode::Read, move |res| {
530 let _ = tx.send(res);
531 });
532 let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
533 await_map(pollster::block_on(rx.receive()))?;
534 self.read_hitmap(width, height);
535 Ok(())
536 }
537
538 fn submit_pick(&mut self, width: u32, height: u32) -> Result<(), BackendError> {
543 self.ensure_pick_target(width, height);
544 let mut encoder = self
545 .device
546 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
547 label: Some("hephaestus.hybrid.pick"),
548 });
549 let pick_view = self
550 .pick_target
551 .as_ref()
552 .expect("pick target ensured")
553 .view
554 .clone();
555 self.rasterise(&mut encoder, Pass::Pick, &pick_view, width, height)?;
556 let pick_target = self.pick_target.as_ref().expect("pick target ensured");
557 copy_to_readback(&mut encoder, pick_target, width, height);
558 self.queue.submit(std::iter::once(encoder.finish()));
559
560 let slot = std::sync::Arc::new(std::sync::Mutex::new(None));
561 let sink = std::sync::Arc::clone(&slot);
562 pick_target
563 .readback
564 .slice(..)
565 .map_async(wgpu::MapMode::Read, move |res| {
566 if let Ok(mut guard) = sink.lock() {
567 *guard = Some(res);
568 }
569 });
570 self.pick_pending = Some(PendingPick {
571 slot,
572 width,
573 height,
574 });
575 Ok(())
576 }
577
578 pub fn try_finish_pick(&mut self) -> Result<bool, BackendError> {
589 let Some(pending) = self.pick_pending.as_ref() else {
590 return Ok(false);
591 };
592 let landed = pending
593 .slot
594 .lock()
595 .map_err(|_| BackendError::Readback("pick readback slot poisoned".into()))?
596 .take();
597 let Some(result) = landed else {
598 return Ok(false);
599 };
600 let PendingPick { width, height, .. } =
601 self.pick_pending.take().expect("checked just above");
602 result.map_err(|e| BackendError::Readback(e.to_string()))?;
603 self.read_hitmap(width, height);
604 Ok(true)
605 }
606
607 pub fn render_to_texture_deferring_pick(
616 &mut self,
617 view: &wgpu::TextureView,
618 width: u32,
619 height: u32,
620 background: Color,
621 ) -> Result<(), BackendError> {
622 let mut encoder = self
623 .device
624 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
625 label: Some("hephaestus.hybrid.render_to_texture"),
626 });
627 let format = self.target_format;
628 self.prepare(width, height, background, format, &mut encoder)?;
629 self.rasterise(&mut encoder, Pass::Display, view, width, height)?;
630 self.queue.submit(std::iter::once(encoder.finish()));
631
632 if self.refreshes_pick() {
633 self.try_finish_pick()?;
638 if self.pick_pending.is_none() {
642 self.submit_pick(width, height)?;
643 }
644 }
645 Ok(())
646 }
647
648 fn prepare(
651 &mut self,
652 width: u32,
653 height: u32,
654 background: Color,
655 format: wgpu::TextureFormat,
656 encoder: &mut wgpu::CommandEncoder,
657 ) -> Result<(), BackendError> {
658 if width == 0 || height == 0 {
659 return Err(BackendError::Other(
660 "cannot render a zero-sized frame".into(),
661 ));
662 }
663 self.ensure_sized(width, height, format)?;
664 self.upload_images(encoder)?;
665 self.replay(background, width, height, self.refresh_pick);
666 Ok(())
667 }
668}
669
670impl Renderer for HybridRenderer {
671 type Scene = HybridScene;
672
673 fn scene(&mut self) -> &mut Self::Scene {
674 &mut self.scene
675 }
676
677 fn render_to_buffer(
678 &mut self,
679 width: u32,
680 height: u32,
681 background: Color,
682 out: &mut [u8],
683 ) -> Result<(), BackendError> {
684 let expected = (width as usize) * (height as usize) * 4;
685 if out.len() != expected {
686 return Err(BackendError::BufferSize {
687 expected,
688 actual: out.len(),
689 });
690 }
691
692 let mut encoder = self
693 .device
694 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
695 label: Some("hephaestus.hybrid.render"),
696 });
697 self.prepare(
698 width,
699 height,
700 background,
701 wgpu::TextureFormat::Rgba8Unorm,
702 &mut encoder,
703 )?;
704 if self
705 .target
706 .as_ref()
707 .is_none_or(|t| t.width != width || t.height != height)
708 {
709 self.target = Some(Target::new(
710 &self.device,
711 width,
712 height,
713 wgpu::TextureFormat::Rgba8Unorm,
714 ));
715 }
716 let picking = self.refreshes_pick();
717 if picking {
718 self.settle_pending_pick()?;
719 self.ensure_pick_target(width, height);
720 }
721
722 let display_view = self.target.as_ref().expect("target ensured").view.clone();
723 self.rasterise(&mut encoder, Pass::Display, &display_view, width, height)?;
724 {
725 let target = self.target.as_ref().expect("target ensured");
726 copy_to_readback(&mut encoder, target, width, height);
727 }
728 self.queue.submit(std::iter::once(encoder.finish()));
735 if picking {
736 self.submit_pick_blocking(width, height)?;
737 }
738 let target = self.target.as_ref().expect("target ensured");
739
740 let display_slice = target.readback.slice(..);
741 let (display_tx, display_rx) = futures_intrusive::channel::shared::oneshot_channel();
742 display_slice.map_async(wgpu::MapMode::Read, move |res| {
743 let _ = display_tx.send(res);
744 });
745 let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
746 await_map(pollster::block_on(display_rx.receive()))?;
747
748 let row_bytes = (width as usize) * 4;
749 {
750 let data = display_slice.get_mapped_range();
751 let padded = target.padded_bytes_per_row as usize;
752 for y in 0..height as usize {
753 out[y * row_bytes..(y + 1) * row_bytes]
754 .copy_from_slice(&data[y * padded..y * padded + row_bytes]);
755 }
756 }
757 target.readback.unmap();
758 unpremultiply(out);
761 Ok(())
762 }
763}
764
765impl WgpuRenderer for HybridRenderer {
766 const REQUIRED_TARGET_USAGE: wgpu::TextureUsages = wgpu::TextureUsages::RENDER_ATTACHMENT;
767
768 const TARGET_IS_PREMULTIPLIED: bool = true;
769
770 fn render_to_texture(
771 &mut self,
772 view: &wgpu::TextureView,
773 width: u32,
774 height: u32,
775 background: Color,
776 ) -> Result<(), BackendError> {
777 let mut encoder = self
778 .device
779 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
780 label: Some("hephaestus.hybrid.render_to_texture"),
781 });
782 let format = self.target_format;
783 self.prepare(width, height, background, format, &mut encoder)?;
784 self.rasterise(&mut encoder, Pass::Display, view, width, height)?;
785 self.queue.submit(std::iter::once(encoder.finish()));
788
789 if self.refreshes_pick() {
790 self.settle_pending_pick()?;
791 self.ensure_pick_target(width, height);
792 self.submit_pick_blocking(width, height)?;
793 }
794 Ok(())
795 }
796}
797
798fn copy_to_readback(encoder: &mut wgpu::CommandEncoder, target: &Target, width: u32, height: u32) {
800 encoder.copy_texture_to_buffer(
801 wgpu::TexelCopyTextureInfo {
802 texture: &target.texture,
803 mip_level: 0,
804 origin: wgpu::Origin3d::ZERO,
805 aspect: wgpu::TextureAspect::All,
806 },
807 wgpu::TexelCopyBufferInfo {
808 buffer: &target.readback,
809 layout: wgpu::TexelCopyBufferLayout {
810 offset: 0,
811 bytes_per_row: Some(target.padded_bytes_per_row),
812 rows_per_image: Some(height),
813 },
814 },
815 wgpu::Extent3d {
816 width,
817 height,
818 depth_or_array_layers: 1,
819 },
820 );
821}
822
823fn await_map(res: Option<Result<(), wgpu::BufferAsyncError>>) -> Result<(), BackendError> {
825 match res {
826 Some(Ok(())) => Ok(()),
827 Some(Err(e)) => Err(BackendError::Readback(e.to_string())),
828 None => Err(BackendError::Readback("map_async sender dropped".into())),
829 }
830}