Skip to main content

damascene_ash/
runner.rs

1use std::collections::{HashMap, HashSet};
2use std::path::PathBuf;
3use std::sync::{Arc, Mutex};
4
5use ash::vk;
6use damascene_core::event::{KeyChord, KeyModifiers, LogicalKey, PhysicalKey, Pointer, UiEvent};
7use damascene_core::icons::svg::IconSource;
8use damascene_core::ir::TextAnchor;
9use damascene_core::paint::{IconRunKind, PaintItem, PhysicalScissor, QuadInstance};
10use damascene_core::runtime::{RecordedPaint, RunnerCore, TextRecorder};
11use damascene_core::shader::{ShaderHandle, StockShader, stock_wgsl};
12use damascene_core::state::{AnimationMode, UiState};
13use damascene_core::surface::SurfaceAlpha;
14use damascene_core::text::atlas::RunStyle;
15use damascene_core::theme::Theme;
16use damascene_core::tree::{Color, El, FontWeight, Rect, TextWrap};
17use damascene_core::vector::IconMaterial;
18use gpu_allocator::MemoryLocation;
19use gpu_allocator::vulkan::Allocator;
20use web_time::Instant;
21
22use crate::buffer::GpuBuffer;
23use crate::icon::IconPaint;
24use crate::image::{ImagePaint, ImageRecord};
25use crate::naga_compile::{CompileError, wgsl_to_spirv};
26use crate::pipeline::{FrameUniforms, build_quad_pipeline, build_quad_pipeline_from_spirv};
27use crate::scene::Scene3DPaint;
28use crate::surface::SurfacePaint;
29use crate::text::{TextPaint, TextRunKind};
30
31const INITIAL_INSTANCE_CAPACITY: usize = 256;
32const QUAD_VERTICES: [f32; 8] = [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0];
33
34pub type Result<T> = std::result::Result<T, Error>;
35
36/// Errors surfaced by the ash backend.
37#[derive(Debug)]
38pub enum Error {
39    ShaderCompile(CompileError),
40    Vulkan {
41        op: &'static str,
42        result: vk::Result,
43    },
44    Allocation(gpu_allocator::AllocationError),
45    AllocatorPoisoned,
46    BufferTooSmall {
47        requested: usize,
48        capacity: usize,
49    },
50    UnmappedAllocation,
51    ResourceDestroyed(&'static str),
52    IncompatibleTarget {
53        expected_format: vk::Format,
54        actual_format: vk::Format,
55        expected_sample_count: vk::SampleCountFlags,
56        actual_sample_count: vk::SampleCountFlags,
57    },
58    MissingPipeline(ShaderHandle),
59    PipelineCreationReturnedEmpty {
60        name: String,
61    },
62    Unsupported(&'static str),
63}
64
65impl std::fmt::Display for Error {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            Error::ShaderCompile(err) => err.fmt(f),
69            Error::Vulkan { op, result } => write!(f, "Vulkan error in {op}: {result:?}"),
70            Error::Allocation(err) => err.fmt(f),
71            Error::AllocatorPoisoned => write!(f, "Vulkan allocator mutex was poisoned"),
72            Error::BufferTooSmall {
73                requested,
74                capacity,
75            } => {
76                write!(
77                    f,
78                    "buffer upload requested {requested} bytes but capacity is {capacity} bytes"
79                )
80            }
81            Error::UnmappedAllocation => write!(f, "GPU allocation is not host-mapped"),
82            Error::ResourceDestroyed(name) => write!(f, "resource was already destroyed: {name}"),
83            Error::IncompatibleTarget {
84                expected_format,
85                actual_format,
86                expected_sample_count,
87                actual_sample_count,
88            } => write!(
89                f,
90                "render target is incompatible: expected {expected_format:?}/{expected_sample_count:?}, got {actual_format:?}/{actual_sample_count:?}"
91            ),
92            Error::MissingPipeline(handle) => {
93                write!(f, "no ash pipeline registered for {handle:?}")
94            }
95            Error::PipelineCreationReturnedEmpty { name } => {
96                write!(
97                    f,
98                    "Vulkan pipeline creation for `{name}` returned no pipeline"
99                )
100            }
101            Error::Unsupported(message) => write!(f, "{message}"),
102        }
103    }
104}
105
106impl std::error::Error for Error {
107    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
108        match self {
109            Error::ShaderCompile(err) => Some(err),
110            Error::Allocation(err) => Some(err),
111            Error::Unsupported(_) => None,
112            _ => None,
113        }
114    }
115}
116
117impl From<CompileError> for Error {
118    fn from(value: CompileError) -> Self {
119        Error::ShaderCompile(value)
120    }
121}
122
123impl From<vk::Result> for Error {
124    fn from(value: vk::Result) -> Self {
125        Error::Vulkan {
126            op: "vulkan call",
127            result: value,
128        }
129    }
130}
131
132impl From<gpu_allocator::AllocationError> for Error {
133    fn from(value: gpu_allocator::AllocationError) -> Self {
134        Error::Allocation(value)
135    }
136}
137
138/// Host-owned Vulkan context that `damascene-ash` borrows for resource
139/// creation and command recording.
140///
141/// The runner does not submit queues or own frame synchronization. The
142/// allocator is explicit because ash does not provide memory/resource
143/// ownership. The runner uses it for its internal buffers while leaving
144/// allocator construction and lifetime with the host.
145pub struct AshContext {
146    pub device: Arc<ash::Device>,
147    pub allocator: Arc<Mutex<Allocator>>,
148    pub queue_family_index: u32,
149    /// The selected physical device's `max_image_dimension2_d` limit.
150    /// `damascene-ash` holds only the logical device, so the caller —
151    /// which has the instance + physical device — must query
152    /// `get_physical_device_properties(...).limits.max_image_dimension2_d`
153    /// and pass it here. Raster-image uploads larger than this would
154    /// fail `vkCreateImage` validation, so they're downscaled to fit
155    /// before upload (issue #78).
156    pub max_image_dimension_2d: u32,
157}
158
159impl AshContext {
160    pub fn new(
161        device: Arc<ash::Device>,
162        allocator: Arc<Mutex<Allocator>>,
163        queue_family_index: u32,
164        max_image_dimension_2d: u32,
165    ) -> Self {
166        Self {
167            device,
168            allocator,
169            queue_family_index,
170            max_image_dimension_2d,
171        }
172    }
173}
174
175/// Pipeline/target compatibility information supplied at construction.
176#[derive(Clone, Copy, Debug)]
177pub struct TargetInfo {
178    pub format: vk::Format,
179    pub sample_count: vk::SampleCountFlags,
180}
181
182impl TargetInfo {
183    pub fn new(format: vk::Format) -> Self {
184        Self {
185            format,
186            sample_count: vk::SampleCountFlags::TYPE_1,
187        }
188    }
189
190    pub fn with_sample_count(mut self, sample_count: vk::SampleCountFlags) -> Self {
191        self.sample_count = sample_count;
192        self
193    }
194}
195
196/// A host-owned image/view pair Damascene can render into.
197#[derive(Clone, Copy, Debug)]
198pub struct AshRenderTarget {
199    pub image: vk::Image,
200    pub view: vk::ImageView,
201    pub format: vk::Format,
202    pub extent: vk::Extent2D,
203    pub sample_count: vk::SampleCountFlags,
204    /// Layout the host guarantees before Damascene records its commands.
205    pub initial_layout: vk::ImageLayout,
206    /// Layout Damascene should leave the image in after rendering.
207    pub final_layout: vk::ImageLayout,
208}
209
210impl AshRenderTarget {
211    pub fn color_attachment(
212        image: vk::Image,
213        view: vk::ImageView,
214        format: vk::Format,
215        extent: vk::Extent2D,
216    ) -> Self {
217        Self {
218            image,
219            view,
220            format,
221            extent,
222            sample_count: vk::SampleCountFlags::TYPE_1,
223            initial_layout: vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
224            final_layout: vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
225        }
226    }
227}
228
229/// Load operation for the first Damascene-owned rendering scope.
230#[derive(Clone, Copy, Debug)]
231pub enum LoadOp {
232    Clear([f32; 4]),
233    Load,
234}
235
236/// Lightweight diagnostic returned by [`Runner::prepared_frame`].
237#[derive(Clone, Copy, Debug, Default)]
238pub struct PreparedFrame {
239    pub quad_instances: usize,
240    pub paint_items: usize,
241    pub runs: usize,
242}
243
244/// Ash runtime owned by a custom host. One runner per compatible target.
245pub struct Runner {
246    context: AshContext,
247    target: TargetInfo,
248    frame_buf: GpuBuffer,
249    quad_vbo: GpuBuffer,
250    instance_buf: GpuBuffer,
251    instance_capacity: usize,
252    descriptor_set_layout: vk::DescriptorSetLayout,
253    descriptor_pool: vk::DescriptorPool,
254    descriptor_set: vk::DescriptorSet,
255    pipeline_layout: vk::PipelineLayout,
256    pipelines: HashMap<ShaderHandle, vk::Pipeline>,
257    text_paint: TextPaint,
258    icon_paint: IconPaint,
259    image_paint: ImagePaint,
260    surface_paint: SurfacePaint,
261    scene_paint: Scene3DPaint,
262    backdrop_shaders: HashSet<&'static str>,
263    time_shaders: HashSet<&'static str>,
264    start_time: Instant,
265
266    /// Output white-level scale written into `FrameUniforms.white_scale`.
267    /// 1.0 on SDR targets; a host driving a Windows-scRGB swapchain sets
268    /// 203/80 so UI white lands at the encoding's assumed reference
269    /// white. See docs/COLOR_MANAGEMENT.md.
270    white_scale: f32,
271    /// Output luminance headroom (`target_max / reference`, 1.0 on SDR)
272    /// and reference white in cd/m², written into
273    /// `FrameUniforms.headroom/ref_nits` and (headroom) mirrored into
274    /// the image paint for the per-image HDR remaster. See
275    /// [`Self::set_output_luminance`].
276    headroom: f32,
277    ref_nits: f32,
278    core: RunnerCore,
279}
280
281impl Runner {
282    pub fn new(context: AshContext, target: TargetInfo) -> Result<Self> {
283        let (frame_buf, quad_vbo, instance_buf) = {
284            let mut allocator = context
285                .allocator
286                .lock()
287                .map_err(|_| Error::AllocatorPoisoned)?;
288            let frame_buf = GpuBuffer::new(
289                &context.device,
290                &mut allocator,
291                "damascene_ash::frame_uniforms",
292                std::mem::size_of::<FrameUniforms>() as vk::DeviceSize,
293                vk::BufferUsageFlags::UNIFORM_BUFFER,
294                MemoryLocation::CpuToGpu,
295            )?;
296            let mut quad_vbo = GpuBuffer::new(
297                &context.device,
298                &mut allocator,
299                "damascene_ash::quad_vbo",
300                std::mem::size_of_val(&QUAD_VERTICES) as vk::DeviceSize,
301                vk::BufferUsageFlags::VERTEX_BUFFER,
302                MemoryLocation::CpuToGpu,
303            )?;
304            quad_vbo.write_bytes(bytemuck::cast_slice(&QUAD_VERTICES))?;
305            let instance_buf = GpuBuffer::new(
306                &context.device,
307                &mut allocator,
308                "damascene_ash::instance_buf",
309                (INITIAL_INSTANCE_CAPACITY * std::mem::size_of::<QuadInstance>()) as vk::DeviceSize,
310                vk::BufferUsageFlags::VERTEX_BUFFER,
311                MemoryLocation::CpuToGpu,
312            )?;
313            (frame_buf, quad_vbo, instance_buf)
314        };
315
316        let descriptor_set_layout = create_frame_descriptor_set_layout(&context.device)?;
317        let descriptor_pool = create_descriptor_pool(&context.device)?;
318        let descriptor_set =
319            allocate_frame_descriptor_set(&context.device, descriptor_pool, descriptor_set_layout)?;
320        update_frame_descriptor_set(&context.device, descriptor_set, frame_buf.buffer);
321        let pipeline_layout = create_pipeline_layout(&context.device, descriptor_set_layout)?;
322        let text_paint = {
323            let mut allocator = context
324                .allocator
325                .lock()
326                .map_err(|_| Error::AllocatorPoisoned)?;
327            TextPaint::new(
328                &context.device,
329                &mut allocator,
330                descriptor_set_layout,
331                target,
332            )?
333        };
334        let icon_paint = {
335            let mut allocator = context
336                .allocator
337                .lock()
338                .map_err(|_| Error::AllocatorPoisoned)?;
339            IconPaint::new(
340                &context.device,
341                &mut allocator,
342                descriptor_set_layout,
343                target,
344            )?
345        };
346        let image_paint = {
347            let mut allocator = context
348                .allocator
349                .lock()
350                .map_err(|_| Error::AllocatorPoisoned)?;
351            ImagePaint::new(
352                &context.device,
353                &mut allocator,
354                descriptor_set_layout,
355                target,
356                context.max_image_dimension_2d,
357            )?
358        };
359        let surface_paint = {
360            let mut allocator = context
361                .allocator
362                .lock()
363                .map_err(|_| Error::AllocatorPoisoned)?;
364            SurfacePaint::new(
365                &context.device,
366                &mut allocator,
367                descriptor_set_layout,
368                target,
369            )?
370        };
371        let scene_paint = {
372            let mut allocator = context
373                .allocator
374                .lock()
375                .map_err(|_| Error::AllocatorPoisoned)?;
376            Scene3DPaint::new(
377                &context.device,
378                &mut allocator,
379                descriptor_set_layout,
380                target,
381                damascene_core::paint::DEFAULT_WORKING_COLOR_SPACE,
382            )?
383        };
384
385        let mut runner = Self {
386            context,
387            target,
388            frame_buf,
389            quad_vbo,
390            instance_buf,
391            instance_capacity: INITIAL_INSTANCE_CAPACITY,
392            descriptor_set_layout,
393            descriptor_pool,
394            descriptor_set,
395            pipeline_layout,
396            pipelines: HashMap::new(),
397            text_paint,
398            icon_paint,
399            image_paint,
400            surface_paint,
401            scene_paint,
402            backdrop_shaders: HashSet::new(),
403            time_shaders: HashSet::new(),
404            start_time: Instant::now(),
405            white_scale: 1.0,
406            headroom: 1.0,
407            ref_nits: damascene_core::color::BT2408_REFERENCE_WHITE_NITS,
408            core: RunnerCore::new(),
409        };
410        runner.register_stock_shaders()?;
411        Ok(runner)
412    }
413
414    pub fn target(&self) -> TargetInfo {
415        self.target
416    }
417
418    pub fn device(&self) -> &ash::Device {
419        &self.context.device
420    }
421
422    pub fn queue_family_index(&self) -> u32 {
423        self.context.queue_family_index
424    }
425
426    pub fn set_surface_size(&mut self, width: u32, height: u32) {
427        self.core.set_surface_size(width, height);
428    }
429
430    /// Set the color space the renderer composites in. Hosts call this
431    /// once after negotiating a swapchain format with the display
432    /// server and before the first frame. Updates the shared quad path
433    /// (via `RunnerCore`) and this backend's text / icon / image /
434    /// scene color recorders so every color crosses the working-space
435    /// boundary consistently.
436    ///
437    /// The working space must match how the swapchain interprets the
438    /// pixels the renderer writes: `SRGB_LINEAR` for an `*_SRGB`
439    /// format (the default), `SCRGB_LINEAR` for
440    /// `R16G16B16A16_SFLOAT` + `EXTENDED_SRGB_LINEAR_EXT`, etc.
441    pub fn set_working_color_space(&mut self, space: damascene_core::color::ColorSpace) {
442        self.core.set_working_color_space(space);
443        self.text_paint.set_working_color_space(space);
444        self.icon_paint.set_working_color_space(space);
445        self.image_paint.set_working_color_space(space);
446        self.scene_paint.set_working_color_space(space);
447    }
448
449    /// The color space the renderer currently composites in.
450    pub fn working_color_space(&self) -> damascene_core::color::ColorSpace {
451        self.core.working_color_space()
452    }
453
454    /// Set the output white-level scale (default 1.0) written into
455    /// `FrameUniforms.white_scale`. Hosts driving a Windows-scRGB
456    /// (`R16G16B16A16_SFLOAT`) swapchain pass
457    /// `damascene_core::color::WINDOWS_SCRGB_WHITE_SCALE` so SDR-referred UI
458    /// white lands at the encoding's assumed reference white (203 cd/m²)
459    /// instead of 80 cd/m². See docs/COLOR_MANAGEMENT.md.
460    pub fn set_white_scale(&mut self, scale: f32) {
461        self.white_scale = scale;
462    }
463
464    /// Set the output's luminance frame: `headroom` = usable range in
465    /// multiples of reference white (`target_max / reference`; 1.0 on
466    /// SDR — the default — or `f32::INFINITY` when the output declared
467    /// no maximum) and `reference_nits` = the output's reference white
468    /// in cd/m² (default 203, BT.2408). Feeds
469    /// `FrameUniforms.headroom/ref_nits` and the per-image HDR
470    /// remaster: image draws whose measured content peak exceeds their
471    /// [`damascene_core::image::DynamicRangeLimit`] resolved against
472    /// this headroom are rolled off (BT.2390) to fit. Hosts re-call
473    /// this whenever the output's preferred description changes.
474    pub fn set_output_luminance(&mut self, headroom: f32, reference_nits: f32) {
475        self.headroom = headroom;
476        self.ref_nits = reference_nits;
477        self.image_paint.set_headroom(headroom);
478    }
479
480    pub fn set_theme(&mut self, theme: Theme) {
481        self.icon_paint.set_material(theme.icon_material());
482        self.core.set_theme(theme);
483    }
484
485    pub fn theme(&self) -> &Theme {
486        self.core.theme()
487    }
488
489    /// Select the stock material used by the vector-icon painter.
490    pub fn set_icon_material(&mut self, material: IconMaterial) {
491        self.icon_paint.set_material(material);
492    }
493
494    pub fn icon_material(&self) -> IconMaterial {
495        self.icon_paint.material()
496    }
497
498    pub fn register_shader(&mut self, name: &'static str, wgsl: &str) -> Result<()> {
499        self.register_shader_with(name, wgsl, false, false)
500    }
501
502    pub fn register_shader_with(
503        &mut self,
504        name: &'static str,
505        wgsl: &str,
506        samples_backdrop: bool,
507        samples_time: bool,
508    ) -> Result<()> {
509        if samples_backdrop {
510            return Err(Error::Unsupported(
511                "damascene-ash backdrop-sampling custom shaders are not implemented yet",
512            ));
513        }
514        let words = wgsl_to_spirv(name, wgsl)?;
515        let pipeline = build_quad_pipeline_from_spirv(
516            &self.context.device,
517            self.pipeline_layout,
518            self.target,
519            name,
520            &words,
521        )?;
522        if let Some(old) = self.pipelines.insert(ShaderHandle::Custom(name), pipeline) {
523            unsafe {
524                self.context.device.destroy_pipeline(old, None);
525            }
526        }
527        self.backdrop_shaders.remove(name);
528        if samples_time {
529            self.time_shaders.insert(name);
530        } else {
531            self.time_shaders.remove(name);
532        }
533        // Named uniform routing + Rust↔WGSL drift detection (issue
534        // #99); failure is non-fatal — positional vec_a..vec_e routing
535        // still works.
536        match damascene_core::paint::slots::introspect_wgsl(wgsl) {
537            Ok(map) => self.core.register_shader_slots(name, map),
538            Err(e) => log::warn!(
539                "damascene-ash: could not introspect shader `{name}` for named uniform \
540                 routing ({e}); positional vec_a..vec_e routing still applies"
541            ),
542        }
543        Ok(())
544    }
545
546    pub fn ui_state(&self) -> &UiState {
547        self.core.ui_state()
548    }
549
550    pub fn debug_summary(&self) -> String {
551        self.core.debug_summary()
552    }
553
554    pub fn rect_of_key(&self, key: &str) -> Option<Rect> {
555        self.core.rect_of_key(key)
556    }
557
558    /// Pointer cursor resolved from the snapshot tree `prepare` just
559    /// stored. Call after `prepare`; paint-only frames keep the
560    /// previously resolved cursor.
561    pub fn snapshot_cursor(&self) -> damascene_core::cursor::Cursor {
562        self.core.snapshot_cursor()
563    }
564
565    pub fn prepared_frame(&self) -> PreparedFrame {
566        PreparedFrame {
567            quad_instances: self.core.quad_scratch.len(),
568            paint_items: self.core.paint_items.len(),
569            runs: self.core.runs.len(),
570        }
571    }
572
573    /// Lay out the tree and lower it into this frame's paint data.
574    ///
575    /// # Synchronization
576    ///
577    /// `prepare` writes persistently-mapped vertex/uniform buffers and
578    /// immediately destroys GPU resources it evicts or regrows
579    /// (instance buffers, scene geometry, cached textures, descriptor
580    /// sets). It does **not** fence-gate any of that, so the host must
581    /// guarantee that every previously submitted command buffer that
582    /// references this runner's resources has completed before calling
583    /// it — in a classic frames-in-flight loop, wait the frame fence
584    /// *before* `prepare`, not just before recording.
585    pub fn prepare(
586        &mut self,
587        mut root: El,
588        viewport: Rect,
589        scale_factor: f32,
590    ) -> damascene_core::runtime::PrepareResult {
591        let mut timings = damascene_core::runtime::PrepareTimings::default();
592
593        // Install any scene depth maps the previous frame's capture finished
594        // reading back, so this frame's `draw_ops` can occlude scene-anchored
595        // labels behind geometry. Done before `prepare_layout`; stale maps for
596        // scenes that left the tree are GC'd.
597        let ready_depth = self.scene_paint.collect_depth_maps();
598        if !ready_depth.is_empty() {
599            let depth_maps = self.core.ui_state.scene_depth_mut();
600            for (id, map) in ready_depth {
601                depth_maps.insert(id, map);
602            }
603        }
604        self.core
605            .ui_state
606            .scene_depth_mut()
607            .retain(|id, _| self.scene_paint.has_target(id));
608
609        let time_shaders = &self.time_shaders;
610        let damascene_core::runtime::LayoutPrepared {
611            ops,
612            mut needs_redraw,
613            mut next_layout_redraw_in,
614            next_paint_redraw_in,
615        } = self.core.prepare_layout(
616            &mut root,
617            viewport,
618            scale_factor,
619            &mut timings,
620            |handle| matches!(handle, ShaderHandle::Custom(name) if time_shaders.contains(name)),
621        );
622
623        self.text_paint.frame_begin();
624        self.icon_paint.frame_begin();
625        self.image_paint.frame_begin();
626        self.surface_paint.frame_begin();
627        self.scene_paint.frame_begin();
628        let pipelines = &self.pipelines;
629        let backdrop_shaders = &self.backdrop_shaders;
630        {
631            let mut allocator = self
632                .context
633                .allocator
634                .lock()
635                .expect("damascene-ash: allocator mutex poisoned during paint recording");
636            let mut recorder = PaintRecorder {
637                text: &mut self.text_paint,
638                icons: &mut self.icon_paint,
639                images: &mut self.image_paint,
640                surfaces: &mut self.surface_paint,
641                scenes: &mut self.scene_paint,
642                device: &self.context.device,
643                allocator: &mut allocator,
644            };
645            self.core.prepare_paint(
646                &ops,
647                |shader| match shader {
648                    ShaderHandle::Stock(StockShader::RoundedRect)
649                    | ShaderHandle::Stock(StockShader::Spinner)
650                    | ShaderHandle::Stock(StockShader::Skeleton)
651                    | ShaderHandle::Stock(StockShader::ProgressIndeterminate) => {
652                        pipelines.contains_key(shader)
653                    }
654                    ShaderHandle::Custom(_) => pipelines.contains_key(shader),
655                    _ => false,
656                },
657                |shader| matches!(shader, ShaderHandle::Custom(name) if backdrop_shaders.contains(name)),
658                &mut recorder,
659                scale_factor,
660                &mut timings,
661            );
662        }
663        let t_gpu_start = Instant::now();
664        self.upload_frame_data(viewport, scale_factor)
665            .expect("damascene-ash: failed to upload prepared frame data");
666        timings.gpu_upload = Instant::now() - t_gpu_start;
667
668        self.core.snapshot_owned(root, &mut timings);
669        self.core.last_ops = ops;
670
671        // Keep frames coming until every labelled scene has a depth map for its
672        // current pose — the async-ish read-back needs a couple of frames, and
673        // a capture started in `render` would sit unread after the camera
674        // settles. Drive the *layout* lane (the paint-only `repaint` path skips
675        // `collect_depth_maps`), not just `needs_redraw`. Settled + current
676        // scenes report `false`, so lazy idle is preserved. Mirrors wgpu/vulkano.
677        if self.scene_paint.occlusion_unsettled() {
678            needs_redraw = true;
679            next_layout_redraw_in = Some(std::time::Duration::ZERO);
680        }
681
682        let next_redraw_in = match (next_layout_redraw_in, next_paint_redraw_in) {
683            (Some(a), Some(b)) => Some(a.min(b)),
684            (a, b) => a.or(b),
685        };
686
687        damascene_core::runtime::PrepareResult {
688            needs_redraw,
689            next_redraw_in,
690            next_layout_redraw_in,
691            next_paint_redraw_in,
692            timings,
693        }
694    }
695
696    pub fn repaint(
697        &mut self,
698        _viewport: Rect,
699        scale_factor: f32,
700    ) -> damascene_core::runtime::PrepareResult {
701        let mut timings = damascene_core::runtime::PrepareTimings::default();
702        let time_shaders = &self.time_shaders;
703        self.text_paint.frame_begin();
704        self.icon_paint.frame_begin();
705        self.image_paint.frame_begin();
706        self.surface_paint.frame_begin();
707        self.scene_paint.frame_begin();
708        let pipelines = &self.pipelines;
709        let backdrop_shaders = &self.backdrop_shaders;
710        {
711            let mut allocator = self
712                .context
713                .allocator
714                .lock()
715                .expect("damascene-ash: allocator mutex poisoned during paint recording");
716            let mut recorder = PaintRecorder {
717                text: &mut self.text_paint,
718                icons: &mut self.icon_paint,
719                images: &mut self.image_paint,
720                surfaces: &mut self.surface_paint,
721                scenes: &mut self.scene_paint,
722                device: &self.context.device,
723                allocator: &mut allocator,
724            };
725            self.core.prepare_paint_cached(
726                |shader| match shader {
727                    ShaderHandle::Stock(StockShader::RoundedRect)
728                    | ShaderHandle::Stock(StockShader::Spinner)
729                    | ShaderHandle::Stock(StockShader::Skeleton)
730                    | ShaderHandle::Stock(StockShader::ProgressIndeterminate) => {
731                        pipelines.contains_key(shader)
732                    }
733                    ShaderHandle::Custom(_) => pipelines.contains_key(shader),
734                    _ => false,
735                },
736                |shader| matches!(shader, ShaderHandle::Custom(name) if backdrop_shaders.contains(name)),
737                &mut recorder,
738                scale_factor,
739                &mut timings,
740            );
741        }
742        let next_paint_redraw_in = self.core.scan_continuous_shaders(
743            |handle| matches!(handle, ShaderHandle::Custom(name) if time_shaders.contains(name)),
744        );
745        self.upload_frame_data(_viewport, scale_factor)
746            .expect("damascene-ash: failed to upload repainted frame data");
747        damascene_core::runtime::PrepareResult {
748            needs_redraw: next_paint_redraw_in.is_some(),
749            next_redraw_in: next_paint_redraw_in,
750            next_layout_redraw_in: None,
751            next_paint_redraw_in,
752            timings,
753        }
754    }
755
756    /// Record Damascene draw commands into a host-opened dynamic-rendering scope.
757    ///
758    /// **3D scenes need the pre-pass.** `Scene3D` paint items composite
759    /// from offscreen targets that must be rendered before the host's
760    /// rendering scope begins — call [`Self::record_scene_prepass`]
761    /// first (outside the scope, after [`Self::record_uploads`]), or
762    /// every scene in the frame samples a never-rendered target and
763    /// composites blank.
764    ///
765    /// # Safety
766    ///
767    /// The command buffer must be recording, graphics-compatible, and
768    /// inside a dynamic-rendering scope whose color attachment format
769    /// and sample count match this runner's target. The host is
770    /// responsible for synchronization and image layouts. Classic
771    /// render-pass/framebuffer compatibility is not wired yet.
772    pub unsafe fn draw(&self, _cmd: vk::CommandBuffer) -> Result<()> {
773        unsafe { self.draw_items(_cmd, &self.core.paint_items) }
774    }
775
776    /// Record the offscreen pre-pass for any 3D scenes in this frame's
777    /// paint stream: each `Scene3D` renders into its own offscreen
778    /// target, and label-bearing scenes capture depth for next frame's
779    /// label occlusion. No-op when the frame has no scenes.
780    ///
781    /// Hosts using [`Self::render`] do not need to call this directly.
782    /// Hosts using [`Self::draw`] should call it after
783    /// [`Self::record_uploads`] and before beginning their
784    /// dynamic-rendering scope.
785    ///
786    /// # Safety
787    ///
788    /// The command buffer must be recording and outside a render pass
789    /// or dynamic-rendering scope (the pre-pass opens and closes its
790    /// own scopes).
791    pub unsafe fn record_scene_prepass(&mut self, cmd: vk::CommandBuffer) {
792        if self.scene_paint.has_runs() {
793            unsafe {
794                self.scene_paint.encode_offscreen(&self.context.device, cmd);
795                // Capture each label-bearing scene's depth into its
796                // read-back buffer (the depth is still stored from the
797                // pass above). The CPU read happens next frame in
798                // `prepare`, after the fence.
799                self.scene_paint
800                    .encode_depth_capture(&self.context.device, cmd);
801            }
802        }
803    }
804
805    /// Record pending atlas/texture uploads before drawing.
806    ///
807    /// Hosts using [`Self::render`] do not need to call this directly;
808    /// `render` records uploads before opening its dynamic-rendering scope.
809    /// Hosts using [`Self::draw`] inside their own rendering scope should
810    /// call this after `prepare`/`repaint` and before beginning rendering.
811    ///
812    /// # Safety
813    ///
814    /// The command buffer must be recording and outside a render pass or
815    /// dynamic-rendering scope. The host must keep submitted work ordered
816    /// so retained staging buffers are not dropped before the copy commands
817    /// complete.
818    pub unsafe fn record_uploads(&mut self, cmd: vk::CommandBuffer) -> Result<()> {
819        unsafe {
820            self.text_paint
821                .record_pending_uploads(&self.context.device, cmd)?;
822            self.icon_paint
823                .record_pending_uploads(&self.context.device, cmd)?;
824            self.image_paint
825                .record_pending_uploads(&self.context.device, cmd);
826        }
827        Ok(())
828    }
829
830    /// Record Damascene-owned rendering into `target`.
831    ///
832    /// # Safety
833    ///
834    /// The command buffer must be recording and graphics-compatible.
835    /// The target image/view must be valid for color attachment use,
836    /// with layouts and synchronization matching `AshRenderTarget`.
837    pub unsafe fn render(
838        &mut self,
839        _cmd: vk::CommandBuffer,
840        _target: AshRenderTarget,
841        _load: LoadOp,
842    ) -> Result<()> {
843        if _target.format != self.target.format || _target.sample_count != self.target.sample_count
844        {
845            return Err(Error::IncompatibleTarget {
846                expected_format: self.target.format,
847                actual_format: _target.format,
848                expected_sample_count: self.target.sample_count,
849                actual_sample_count: _target.sample_count,
850            });
851        }
852
853        let (load_op, clear_value) = match _load {
854            LoadOp::Clear(color) => (
855                vk::AttachmentLoadOp::CLEAR,
856                vk::ClearValue {
857                    color: vk::ClearColorValue { float32: color },
858                },
859            ),
860            LoadOp::Load => (vk::AttachmentLoadOp::LOAD, vk::ClearValue::default()),
861        };
862        unsafe {
863            self.record_uploads(_cmd)?;
864            // 3D scenes render into their own offscreen targets first, ahead
865            // of the main rendering scope (scopes can't nest), leaving each
866            // resolved image ready to sample for the composite below.
867            self.record_scene_prepass(_cmd);
868            transition_color_target(
869                &self.context.device,
870                _cmd,
871                _target.image,
872                _target.initial_layout,
873                vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
874            );
875        }
876
877        let color_attachment = vk::RenderingAttachmentInfo::default()
878            .image_view(_target.view)
879            .image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
880            .load_op(load_op)
881            .store_op(vk::AttachmentStoreOp::STORE)
882            .clear_value(clear_value);
883        let color_attachments = [color_attachment];
884        let rendering_info = vk::RenderingInfo::default()
885            .render_area(vk::Rect2D {
886                offset: vk::Offset2D { x: 0, y: 0 },
887                extent: _target.extent,
888            })
889            .layer_count(1)
890            .color_attachments(&color_attachments);
891
892        unsafe {
893            self.context
894                .device
895                .cmd_begin_rendering(_cmd, &rendering_info);
896            self.draw_items(_cmd, &self.core.paint_items)?;
897            self.context.device.cmd_end_rendering(_cmd);
898            transition_color_target(
899                &self.context.device,
900                _cmd,
901                _target.image,
902                vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
903                _target.final_layout,
904            );
905        }
906        Ok(())
907    }
908
909    pub fn pointer_moved(&mut self, p: Pointer) -> damascene_core::runtime::PointerMove {
910        self.core.pointer_moved(p)
911    }
912
913    pub fn pointer_left(&mut self) -> Vec<UiEvent> {
914        self.core.pointer_left()
915    }
916
917    pub fn pointer_cancelled(&mut self) -> Vec<UiEvent> {
918        self.core.pointer_cancelled()
919    }
920
921    pub fn file_hovered(&mut self, path: PathBuf, x: f32, y: f32) -> Vec<UiEvent> {
922        self.core.file_hovered(path, x, y)
923    }
924
925    pub fn file_hover_cancelled(&mut self) -> Vec<UiEvent> {
926        self.core.file_hover_cancelled()
927    }
928
929    pub fn file_dropped(&mut self, path: PathBuf, x: f32, y: f32) -> Vec<UiEvent> {
930        self.core.file_dropped(path, x, y)
931    }
932
933    pub fn would_press_focus_text_input(&self, x: f32, y: f32) -> bool {
934        self.core.would_press_focus_text_input(x, y)
935    }
936
937    pub fn focused_captures_keys(&self) -> bool {
938        self.core.focused_captures_keys()
939    }
940
941    pub fn pointer_down(&mut self, p: Pointer) -> Vec<UiEvent> {
942        self.core.pointer_down(p)
943    }
944
945    pub fn pointer_up(&mut self, p: Pointer) -> Vec<UiEvent> {
946        self.core.pointer_up(p)
947    }
948
949    pub fn set_modifiers(&mut self, modifiers: KeyModifiers) {
950        self.core.ui_state.set_modifiers(modifiers);
951    }
952
953    pub fn key_down(
954        &mut self,
955        logical: LogicalKey,
956        physical: PhysicalKey,
957        modifiers: KeyModifiers,
958        repeat: bool,
959    ) -> Vec<UiEvent> {
960        self.core.key_down(logical, physical, modifiers, repeat)
961    }
962
963    pub fn text_input(&mut self, text: String) -> Option<UiEvent> {
964        self.core.text_input(text)
965    }
966
967    pub fn set_hotkeys(&mut self, hotkeys: Vec<(KeyChord, String)>) {
968        self.core.set_hotkeys(hotkeys);
969    }
970
971    pub fn set_selection(&mut self, selection: damascene_core::selection::Selection) {
972        self.core.set_selection(selection);
973    }
974
975    pub fn selected_text(&self) -> Option<String> {
976        self.core.selected_text()
977    }
978
979    pub fn selected_text_for(
980        &self,
981        selection: &damascene_core::selection::Selection,
982    ) -> Option<String> {
983        self.core.selected_text_for(selection)
984    }
985
986    pub fn push_toasts(&mut self, specs: Vec<damascene_core::toast::ToastSpec>) {
987        self.core.push_toasts(specs);
988    }
989
990    pub fn dismiss_toast(&mut self, id: u64) {
991        self.core.dismiss_toast(id);
992    }
993
994    pub fn push_focus_requests(&mut self, keys: Vec<String>) {
995        self.core.push_focus_requests(keys);
996    }
997
998    pub fn push_scroll_requests(&mut self, requests: Vec<damascene_core::scroll::ScrollRequest>) {
999        self.core.push_scroll_requests(requests);
1000    }
1001
1002    pub fn push_viewport_requests(
1003        &mut self,
1004        requests: Vec<damascene_core::viewport::ViewportRequest>,
1005    ) {
1006        self.core.push_viewport_requests(requests);
1007    }
1008
1009    pub fn push_plot_requests(&mut self, requests: Vec<damascene_core::plot::PlotRequest>) {
1010        self.core.push_plot_requests(requests);
1011    }
1012
1013    pub fn set_animation_mode(&mut self, mode: AnimationMode) {
1014        self.core.set_animation_mode(mode);
1015    }
1016
1017    pub fn pointer_wheel(&mut self, x: f32, y: f32, dy: f32) -> bool {
1018        self.core.pointer_wheel(x, y, dy)
1019    }
1020
1021    pub fn pointer_wheel_event(&mut self, x: f32, y: f32, dx: f32, dy: f32) -> Option<UiEvent> {
1022        self.core.pointer_wheel_event(x, y, dx, dy)
1023    }
1024
1025    pub fn poll_input(&mut self, now: Instant) -> Vec<UiEvent> {
1026        self.core.poll_input(now)
1027    }
1028
1029    fn upload_frame_data(&mut self, viewport: Rect, scale_factor: f32) -> Result<()> {
1030        self.ensure_instance_capacity(self.core.quad_scratch.len())?;
1031        let frame = FrameUniforms {
1032            viewport: [viewport.w, viewport.h],
1033            time: (Instant::now() - self.start_time).as_secs_f32(),
1034            scale_factor,
1035            white_scale: self.white_scale,
1036            headroom: self.headroom,
1037            ref_nits: self.ref_nits,
1038            _reserved: 0.0,
1039        };
1040        self.frame_buf.write_bytes(bytemuck::bytes_of(&frame))?;
1041        self.instance_buf
1042            .write_bytes(bytemuck::cast_slice(&self.core.quad_scratch))?;
1043        let mut allocator = self
1044            .context
1045            .allocator
1046            .lock()
1047            .map_err(|_| Error::AllocatorPoisoned)?;
1048        self.text_paint
1049            .flush(&self.context.device, &mut allocator)?;
1050        self.icon_paint
1051            .flush(&self.context.device, &mut allocator)?;
1052        self.image_paint
1053            .flush(&self.context.device, &mut allocator)?;
1054        self.surface_paint
1055            .flush(&self.context.device, &mut allocator)?;
1056        self.scene_paint
1057            .flush(&self.context.device, &mut allocator)?;
1058        Ok(())
1059    }
1060
1061    fn ensure_instance_capacity(&mut self, instances: usize) -> Result<()> {
1062        if instances <= self.instance_capacity {
1063            return Ok(());
1064        }
1065        let mut next = self.instance_capacity.max(1);
1066        while next < instances {
1067            next *= 2;
1068        }
1069        let mut allocator = self
1070            .context
1071            .allocator
1072            .lock()
1073            .map_err(|_| Error::AllocatorPoisoned)?;
1074        unsafe {
1075            self.instance_buf
1076                .destroy(&self.context.device, &mut allocator);
1077        }
1078        self.instance_buf = GpuBuffer::new(
1079            &self.context.device,
1080            &mut allocator,
1081            "damascene_ash::instance_buf",
1082            (next * std::mem::size_of::<QuadInstance>()) as vk::DeviceSize,
1083            vk::BufferUsageFlags::VERTEX_BUFFER,
1084            MemoryLocation::CpuToGpu,
1085        )?;
1086        self.instance_capacity = next;
1087        Ok(())
1088    }
1089
1090    unsafe fn draw_items(&self, cmd: vk::CommandBuffer, items: &[PaintItem]) -> Result<()> {
1091        let full = PhysicalScissor {
1092            x: 0,
1093            y: 0,
1094            w: self.core.viewport_px.0,
1095            h: self.core.viewport_px.1,
1096        };
1097        for item in items {
1098            match *item {
1099                PaintItem::QuadRun(index) => {
1100                    let run = &self.core.runs[index];
1101                    let pipeline = self
1102                        .pipelines
1103                        .get(&run.handle)
1104                        .ok_or(Error::MissingPipeline(run.handle))?;
1105                    unsafe {
1106                        self.context.device.cmd_bind_pipeline(
1107                            cmd,
1108                            vk::PipelineBindPoint::GRAPHICS,
1109                            *pipeline,
1110                        );
1111                        self.set_viewport(cmd);
1112                        self.set_scissor(cmd, run.scissor, full);
1113                        self.context.device.cmd_bind_descriptor_sets(
1114                            cmd,
1115                            vk::PipelineBindPoint::GRAPHICS,
1116                            self.pipeline_layout,
1117                            0,
1118                            &[self.descriptor_set],
1119                            &[],
1120                        );
1121                        self.context.device.cmd_bind_vertex_buffers(
1122                            cmd,
1123                            0,
1124                            &[self.quad_vbo.buffer, self.instance_buf.buffer],
1125                            &[0, 0],
1126                        );
1127                        self.context
1128                            .device
1129                            .cmd_draw(cmd, 4, run.count, 0, run.first);
1130                    }
1131                }
1132                PaintItem::Text(index) => {
1133                    let run = self.text_paint.run(index);
1134                    let pipeline = self.text_paint.pipeline_for(run.kind);
1135                    let layout = self.text_paint.pipeline_layout_for(run.kind);
1136                    unsafe {
1137                        self.context.device.cmd_bind_pipeline(
1138                            cmd,
1139                            vk::PipelineBindPoint::GRAPHICS,
1140                            pipeline,
1141                        );
1142                        self.set_viewport(cmd);
1143                        self.set_scissor(cmd, run.scissor, full);
1144                        match run.kind {
1145                            TextRunKind::Color | TextRunKind::Msdf => {
1146                                let page = self.text_paint.page_descriptor(run.kind, run.page);
1147                                self.context.device.cmd_bind_descriptor_sets(
1148                                    cmd,
1149                                    vk::PipelineBindPoint::GRAPHICS,
1150                                    layout,
1151                                    0,
1152                                    &[self.descriptor_set, page],
1153                                    &[],
1154                                );
1155                            }
1156                            TextRunKind::Highlight => {
1157                                self.context.device.cmd_bind_descriptor_sets(
1158                                    cmd,
1159                                    vk::PipelineBindPoint::GRAPHICS,
1160                                    layout,
1161                                    0,
1162                                    &[self.descriptor_set],
1163                                    &[],
1164                                );
1165                            }
1166                        }
1167                        self.context.device.cmd_bind_vertex_buffers(
1168                            cmd,
1169                            0,
1170                            &[
1171                                self.quad_vbo.buffer,
1172                                self.text_paint.instance_buffer(run.kind),
1173                            ],
1174                            &[0, 0],
1175                        );
1176                        self.context
1177                            .device
1178                            .cmd_draw(cmd, 4, run.count, 0, run.first);
1179                    }
1180                }
1181                PaintItem::Image(index) => {
1182                    let run = self.image_paint.run(index);
1183                    let pipeline = self.image_paint.pipeline();
1184                    let layout = self.image_paint.pipeline_layout();
1185                    let texture_set = self.image_paint.descriptor_for_run(run);
1186                    unsafe {
1187                        self.context.device.cmd_bind_pipeline(
1188                            cmd,
1189                            vk::PipelineBindPoint::GRAPHICS,
1190                            pipeline,
1191                        );
1192                        self.set_viewport(cmd);
1193                        self.set_scissor(cmd, run.scissor, full);
1194                        self.context.device.cmd_bind_descriptor_sets(
1195                            cmd,
1196                            vk::PipelineBindPoint::GRAPHICS,
1197                            layout,
1198                            0,
1199                            &[self.descriptor_set, texture_set],
1200                            &[],
1201                        );
1202                        self.context.device.cmd_bind_vertex_buffers(
1203                            cmd,
1204                            0,
1205                            &[self.quad_vbo.buffer, self.image_paint.instance_buffer()],
1206                            &[0, 0],
1207                        );
1208                        self.context
1209                            .device
1210                            .cmd_draw(cmd, 4, run.count, 0, run.first);
1211                    }
1212                }
1213                PaintItem::IconRun(index) | PaintItem::Vector(index) => {
1214                    let run = self.icon_paint.run(index);
1215                    match run.kind {
1216                        IconRunKind::Msdf => {
1217                            let page = self.icon_paint.page_descriptor(run.page);
1218                            unsafe {
1219                                self.context.device.cmd_bind_pipeline(
1220                                    cmd,
1221                                    vk::PipelineBindPoint::GRAPHICS,
1222                                    self.icon_paint.pipeline(),
1223                                );
1224                                self.set_viewport(cmd);
1225                                self.set_scissor(cmd, run.scissor, full);
1226                                self.context.device.cmd_bind_descriptor_sets(
1227                                    cmd,
1228                                    vk::PipelineBindPoint::GRAPHICS,
1229                                    self.icon_paint.pipeline_layout(),
1230                                    0,
1231                                    &[self.descriptor_set, page],
1232                                    &[],
1233                                );
1234                                self.context.device.cmd_bind_vertex_buffers(
1235                                    cmd,
1236                                    0,
1237                                    &[self.quad_vbo.buffer, self.icon_paint.instance_buffer()],
1238                                    &[0, 0],
1239                                );
1240                                self.context
1241                                    .device
1242                                    .cmd_draw(cmd, 4, run.count, 0, run.first);
1243                            }
1244                        }
1245                        IconRunKind::Tess => unsafe {
1246                            self.context.device.cmd_bind_pipeline(
1247                                cmd,
1248                                vk::PipelineBindPoint::GRAPHICS,
1249                                self.icon_paint.tess_pipeline(run.material),
1250                            );
1251                            self.set_viewport(cmd);
1252                            self.set_scissor(cmd, run.scissor, full);
1253                            self.context.device.cmd_bind_descriptor_sets(
1254                                cmd,
1255                                vk::PipelineBindPoint::GRAPHICS,
1256                                self.icon_paint.tess_pipeline_layout(),
1257                                0,
1258                                &[self.descriptor_set],
1259                                &[],
1260                            );
1261                            self.context.device.cmd_bind_vertex_buffers(
1262                                cmd,
1263                                0,
1264                                &[self.icon_paint.tess_vertex_buffer()],
1265                                &[0],
1266                            );
1267                            self.context
1268                                .device
1269                                .cmd_draw(cmd, run.count, 1, run.first, 0);
1270                        },
1271                    }
1272                }
1273                PaintItem::Scene3D(index) => {
1274                    // The scene already rendered + resolved offscreen ahead of
1275                    // this scope; composite the resolved texture in via the
1276                    // stock surface pipeline, like an AppTexture.
1277                    let run = self.scene_paint.run(index);
1278                    let pipeline = self.scene_paint.composite_pipeline();
1279                    let layout = self.scene_paint.composite_pipeline_layout();
1280                    let texture_set = self.scene_paint.composite_descriptor(run);
1281                    unsafe {
1282                        self.context.device.cmd_bind_pipeline(
1283                            cmd,
1284                            vk::PipelineBindPoint::GRAPHICS,
1285                            pipeline,
1286                        );
1287                        self.set_viewport(cmd);
1288                        self.set_scissor(cmd, run.scissor, full);
1289                        self.context.device.cmd_bind_descriptor_sets(
1290                            cmd,
1291                            vk::PipelineBindPoint::GRAPHICS,
1292                            layout,
1293                            0,
1294                            &[self.descriptor_set, texture_set],
1295                            &[],
1296                        );
1297                        self.context.device.cmd_bind_vertex_buffers(
1298                            cmd,
1299                            0,
1300                            &[
1301                                self.quad_vbo.buffer,
1302                                self.scene_paint.composite_instance_buffer(),
1303                            ],
1304                            &[0, 0],
1305                        );
1306                        self.context
1307                            .device
1308                            .cmd_draw(cmd, 4, 1, 0, run.composite_instance);
1309                    }
1310                }
1311                PaintItem::BackdropSnapshot => {
1312                    return Err(Error::Unsupported(
1313                        "damascene-ash backdrop snapshot rendering is not implemented yet",
1314                    ));
1315                }
1316                PaintItem::AppTexture(index) => {
1317                    let run = self.surface_paint.run(index);
1318                    let pipeline = self.surface_paint.pipeline_for(run.alpha);
1319                    let layout = self.surface_paint.pipeline_layout();
1320                    let texture_set = self.surface_paint.descriptor_for_run(run);
1321                    unsafe {
1322                        self.context.device.cmd_bind_pipeline(
1323                            cmd,
1324                            vk::PipelineBindPoint::GRAPHICS,
1325                            pipeline,
1326                        );
1327                        self.set_viewport(cmd);
1328                        self.set_scissor(cmd, run.scissor, full);
1329                        self.context.device.cmd_bind_descriptor_sets(
1330                            cmd,
1331                            vk::PipelineBindPoint::GRAPHICS,
1332                            layout,
1333                            0,
1334                            &[self.descriptor_set, texture_set],
1335                            &[],
1336                        );
1337                        self.context.device.cmd_bind_vertex_buffers(
1338                            cmd,
1339                            0,
1340                            &[self.quad_vbo.buffer, self.surface_paint.instance_buffer()],
1341                            &[0, 0],
1342                        );
1343                        self.context
1344                            .device
1345                            .cmd_draw(cmd, 4, run.count, 0, run.first);
1346                    }
1347                }
1348            }
1349        }
1350        Ok(())
1351    }
1352
1353    unsafe fn set_viewport(&self, cmd: vk::CommandBuffer) {
1354        let viewport = vk::Viewport {
1355            x: 0.0,
1356            y: 0.0,
1357            width: self.core.viewport_px.0 as f32,
1358            height: self.core.viewport_px.1 as f32,
1359            min_depth: 0.0,
1360            max_depth: 1.0,
1361        };
1362        unsafe {
1363            self.context.device.cmd_set_viewport(cmd, 0, &[viewport]);
1364        }
1365    }
1366
1367    unsafe fn set_scissor(
1368        &self,
1369        cmd: vk::CommandBuffer,
1370        scissor: Option<PhysicalScissor>,
1371        full: PhysicalScissor,
1372    ) {
1373        let s = scissor.unwrap_or(full);
1374        let rect = vk::Rect2D {
1375            offset: vk::Offset2D {
1376                x: s.x as i32,
1377                y: s.y as i32,
1378            },
1379            extent: vk::Extent2D {
1380                width: s.w.max(1),
1381                height: s.h.max(1),
1382            },
1383        };
1384        unsafe {
1385            self.context.device.cmd_set_scissor(cmd, 0, &[rect]);
1386        }
1387    }
1388
1389    fn register_stock_shaders(&mut self) -> Result<()> {
1390        self.insert_stock_pipeline(StockShader::RoundedRect, stock_wgsl::ROUNDED_RECT)?;
1391        self.insert_stock_pipeline(StockShader::Spinner, stock_wgsl::SPINNER)?;
1392        self.insert_stock_pipeline(StockShader::Skeleton, stock_wgsl::SKELETON)?;
1393        self.insert_stock_pipeline(
1394            StockShader::ProgressIndeterminate,
1395            stock_wgsl::PROGRESS_INDETERMINATE,
1396        )?;
1397        Ok(())
1398    }
1399
1400    fn insert_stock_pipeline(&mut self, shader: StockShader, wgsl: &str) -> Result<()> {
1401        let pipeline = build_quad_pipeline(
1402            &self.context.device,
1403            self.pipeline_layout,
1404            self.target,
1405            shader.name(),
1406            wgsl,
1407        );
1408        self.pipelines
1409            .insert(ShaderHandle::Stock(shader), pipeline?);
1410        Ok(())
1411    }
1412}
1413
1414struct PaintRecorder<'a> {
1415    text: &'a mut TextPaint,
1416    icons: &'a mut IconPaint,
1417    images: &'a mut ImagePaint,
1418    surfaces: &'a mut SurfacePaint,
1419    scenes: &'a mut Scene3DPaint,
1420    device: &'a ash::Device,
1421    allocator: &'a mut Allocator,
1422}
1423
1424impl TextRecorder for PaintRecorder<'_> {
1425    fn record(
1426        &mut self,
1427        rect: Rect,
1428        scissor: Option<PhysicalScissor>,
1429        style: &RunStyle,
1430        text: &str,
1431        size: f32,
1432        line_height: f32,
1433        wrap: TextWrap,
1434        anchor: TextAnchor,
1435        scale_factor: f32,
1436    ) -> std::ops::Range<usize> {
1437        self.text.record(
1438            rect,
1439            scissor,
1440            style,
1441            text,
1442            size,
1443            line_height,
1444            wrap,
1445            anchor,
1446            scale_factor,
1447        )
1448    }
1449
1450    fn record_runs(
1451        &mut self,
1452        rect: Rect,
1453        scissor: Option<PhysicalScissor>,
1454        runs: &[(String, RunStyle)],
1455        size: f32,
1456        line_height: f32,
1457        wrap: TextWrap,
1458        anchor: TextAnchor,
1459        scale_factor: f32,
1460    ) -> std::ops::Range<usize> {
1461        self.text.record_runs(
1462            rect,
1463            scissor,
1464            runs,
1465            size,
1466            line_height,
1467            wrap,
1468            anchor,
1469            scale_factor,
1470        )
1471    }
1472
1473    fn record_image(
1474        &mut self,
1475        rect: Rect,
1476        scissor: Option<PhysicalScissor>,
1477        image: &damascene_core::image::Image,
1478        tint: Option<Color>,
1479        radius: damascene_core::tree::Corners,
1480        _fit: damascene_core::image::ImageFit,
1481        range_limit: damascene_core::image::DynamicRangeLimit,
1482        _scale_factor: f32,
1483    ) -> std::ops::Range<usize> {
1484        self.images
1485            .record(
1486                self.device,
1487                self.allocator,
1488                ImageRecord {
1489                    rect,
1490                    scissor,
1491                    image,
1492                    tint,
1493                    radius,
1494                    range_limit,
1495                },
1496            )
1497            .expect("damascene-ash: failed to record image")
1498    }
1499
1500    fn record_icon(
1501        &mut self,
1502        rect: Rect,
1503        scissor: Option<PhysicalScissor>,
1504        source: &IconSource,
1505        color: Color,
1506        size: f32,
1507        stroke_width: f32,
1508        scale_factor: f32,
1509    ) -> RecordedPaint {
1510        let range = self
1511            .icons
1512            .record(rect, scissor, source, color, stroke_width);
1513        if !range.is_empty() {
1514            return RecordedPaint::Icon(range);
1515        }
1516        record_icon_text_fallback(self.text, rect, scissor, source, color, size, scale_factor)
1517    }
1518
1519    fn record_vector(
1520        &mut self,
1521        rect: Rect,
1522        scissor: Option<PhysicalScissor>,
1523        asset: &damascene_core::vector::VectorAsset,
1524        render_mode: damascene_core::vector::VectorRenderMode,
1525        _scale_factor: f32,
1526    ) -> std::ops::Range<usize> {
1527        self.icons.record_vector(rect, scissor, asset, render_mode)
1528    }
1529
1530    fn record_app_texture(
1531        &mut self,
1532        rect: Rect,
1533        scissor: Option<PhysicalScissor>,
1534        texture: &damascene_core::surface::AppTexture,
1535        alpha: SurfaceAlpha,
1536        transform: damascene_core::affine::Affine2,
1537        _scale_factor: f32,
1538    ) -> std::ops::Range<usize> {
1539        self.surfaces
1540            .record(self.device, rect, scissor, texture, alpha, transform)
1541            .expect("damascene-ash: failed to record app texture")
1542    }
1543
1544    fn record_scene3d(
1545        &mut self,
1546        rect: Rect,
1547        scissor: Option<PhysicalScissor>,
1548        id: &str,
1549        scene: &std::sync::Arc<damascene_core::scene::Scene3DData>,
1550        scale_factor: f32,
1551    ) -> std::ops::Range<usize> {
1552        self.scenes
1553            .record(
1554                self.device,
1555                self.allocator,
1556                rect,
1557                scissor,
1558                id,
1559                scene,
1560                scale_factor,
1561            )
1562            .expect("damascene-ash: failed to record scene")
1563    }
1564}
1565
1566fn record_icon_text_fallback(
1567    text: &mut TextPaint,
1568    rect: Rect,
1569    scissor: Option<PhysicalScissor>,
1570    source: &IconSource,
1571    color: Color,
1572    size: f32,
1573    scale_factor: f32,
1574) -> RecordedPaint {
1575    let glyph = match source {
1576        IconSource::Builtin(name) => name.fallback_glyph(),
1577        IconSource::Custom(_) => "?",
1578        IconSource::UnknownName(_) => damascene_core::tree::IconName::AlertCircle.fallback_glyph(),
1579    };
1580    RecordedPaint::Text(text.record(
1581        rect,
1582        scissor,
1583        &RunStyle::new(FontWeight::Regular, color),
1584        glyph,
1585        size,
1586        damascene_core::text::metrics::line_height(size),
1587        TextWrap::NoWrap,
1588        TextAnchor::Middle,
1589        scale_factor,
1590    ))
1591}
1592
1593impl Drop for Runner {
1594    fn drop(&mut self) {
1595        unsafe {
1596            for (_, pipeline) in self.pipelines.drain() {
1597                self.context.device.destroy_pipeline(pipeline, None);
1598            }
1599            if self.pipeline_layout != vk::PipelineLayout::null() {
1600                self.context
1601                    .device
1602                    .destroy_pipeline_layout(self.pipeline_layout, None);
1603            }
1604            if self.descriptor_pool != vk::DescriptorPool::null() {
1605                self.context
1606                    .device
1607                    .destroy_descriptor_pool(self.descriptor_pool, None);
1608            }
1609            if self.descriptor_set_layout != vk::DescriptorSetLayout::null() {
1610                self.context
1611                    .device
1612                    .destroy_descriptor_set_layout(self.descriptor_set_layout, None);
1613            }
1614            if let Ok(mut allocator) = self.context.allocator.lock() {
1615                self.scene_paint
1616                    .destroy(&self.context.device, &mut allocator);
1617                self.text_paint
1618                    .destroy(&self.context.device, &mut allocator);
1619                self.icon_paint
1620                    .destroy(&self.context.device, &mut allocator);
1621                self.image_paint
1622                    .destroy(&self.context.device, &mut allocator);
1623                self.surface_paint
1624                    .destroy(&self.context.device, &mut allocator);
1625                self.instance_buf
1626                    .destroy(&self.context.device, &mut allocator);
1627                self.quad_vbo.destroy(&self.context.device, &mut allocator);
1628                self.frame_buf.destroy(&self.context.device, &mut allocator);
1629            }
1630        }
1631    }
1632}
1633
1634fn create_frame_descriptor_set_layout(device: &ash::Device) -> Result<vk::DescriptorSetLayout> {
1635    let binding = vk::DescriptorSetLayoutBinding::default()
1636        .binding(0)
1637        .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
1638        .descriptor_count(1)
1639        .stage_flags(vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT);
1640    let bindings = [binding];
1641    let info = vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings);
1642    unsafe { device.create_descriptor_set_layout(&info, None) }.map_err(|result| Error::Vulkan {
1643        op: "create_descriptor_set_layout",
1644        result,
1645    })
1646}
1647
1648fn create_descriptor_pool(device: &ash::Device) -> Result<vk::DescriptorPool> {
1649    let pool_size = vk::DescriptorPoolSize {
1650        ty: vk::DescriptorType::UNIFORM_BUFFER,
1651        descriptor_count: 1,
1652    };
1653    let pool_sizes = [pool_size];
1654    let info = vk::DescriptorPoolCreateInfo::default()
1655        .max_sets(1)
1656        .pool_sizes(&pool_sizes);
1657    unsafe { device.create_descriptor_pool(&info, None) }.map_err(|result| Error::Vulkan {
1658        op: "create_descriptor_pool",
1659        result,
1660    })
1661}
1662
1663fn allocate_frame_descriptor_set(
1664    device: &ash::Device,
1665    pool: vk::DescriptorPool,
1666    layout: vk::DescriptorSetLayout,
1667) -> Result<vk::DescriptorSet> {
1668    let layouts = [layout];
1669    let info = vk::DescriptorSetAllocateInfo::default()
1670        .descriptor_pool(pool)
1671        .set_layouts(&layouts);
1672    let sets =
1673        unsafe { device.allocate_descriptor_sets(&info) }.map_err(|result| Error::Vulkan {
1674            op: "allocate_descriptor_sets",
1675            result,
1676        })?;
1677    sets.into_iter().next().ok_or(Error::Unsupported(
1678        "descriptor set allocation returned no sets",
1679    ))
1680}
1681
1682fn update_frame_descriptor_set(
1683    device: &ash::Device,
1684    descriptor_set: vk::DescriptorSet,
1685    frame_buffer: vk::Buffer,
1686) {
1687    let buffer_info = vk::DescriptorBufferInfo {
1688        buffer: frame_buffer,
1689        offset: 0,
1690        range: std::mem::size_of::<FrameUniforms>() as vk::DeviceSize,
1691    };
1692    let buffer_infos = [buffer_info];
1693    let write = vk::WriteDescriptorSet::default()
1694        .dst_set(descriptor_set)
1695        .dst_binding(0)
1696        .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
1697        .buffer_info(&buffer_infos);
1698    unsafe {
1699        device.update_descriptor_sets(&[write], &[]);
1700    }
1701}
1702
1703fn create_pipeline_layout(
1704    device: &ash::Device,
1705    frame_layout: vk::DescriptorSetLayout,
1706) -> Result<vk::PipelineLayout> {
1707    let set_layouts = [frame_layout];
1708    let info = vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts);
1709    unsafe { device.create_pipeline_layout(&info, None) }.map_err(|result| Error::Vulkan {
1710        op: "create_pipeline_layout",
1711        result,
1712    })
1713}
1714
1715unsafe fn transition_color_target(
1716    device: &ash::Device,
1717    cmd: vk::CommandBuffer,
1718    image: vk::Image,
1719    old_layout: vk::ImageLayout,
1720    new_layout: vk::ImageLayout,
1721) {
1722    if old_layout == new_layout {
1723        return;
1724    }
1725
1726    let (src_stage, src_access) = match old_layout {
1727        vk::ImageLayout::UNDEFINED => (
1728            vk::PipelineStageFlags::TOP_OF_PIPE,
1729            vk::AccessFlags::empty(),
1730        ),
1731        vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL => (
1732            vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
1733            vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
1734        ),
1735        vk::ImageLayout::PRESENT_SRC_KHR => (
1736            vk::PipelineStageFlags::BOTTOM_OF_PIPE,
1737            vk::AccessFlags::empty(),
1738        ),
1739        _ => (
1740            vk::PipelineStageFlags::ALL_COMMANDS,
1741            vk::AccessFlags::MEMORY_READ | vk::AccessFlags::MEMORY_WRITE,
1742        ),
1743    };
1744    let (dst_stage, dst_access) = match new_layout {
1745        vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL => (
1746            vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
1747            vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
1748        ),
1749        vk::ImageLayout::PRESENT_SRC_KHR => (
1750            vk::PipelineStageFlags::BOTTOM_OF_PIPE,
1751            vk::AccessFlags::empty(),
1752        ),
1753        _ => (
1754            vk::PipelineStageFlags::ALL_COMMANDS,
1755            vk::AccessFlags::MEMORY_READ | vk::AccessFlags::MEMORY_WRITE,
1756        ),
1757    };
1758    let barrier = vk::ImageMemoryBarrier::default()
1759        .old_layout(old_layout)
1760        .new_layout(new_layout)
1761        .src_access_mask(src_access)
1762        .dst_access_mask(dst_access)
1763        .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1764        .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1765        .image(image)
1766        .subresource_range(vk::ImageSubresourceRange {
1767            aspect_mask: vk::ImageAspectFlags::COLOR,
1768            base_mip_level: 0,
1769            level_count: 1,
1770            base_array_layer: 0,
1771            layer_count: 1,
1772        });
1773    unsafe {
1774        device.cmd_pipeline_barrier(
1775            cmd,
1776            src_stage,
1777            dst_stage,
1778            vk::DependencyFlags::empty(),
1779            &[],
1780            &[],
1781            &[barrier],
1782        );
1783    }
1784}