GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: VkCore split + user images in the 2D pass (RT-renderer phase 1)
VkCore extracts the device-level half of the backend — instance/validation,
physical device + graphics queue, gpu-allocator, command pool — with two
constructors: new_for_wayland_surface (what VkRenderer uses; picks a
present-capable device) and new_headless (no surface extensions, any graphics
device) for offscreen consumers: thumbnail rendering, previews, and the coming
RT engine. VkRenderer keeps only presentation + pipelines; the core is its
last field so drop order tears the swapchain down before the device.
Images become first-class 2D content end to end: vk::upload_rgba queues RGBA
pixels from anywhere (apps never see the renderer) and returns an id usable
immediately; the renderer drains uploads per frame into an ImageStage
(per-image descriptor sets over the glyph shader). Frame2D::images carries
ImageQuads whose z_before names the vertex they sort before, and the batch
draw interleaves them under their own clips — geometry below stays below,
later geometry (popovers, plates) covers them. Prim::Image + PaintCtx::image
plumb the display list through tessellate_display_list (now also returning
DlImages) and the runner.
Verified live in vk-smoke: checkerboard above the 3D backdrop and the
circle-clipped quad, beneath the blur plate, zero validation messages.
Workspace builds; 169 + 8 tests pass.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RFkXq68hDwVDMKnu9fckz3
src/backend/window_runner.rs | 54 +++-
src/scene/paint.rs | 10 +
src/vk/core.rs | 297 ++++++++++++++++++++++
src/vk/image.rs | 574 +++++++++++++++++++++++++++++++++++++++++++
src/vk/mod.rs | 4 +
src/vk/renderer.rs | 532 +++++++++++++++------------------------
src/widget/model.rs | 1 +
7 files changed, 1137 insertions(+), 335 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index b6a4e5f..a5470f2 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1009,6 +1009,17 @@ pub struct DlBatch {
pub end: u32,
}
+/// An image draw from the display list: `at` is the vertex index it sorts
+/// before (its position in the tessellated stream); `clip` is the item's
+/// paint-walk clip. Logical coordinates throughout.
+pub struct DlImage {
+ pub image: u32,
+ pub rect: crate::scene::layout::Rect,
+ pub alpha: f32,
+ pub at: u32,
+ pub clip: Option<crate::scene::layout::Rect>,
+}
+
/// Tessellate a `scene::paint::DisplayList`'s geometry into a flat vertex buffer plus per-clip draw
/// batches, reusing the same tessellators as the legacy path so vertices are identical. `Text`
/// prims are skipped here — text is still rendered via the app's `text_areas()` path. `sw`/`sh` are
@@ -1018,16 +1029,27 @@ pub fn tessellate_display_list(
dl: &crate::scene::paint::DisplayList,
sw: f32,
sh: f32,
-) -> (Vec<Vertex>, Vec<DlBatch>) {
+) -> (Vec<Vertex>, Vec<DlBatch>, Vec<DlImage>) {
use crate::scene::paint::{Cap, Prim};
let no = [0.0f32, 0.0, 0.0];
let mut verts: Vec<Vertex> = Vec::new();
let mut batches: Vec<DlBatch> = Vec::new();
+ let mut images: Vec<DlImage> = Vec::new();
for item in &dl.items {
let start = verts.len() as u32;
match &item.prim {
- Prim::Text { .. } => continue, // text goes through the glyphon/text_areas path
+ Prim::Text { .. } => continue, // text goes through the glyph/text-span path
+ Prim::Image { image, rect, alpha } => {
+ images.push(DlImage {
+ image: *image,
+ rect: *rect,
+ alpha: *alpha,
+ at: verts.len() as u32,
+ clip: item.clip,
+ });
+ continue;
+ }
Prim::Quad { rect, color } => {
verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, *color));
}
@@ -1086,7 +1108,7 @@ pub fn tessellate_display_list(
batches.push(DlBatch { scissor: item.clip, start, end });
}
- (verts, batches)
+ (verts, batches, images)
}
pub fn extra_quad_vertices(
@@ -1740,7 +1762,7 @@ impl<A: Application> EngineState<A> {
}
}
- let (mut verts, mut dl_batches) = tessellate_display_list(&dl, logical_w, logical_h);
+ let (mut verts, mut dl_batches, dl_images) = tessellate_display_list(&dl, logical_w, logical_h);
// custom_vertices (e.g. graph geometry) is appended as a final unclipped batch drawn on top.
let pre_custom = verts.len() as u32;
self.inner.as_mut().unwrap().custom_vertices(&mut verts, LogicalSize::new(logical_w, logical_h), scale_factor);
@@ -1830,6 +1852,29 @@ impl<A: Application> EngineState<A> {
let renderer = self.renderer.as_mut().unwrap();
renderer.prepare_text(self.font_system.as_mut().unwrap(), &mut self.swash_cache, &spans);
+ let image_quads: Vec<crate::vk::ImageQuad> = dl_images
+ .iter()
+ .map(|di| crate::vk::ImageQuad {
+ image: di.image,
+ rect: (
+ di.rect.x * scale_f32,
+ di.rect.y * scale_f32,
+ di.rect.width * scale_f32,
+ di.rect.height * scale_f32,
+ ),
+ alpha: di.alpha,
+ z_before: di.at,
+ clip: di.clip.map(|c| {
+ (
+ (c.x * scale_f32).max(0.0) as u32,
+ (c.y * scale_f32).max(0.0) as u32,
+ (c.width * scale_f32) as u32,
+ (c.height * scale_f32) as u32,
+ )
+ }),
+ })
+ .collect();
+
let batches: Vec<Batch2D> = dl_batches
.iter()
.map(|batch| Batch2D {
@@ -1858,6 +1903,7 @@ impl<A: Application> EngineState<A> {
verts: &verts,
batches: &batches,
overlay_verts: &overlay_verts,
+ images: &image_quads,
clear_color,
});
}
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 8986251..b55c01d 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -56,6 +56,10 @@ pub enum Prim {
/// within a box (the placed-text-box case, e.g. cce-layout-interface's canvas elements);
/// `None` is the ordinary single-run label.
Text { text: String, x: f32, y: f32, font_size: f32, color: [u8; 3], font: Option<String>, bounds: Option<[f32; 4]>, attrs: TextAttrs, layout: Option<TextLayout> },
+ /// A user image (id from `cce_ui::vk::upload_rgba`) drawn as a quad, in
+ /// display-list order like any other primitive. The paint walk's clip
+ /// applies through the item's `clip` as usual.
+ Image { image: u32, rect: Rect, alpha: f32 },
}
/// Horizontal alignment of laid-out (boxed) text — the toolkit-plain mirror of
@@ -209,6 +213,12 @@ impl PaintCtx {
self.push(Prim::Quad { rect, color });
}
+ /// A user image (id from `cce_ui::vk::upload_rgba`) drawn at `rect`.
+ pub fn image(&mut self, image: u32, rect: Rect, alpha: f32) {
+ let rect = self.apply_offset(rect);
+ self.push(Prim::Image { image, rect, alpha });
+ }
+
pub fn rounded_rect(&mut self, rect: Rect, radius: f32, corners: (bool, bool, bool, bool), color: [f32; 4]) {
let rect = self.apply_offset(rect);
self.push(Prim::RoundedRect { rect, radius, corners, color });
diff --git a/src/vk/core.rs b/src/vk/core.rs
new file mode 100644
index 0000000..f4eedbd
--- /dev/null
+++ b/src/vk/core.rs
@@ -0,0 +1,297 @@
+//! `VkCore`: the device-level half of the Vulkan backend — instance (+
+//! validation layers), physical device + graphics queue, gpu-allocator, and
+//! the shared command pool.
+//!
+//! Two ways in: [`VkCore::new_for_wayland_surface`] picks a present-capable
+//! device for a window (what `VkRenderer` uses), and [`VkCore::new_headless`]
+//! builds the same core with no surface at all — for offscreen consumers
+//! (thumbnail rendering, previews, the future RT engine) that render into
+//! images instead of a swapchain.
+
+use std::ffi::{c_void, CStr, CString};
+
+use ash::vk;
+use gpu_allocator::vulkan::{Allocator, AllocatorCreateDesc};
+
+const VALIDATION_LAYER: &CStr = c"VK_LAYER_KHRONOS_validation";
+
+unsafe extern "system" fn debug_callback(
+ severity: vk::DebugUtilsMessageSeverityFlagsEXT,
+ _types: vk::DebugUtilsMessageTypeFlagsEXT,
+ data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>,
+ _user_data: *mut c_void,
+) -> vk::Bool32 {
+ if data.is_null() {
+ return vk::FALSE;
+ }
+ let message = unsafe {
+ let p = (*data).p_message;
+ if p.is_null() {
+ return vk::FALSE;
+ }
+ CStr::from_ptr(p).to_string_lossy()
+ };
+ if severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::ERROR) {
+ log::error!("[vulkan] {message}");
+ } else if severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::WARNING) {
+ log::warn!("[vulkan] {message}");
+ } else {
+ log::debug!("[vulkan] {message}");
+ }
+ vk::FALSE
+}
+
+pub struct VkCore {
+ // Field order is drop order: allocator and command pool go before the
+ // device, the device before debug/instance; `_entry` (the loaded library)
+ // must outlive everything.
+ pub(crate) allocator: Option<Allocator>,
+ pub(crate) command_pool: vk::CommandPool,
+ pub(crate) queue: vk::Queue,
+ #[allow(dead_code)] // RT engine / future consumers select by family
+ pub(crate) queue_family: u32,
+ pub(crate) device: ash::Device,
+ pub(crate) physical_device: vk::PhysicalDevice,
+ pub(crate) surface_loader: ash::khr::surface::Instance,
+ pub(crate) debug: Option<(ash::ext::debug_utils::Instance, vk::DebugUtilsMessengerEXT)>,
+ pub(crate) instance: ash::Instance,
+ pub(crate) _entry: ash::Entry,
+ pub(crate) min_uniform_align: vk::DeviceSize,
+}
+
+impl VkCore {
+ /// A core bound to a Wayland surface: the returned `vk::SurfaceKHR` is
+ /// created from the raw pointers and the chosen device supports presenting
+ /// to it. The caller owns the surface handle (destroy it before the core).
+ ///
+ /// # Safety
+ /// `display_ptr` and `surface_ptr` must be live `wl_display` / `wl_surface`
+ /// pointers that outlive the core and everything created from it.
+ pub unsafe fn new_for_wayland_surface(
+ display_ptr: *mut c_void,
+ surface_ptr: *mut c_void,
+ ) -> (Self, vk::SurfaceKHR) {
+ let (core, surface) = Self::new_inner(Some((display_ptr, surface_ptr)));
+ (core, surface.expect("surface requested but not created"))
+ }
+
+ /// A windowless core: no surface extensions, any graphics-capable device.
+ /// For offscreen rendering (thumbnails, previews) and compute.
+ pub fn new_headless() -> Self {
+ unsafe { Self::new_inner(None).0 }
+ }
+
+ unsafe fn new_inner(
+ wayland: Option<(*mut c_void, *mut c_void)>,
+ ) -> (Self, Option<vk::SurfaceKHR>) {
+ let entry = ash::Entry::load().expect("Failed to load libvulkan");
+
+ // Validation when available (debug builds or CCE_VK_VALIDATION=1).
+ let want_validation =
+ cfg!(debug_assertions) || std::env::var_os("CCE_VK_VALIDATION").is_some();
+ let validation_available = want_validation
+ && entry
+ .enumerate_instance_layer_properties()
+ .map(|layers| {
+ layers
+ .iter()
+ .any(|l| CStr::from_ptr(l.layer_name.as_ptr()) == VALIDATION_LAYER)
+ })
+ .unwrap_or(false);
+ if want_validation && !validation_available {
+ log::warn!(
+ "Vulkan validation requested but VK_LAYER_KHRONOS_validation is not installed"
+ );
+ }
+
+ let api_version = match entry.try_enumerate_instance_version().ok().flatten() {
+ Some(v) if v >= vk::API_VERSION_1_2 => vk::API_VERSION_1_2,
+ Some(v) => v,
+ None => vk::API_VERSION_1_0,
+ };
+ let app_name = c"cce-ui";
+ let app_info = vk::ApplicationInfo::default()
+ .application_name(app_name)
+ .engine_name(app_name)
+ .api_version(api_version);
+
+ let mut extension_names: Vec<*const i8> = Vec::new();
+ if wayland.is_some() {
+ extension_names.push(ash::khr::surface::NAME.as_ptr());
+ extension_names.push(ash::khr::wayland_surface::NAME.as_ptr());
+ }
+ if validation_available {
+ extension_names.push(ash::ext::debug_utils::NAME.as_ptr());
+ }
+ let layer_names_owned: Vec<CString> = if validation_available {
+ vec![VALIDATION_LAYER.to_owned()]
+ } else {
+ Vec::new()
+ };
+ let layer_names: Vec<*const i8> = layer_names_owned.iter().map(|l| l.as_ptr()).collect();
+
+ let instance = entry
+ .create_instance(
+ &vk::InstanceCreateInfo::default()
+ .application_info(&app_info)
+ .enabled_extension_names(&extension_names)
+ .enabled_layer_names(&layer_names),
+ None,
+ )
+ .expect("Failed to create Vulkan instance");
+
+ let debug = if validation_available {
+ let loader = ash::ext::debug_utils::Instance::new(&entry, &instance);
+ let messenger = loader
+ .create_debug_utils_messenger(
+ &vk::DebugUtilsMessengerCreateInfoEXT::default()
+ .message_severity(
+ vk::DebugUtilsMessageSeverityFlagsEXT::ERROR
+ | vk::DebugUtilsMessageSeverityFlagsEXT::WARNING,
+ )
+ .message_type(
+ vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
+ | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION
+ | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE,
+ )
+ .pfn_user_callback(Some(debug_callback)),
+ None,
+ )
+ .expect("Failed to create debug messenger");
+ log::info!("Vulkan validation layers enabled");
+ Some((loader, messenger))
+ } else {
+ None
+ };
+
+ // Instance-level loader; only usable when VK_KHR_surface was enabled.
+ let surface_loader = ash::khr::surface::Instance::new(&entry, &instance);
+
+ let surface = wayland.map(|(display_ptr, surface_ptr)| {
+ let wayland_loader = ash::khr::wayland_surface::Instance::new(&entry, &instance);
+ wayland_loader
+ .create_wayland_surface(
+ &vk::WaylandSurfaceCreateInfoKHR::default()
+ .display(display_ptr)
+ .surface(surface_ptr),
+ None,
+ )
+ .expect("Failed to create Wayland surface")
+ });
+
+ // Physical device + queue family: graphics, plus present support when
+ // a surface exists. Prefer integrated (the toolkit's LowPower default).
+ let mut candidates: Vec<(vk::PhysicalDevice, u32, i32)> = Vec::new();
+ for pd in instance
+ .enumerate_physical_devices()
+ .expect("No Vulkan physical devices")
+ {
+ let families = instance.get_physical_device_queue_family_properties(pd);
+ let family = families.iter().enumerate().find_map(|(i, f)| {
+ let graphics = f.queue_flags.contains(vk::QueueFlags::GRAPHICS);
+ let present = match surface {
+ Some(surface) => surface_loader
+ .get_physical_device_surface_support(pd, i as u32, surface)
+ .unwrap_or(false),
+ None => true,
+ };
+ (graphics && present).then_some(i as u32)
+ });
+ if let Some(family) = family {
+ let props = instance.get_physical_device_properties(pd);
+ let rank = match props.device_type {
+ vk::PhysicalDeviceType::INTEGRATED_GPU => 0,
+ vk::PhysicalDeviceType::DISCRETE_GPU => 1,
+ vk::PhysicalDeviceType::VIRTUAL_GPU => 2,
+ _ => 3,
+ };
+ candidates.push((pd, family, rank));
+ }
+ }
+ candidates.sort_by_key(|&(_, _, rank)| rank);
+ let (physical_device, queue_family, _) = *candidates
+ .first()
+ .expect("No suitable Vulkan device found");
+ {
+ let props = instance.get_physical_device_properties(physical_device);
+ let name = CStr::from_ptr(props.device_name.as_ptr()).to_string_lossy();
+ log::info!("Vulkan device: {name}");
+ }
+ let min_uniform_align = instance
+ .get_physical_device_properties(physical_device)
+ .limits
+ .min_uniform_buffer_offset_alignment;
+
+ let queue_priorities = [1.0f32];
+ let queue_infos = [vk::DeviceQueueCreateInfo::default()
+ .queue_family_index(queue_family)
+ .queue_priorities(&queue_priorities)];
+ let device_extensions: Vec<*const i8> = if wayland.is_some() {
+ vec![ash::khr::swapchain::NAME.as_ptr()]
+ } else {
+ Vec::new()
+ };
+ let device = instance
+ .create_device(
+ physical_device,
+ &vk::DeviceCreateInfo::default()
+ .queue_create_infos(&queue_infos)
+ .enabled_extension_names(&device_extensions),
+ None,
+ )
+ .expect("Failed to create Vulkan device");
+ let queue = device.get_device_queue(queue_family, 0);
+
+ let allocator = Allocator::new(&AllocatorCreateDesc {
+ instance: instance.clone(),
+ device: device.clone(),
+ physical_device,
+ debug_settings: Default::default(),
+ buffer_device_address: false,
+ allocation_sizes: Default::default(),
+ })
+ .expect("Failed to create GPU allocator");
+
+ let command_pool = device
+ .create_command_pool(
+ &vk::CommandPoolCreateInfo::default()
+ .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER)
+ .queue_family_index(queue_family),
+ None,
+ )
+ .expect("Failed to create command pool");
+
+ (
+ VkCore {
+ allocator: Some(allocator),
+ command_pool,
+ queue,
+ queue_family,
+ device,
+ physical_device,
+ surface_loader,
+ debug,
+ instance,
+ _entry: entry,
+ min_uniform_align,
+ },
+ surface,
+ )
+ }
+}
+
+impl Drop for VkCore {
+ fn drop(&mut self) {
+ unsafe {
+ let _ = self.device.device_wait_idle();
+ // The allocator must go before the device it allocates from.
+ drop(self.allocator.take());
+ self.device.destroy_command_pool(self.command_pool, None);
+ self.device.destroy_device(None);
+ if let Some((loader, messenger)) = self.debug.take() {
+ loader.destroy_debug_utils_messenger(messenger, None);
+ }
+ self.instance.destroy_instance(None);
+ }
+ }
+}
diff --git a/src/vk/image.rs b/src/vk/image.rs
new file mode 100644
index 0000000..0caf84c
--- /dev/null
+++ b/src/vk/image.rs
@@ -0,0 +1,574 @@
+//! User images in the 2D pass: upload RGBA pixels once, then draw them as
+//! quads interleaved with the display list — 3D previews in graph nodes,
+//! thumbnails in the files grid, any raster content in the UI.
+//!
+//! Upload is decoupled from the renderer because most apps never touch it
+//! (the engine runner owns the frame): [`upload_rgba`] queues pixels from any
+//! code and returns a stable id usable immediately in draws; the renderer
+//! drains the queue at the next frame. [`free_image`] queues destruction the
+//! same way. Draw ordering comes from [`super::Frame2D::images`]: each
+//! [`ImageQuad`] carries the vertex index it sorts before.
+
+use std::collections::HashMap;
+use std::sync::atomic::{AtomicU32, Ordering};
+use std::sync::Mutex;
+
+use ash::vk;
+use gpu_allocator::vulkan::{Allocation, AllocationCreateDesc, AllocationScheme, Allocator};
+use gpu_allocator::MemoryLocation;
+
+use super::renderer::{compile_wgsl, create_cpu_buffer, destroy_cpu_buffer, AllocatedBuffer};
+
+/// One image draw in a 2D frame.
+pub struct ImageQuad {
+ /// Id from [`upload_rgba`].
+ pub image: u32,
+ /// Destination rect (x, y, w, h) in physical pixels.
+ pub rect: (f32, f32, f32, f32),
+ pub alpha: f32,
+ /// Draw order: this quad renders before the vertex at this index of
+ /// `Frame2D::verts` (so vertices below it stay below, later ones cover it).
+ /// Use `u32::MAX` to draw on top of all display-list geometry.
+ pub z_before: u32,
+ /// Optional scissor (x, y, w, h) in physical pixels.
+ pub clip: Option<(u32, u32, u32, u32)>,
+}
+
+enum Pending {
+ Upload { id: u32, pixels: Vec<u8>, width: u32, height: u32 },
+ Free { id: u32 },
+}
+
+static PENDING: Mutex<Vec<Pending>> = Mutex::new(Vec::new());
+static NEXT_ID: AtomicU32 = AtomicU32::new(1);
+
+/// Queue an RGBA8 image for upload; the id is usable in [`ImageQuad`]s right
+/// away (draws before the upload lands are skipped, not errors).
+pub fn upload_rgba(pixels: Vec<u8>, width: u32, height: u32) -> u32 {
+ assert_eq!(pixels.len(), (width * height * 4) as usize, "RGBA8 size mismatch");
+ let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
+ PENDING.lock().unwrap().push(Pending::Upload { id, pixels, width, height });
+ id
+}
+
+/// Queue an image's GPU resources for destruction.
+pub fn free_image(id: u32) {
+ PENDING.lock().unwrap().push(Pending::Free { id });
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
+struct ImageVertex {
+ position: [f32; 2],
+ uv: [f32; 2],
+ color: [f32; 4],
+ clip_circle: [f32; 3],
+}
+
+struct GpuImage {
+ image: vk::Image,
+ view: vk::ImageView,
+ allocation: Option<Allocation>,
+ descriptor_set: vk::DescriptorSet,
+}
+
+const MAX_IMAGES: u32 = 256;
+
+pub(crate) struct ImageStage {
+ pipeline: vk::Pipeline,
+ pipeline_layout: vk::PipelineLayout,
+ descriptor_set_layout: vk::DescriptorSetLayout,
+ descriptor_pool: vk::DescriptorPool,
+ shader_module: vk::ShaderModule,
+ sampler: vk::Sampler,
+ images: HashMap<u32, GpuImage>,
+ /// Per frame in flight: this frame's quad vertices (6 per ImageQuad).
+ frame_buffers: Vec<AllocatedBuffer>,
+}
+
+impl ImageStage {
+ pub(crate) fn new(
+ device: &ash::Device,
+ allocator: &mut Allocator,
+ render_pass: vk::RenderPass,
+ frames_in_flight: usize,
+ ) -> Self {
+ unsafe {
+ let bindings = [
+ vk::DescriptorSetLayoutBinding::default()
+ .binding(0)
+ .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
+ .descriptor_count(1)
+ .stage_flags(vk::ShaderStageFlags::FRAGMENT),
+ vk::DescriptorSetLayoutBinding::default()
+ .binding(1)
+ .descriptor_type(vk::DescriptorType::SAMPLER)
+ .descriptor_count(1)
+ .stage_flags(vk::ShaderStageFlags::FRAGMENT),
+ ];
+ let descriptor_set_layout = device
+ .create_descriptor_set_layout(
+ &vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
+ None,
+ )
+ .expect("Failed to create image descriptor set layout");
+ let set_layouts = [descriptor_set_layout];
+ let pipeline_layout = device
+ .create_pipeline_layout(
+ &vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts),
+ None,
+ )
+ .expect("Failed to create image pipeline layout");
+
+ // Same shader as glyphs: sampled texel * vertex color (+ circle clip).
+ let spirv = compile_wgsl(include_str!("glyph.wgsl"));
+ let shader_module = device
+ .create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&spirv), None)
+ .expect("Failed to create image shader module");
+ let stages = [
+ vk::PipelineShaderStageCreateInfo::default()
+ .stage(vk::ShaderStageFlags::VERTEX)
+ .module(shader_module)
+ .name(c"vs_main"),
+ vk::PipelineShaderStageCreateInfo::default()
+ .stage(vk::ShaderStageFlags::FRAGMENT)
+ .module(shader_module)
+ .name(c"fs_main"),
+ ];
+ let vertex_bindings = [vk::VertexInputBindingDescription::default()
+ .binding(0)
+ .stride(std::mem::size_of::<ImageVertex>() as u32)
+ .input_rate(vk::VertexInputRate::VERTEX)];
+ let vertex_attributes = [
+ vk::VertexInputAttributeDescription::default()
+ .location(0)
+ .binding(0)
+ .format(vk::Format::R32G32_SFLOAT)
+ .offset(0),
+ vk::VertexInputAttributeDescription::default()
+ .location(1)
+ .binding(0)
+ .format(vk::Format::R32G32_SFLOAT)
+ .offset(8),
+ vk::VertexInputAttributeDescription::default()
+ .location(2)
+ .binding(0)
+ .format(vk::Format::R32G32B32A32_SFLOAT)
+ .offset(16),
+ vk::VertexInputAttributeDescription::default()
+ .location(3)
+ .binding(0)
+ .format(vk::Format::R32G32B32_SFLOAT)
+ .offset(32),
+ ];
+ let vertex_input = vk::PipelineVertexInputStateCreateInfo::default()
+ .vertex_binding_descriptions(&vertex_bindings)
+ .vertex_attribute_descriptions(&vertex_attributes);
+ let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::default()
+ .topology(vk::PrimitiveTopology::TRIANGLE_LIST);
+ let viewport_state = vk::PipelineViewportStateCreateInfo::default()
+ .viewport_count(1)
+ .scissor_count(1);
+ let rasterization = vk::PipelineRasterizationStateCreateInfo::default()
+ .polygon_mode(vk::PolygonMode::FILL)
+ .cull_mode(vk::CullModeFlags::NONE)
+ .front_face(vk::FrontFace::COUNTER_CLOCKWISE)
+ .line_width(1.0);
+ let multisample = vk::PipelineMultisampleStateCreateInfo::default()
+ .rasterization_samples(vk::SampleCountFlags::TYPE_1);
+ let blend_attachments = [vk::PipelineColorBlendAttachmentState::default()
+ .blend_enable(true)
+ .src_color_blend_factor(vk::BlendFactor::SRC_ALPHA)
+ .dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
+ .color_blend_op(vk::BlendOp::ADD)
+ .src_alpha_blend_factor(vk::BlendFactor::ONE)
+ .dst_alpha_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
+ .alpha_blend_op(vk::BlendOp::ADD)
+ .color_write_mask(vk::ColorComponentFlags::RGBA)];
+ let color_blend = vk::PipelineColorBlendStateCreateInfo::default()
+ .attachments(&blend_attachments);
+ let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR];
+ let dynamic_state =
+ vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
+ let pipeline = device
+ .create_graphics_pipelines(
+ vk::PipelineCache::null(),
+ &[vk::GraphicsPipelineCreateInfo::default()
+ .stages(&stages)
+ .vertex_input_state(&vertex_input)
+ .input_assembly_state(&input_assembly)
+ .viewport_state(&viewport_state)
+ .rasterization_state(&rasterization)
+ .multisample_state(&multisample)
+ .color_blend_state(&color_blend)
+ .dynamic_state(&dynamic_state)
+ .layout(pipeline_layout)
+ .render_pass(render_pass)
+ .subpass(0)],
+ None,
+ )
+ .expect("Failed to create image pipeline")[0];
+
+ // Linear filtering: thumbnails scale smoothly.
+ let sampler = device
+ .create_sampler(
+ &vk::SamplerCreateInfo::default()
+ .mag_filter(vk::Filter::LINEAR)
+ .min_filter(vk::Filter::LINEAR)
+ .mipmap_mode(vk::SamplerMipmapMode::NEAREST)
+ .address_mode_u(vk::SamplerAddressMode::CLAMP_TO_EDGE)
+ .address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE)
+ .address_mode_w(vk::SamplerAddressMode::CLAMP_TO_EDGE),
+ None,
+ )
+ .expect("Failed to create image sampler");
+
+ let pool_sizes = [
+ vk::DescriptorPoolSize::default()
+ .ty(vk::DescriptorType::SAMPLED_IMAGE)
+ .descriptor_count(MAX_IMAGES),
+ vk::DescriptorPoolSize::default()
+ .ty(vk::DescriptorType::SAMPLER)
+ .descriptor_count(MAX_IMAGES),
+ ];
+ let descriptor_pool = device
+ .create_descriptor_pool(
+ &vk::DescriptorPoolCreateInfo::default()
+ .flags(vk::DescriptorPoolCreateFlags::FREE_DESCRIPTOR_SET)
+ .max_sets(MAX_IMAGES)
+ .pool_sizes(&pool_sizes),
+ None,
+ )
+ .expect("Failed to create image descriptor pool");
+
+ let frame_buffers = (0..frames_in_flight)
+ .map(|_| {
+ create_cpu_buffer(
+ device,
+ allocator,
+ 16 * 1024,
+ vk::BufferUsageFlags::VERTEX_BUFFER,
+ "image-quads",
+ )
+ })
+ .collect();
+
+ ImageStage {
+ pipeline,
+ pipeline_layout,
+ descriptor_set_layout,
+ descriptor_pool,
+ shader_module,
+ sampler,
+ images: HashMap::new(),
+ frame_buffers,
+ }
+ }
+ }
+
+ /// Drain the global upload/free queue. Uploads are synchronous one-time
+ /// submits (rare: images load once); frees wait for device idle.
+ pub(crate) fn process_pending(
+ &mut self,
+ device: &ash::Device,
+ allocator: &mut Allocator,
+ queue: vk::Queue,
+ command_pool: vk::CommandPool,
+ ) {
+ let pending: Vec<Pending> = std::mem::take(&mut *PENDING.lock().unwrap());
+ for item in pending {
+ match item {
+ Pending::Upload { id, pixels, width, height } => {
+ self.upload(device, allocator, queue, command_pool, id, &pixels, width, height);
+ }
+ Pending::Free { id } => {
+ if let Some(mut gpu) = self.images.remove(&id) {
+ unsafe {
+ let _ = device.device_wait_idle();
+ device.destroy_image_view(gpu.view, None);
+ device.destroy_image(gpu.image, None);
+ let _ = device.free_descriptor_sets(
+ self.descriptor_pool,
+ &[gpu.descriptor_set],
+ );
+ }
+ if let Some(a) = gpu.allocation.take() {
+ let _ = allocator.free(a);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ fn upload(
+ &mut self,
+ device: &ash::Device,
+ allocator: &mut Allocator,
+ queue: vk::Queue,
+ command_pool: vk::CommandPool,
+ id: u32,
+ pixels: &[u8],
+ width: u32,
+ height: u32,
+ ) {
+ if self.images.len() as u32 >= MAX_IMAGES {
+ log::error!("image registry full ({MAX_IMAGES}); dropping upload {id}");
+ return;
+ }
+ unsafe {
+ let image = device
+ .create_image(
+ &vk::ImageCreateInfo::default()
+ .image_type(vk::ImageType::TYPE_2D)
+ .format(vk::Format::R8G8B8A8_UNORM)
+ .extent(vk::Extent3D { width, height, depth: 1 })
+ .mip_levels(1)
+ .array_layers(1)
+ .samples(vk::SampleCountFlags::TYPE_1)
+ .tiling(vk::ImageTiling::OPTIMAL)
+ .usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST)
+ .initial_layout(vk::ImageLayout::UNDEFINED),
+ None,
+ )
+ .expect("Failed to create user image");
+ let requirements = device.get_image_memory_requirements(image);
+ let allocation = allocator
+ .allocate(&AllocationCreateDesc {
+ name: "user-image",
+ requirements,
+ location: MemoryLocation::GpuOnly,
+ linear: false,
+ allocation_scheme: AllocationScheme::GpuAllocatorManaged,
+ })
+ .expect("Failed to allocate user image memory");
+ device
+ .bind_image_memory(image, allocation.memory(), allocation.offset())
+ .expect("Failed to bind user image memory");
+
+ let mut staging = create_cpu_buffer(
+ device,
+ allocator,
+ pixels.len() as vk::DeviceSize,
+ vk::BufferUsageFlags::TRANSFER_SRC,
+ "image-staging",
+ );
+ staging.allocation.as_mut().unwrap().mapped_slice_mut().unwrap()[..pixels.len()]
+ .copy_from_slice(pixels);
+
+ let range = vk::ImageSubresourceRange::default()
+ .aspect_mask(vk::ImageAspectFlags::COLOR)
+ .level_count(1)
+ .layer_count(1);
+ let cmd = device
+ .allocate_command_buffers(
+ &vk::CommandBufferAllocateInfo::default()
+ .command_pool(command_pool)
+ .level(vk::CommandBufferLevel::PRIMARY)
+ .command_buffer_count(1),
+ )
+ .expect("Failed to allocate upload command buffer")[0];
+ device
+ .begin_command_buffer(
+ cmd,
+ &vk::CommandBufferBeginInfo::default()
+ .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
+ )
+ .unwrap();
+ device.cmd_pipeline_barrier(
+ cmd,
+ vk::PipelineStageFlags::TOP_OF_PIPE,
+ vk::PipelineStageFlags::TRANSFER,
+ vk::DependencyFlags::empty(),
+ &[],
+ &[],
+ &[vk::ImageMemoryBarrier::default()
+ .src_access_mask(vk::AccessFlags::empty())
+ .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
+ .old_layout(vk::ImageLayout::UNDEFINED)
+ .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
+ .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
+ .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
+ .image(image)
+ .subresource_range(range)],
+ );
+ device.cmd_copy_buffer_to_image(
+ cmd,
+ staging.buffer,
+ image,
+ vk::ImageLayout::TRANSFER_DST_OPTIMAL,
+ &[vk::BufferImageCopy::default()
+ .buffer_row_length(width)
+ .buffer_image_height(height)
+ .image_subresource(
+ vk::ImageSubresourceLayers::default()
+ .aspect_mask(vk::ImageAspectFlags::COLOR)
+ .layer_count(1),
+ )
+ .image_extent(vk::Extent3D { width, height, depth: 1 })],
+ );
+ device.cmd_pipeline_barrier(
+ cmd,
+ vk::PipelineStageFlags::TRANSFER,
+ vk::PipelineStageFlags::FRAGMENT_SHADER,
+ vk::DependencyFlags::empty(),
+ &[],
+ &[],
+ &[vk::ImageMemoryBarrier::default()
+ .src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
+ .dst_access_mask(vk::AccessFlags::SHADER_READ)
+ .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
+ .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
+ .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
+ .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
+ .image(image)
+ .subresource_range(range)],
+ );
+ device.end_command_buffer(cmd).unwrap();
+ let cmds = [cmd];
+ device
+ .queue_submit(queue, &[vk::SubmitInfo::default().command_buffers(&cmds)], vk::Fence::null())
+ .expect("Image upload submit failed");
+ device.queue_wait_idle(queue).expect("Image upload wait failed");
+ device.free_command_buffers(command_pool, &cmds);
+ destroy_cpu_buffer(device, allocator, &mut staging);
+
+ let view = device
+ .create_image_view(
+ &vk::ImageViewCreateInfo::default()
+ .image(image)
+ .view_type(vk::ImageViewType::TYPE_2D)
+ .format(vk::Format::R8G8B8A8_UNORM)
+ .subresource_range(range),
+ None,
+ )
+ .expect("Failed to create user image view");
+
+ let set_layouts = [self.descriptor_set_layout];
+ let descriptor_set = device
+ .allocate_descriptor_sets(
+ &vk::DescriptorSetAllocateInfo::default()
+ .descriptor_pool(self.descriptor_pool)
+ .set_layouts(&set_layouts),
+ )
+ .expect("Failed to allocate image descriptor set")[0];
+ let image_infos = [vk::DescriptorImageInfo::default()
+ .image_view(view)
+ .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)];
+ let sampler_infos = [vk::DescriptorImageInfo::default().sampler(self.sampler)];
+ device.update_descriptor_sets(
+ &[
+ vk::WriteDescriptorSet::default()
+ .dst_set(descriptor_set)
+ .dst_binding(0)
+ .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
+ .image_info(&image_infos),
+ vk::WriteDescriptorSet::default()
+ .dst_set(descriptor_set)
+ .dst_binding(1)
+ .descriptor_type(vk::DescriptorType::SAMPLER)
+ .image_info(&sampler_infos),
+ ],
+ &[],
+ );
+
+ self.images.insert(id, GpuImage { image, view, allocation: Some(allocation), descriptor_set });
+ }
+ }
+
+ /// After the frame fence: build this frame's quad vertices (6 per image,
+ /// in `images` order).
+ pub(crate) fn write_frame_buffer(
+ &mut self,
+ device: &ash::Device,
+ allocator: &mut Allocator,
+ frame_index: usize,
+ images: &[ImageQuad],
+ extent: vk::Extent2D,
+ ) {
+ let sw = extent.width as f32;
+ let sh = extent.height as f32;
+ let mut verts: Vec<ImageVertex> = Vec::with_capacity(images.len() * 6);
+ for q in images {
+ let (x, y, w, h) = q.rect;
+ let ndc = |px: f32, py: f32| [(px / sw) * 2.0 - 1.0, 1.0 - (py / sh) * 2.0];
+ let color = [1.0, 1.0, 1.0, q.alpha];
+ let clip_circle = [0.0; 3];
+ let tl = ImageVertex { position: ndc(x, y), uv: [0.0, 0.0], color, clip_circle };
+ let tr = ImageVertex { position: ndc(x + w, y), uv: [1.0, 0.0], color, clip_circle };
+ let bl = ImageVertex { position: ndc(x, y + h), uv: [0.0, 1.0], color, clip_circle };
+ let br = ImageVertex { position: ndc(x + w, y + h), uv: [1.0, 1.0], color, clip_circle };
+ verts.extend([tl, tr, bl, tr, br, bl]);
+ }
+ let bytes: &[u8] = bytemuck::cast_slice(&verts);
+ let buf = &mut self.frame_buffers[frame_index];
+ if bytes.len() as vk::DeviceSize > buf.size {
+ let mut old = std::mem::replace(buf, AllocatedBuffer::null());
+ destroy_cpu_buffer(device, allocator, &mut old);
+ *buf = create_cpu_buffer(
+ device,
+ allocator,
+ (bytes.len() as vk::DeviceSize).next_power_of_two(),
+ vk::BufferUsageFlags::VERTEX_BUFFER,
+ "image-quads",
+ );
+ }
+ if !bytes.is_empty() {
+ buf.allocation.as_mut().unwrap().mapped_slice_mut().unwrap()[..bytes.len()]
+ .copy_from_slice(bytes);
+ }
+ }
+
+ /// Record one image quad (index `i` of this frame's list). The caller
+ /// restores its own pipeline/scissor state afterwards. Returns false if the
+ /// image hasn't finished uploading (draw skipped).
+ pub(crate) fn record_quad(
+ &self,
+ device: &ash::Device,
+ cmd: vk::CommandBuffer,
+ frame_index: usize,
+ i: usize,
+ image_id: u32,
+ ) -> bool {
+ let Some(gpu) = self.images.get(&image_id) else {
+ return false;
+ };
+ unsafe {
+ device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline);
+ device.cmd_bind_descriptor_sets(
+ cmd,
+ vk::PipelineBindPoint::GRAPHICS,
+ self.pipeline_layout,
+ 0,
+ &[gpu.descriptor_set],
+ &[],
+ );
+ device.cmd_bind_vertex_buffers(cmd, 0, &[self.frame_buffers[frame_index].buffer], &[0]);
+ device.cmd_draw(cmd, 6, 1, (i * 6) as u32, 0);
+ }
+ true
+ }
+
+ pub(crate) fn destroy(&mut self, device: &ash::Device, allocator: &mut Allocator) {
+ unsafe {
+ for (_, mut gpu) in self.images.drain() {
+ device.destroy_image_view(gpu.view, None);
+ device.destroy_image(gpu.image, None);
+ if let Some(a) = gpu.allocation.take() {
+ let _ = allocator.free(a);
+ }
+ }
+ for buf in &mut self.frame_buffers {
+ let mut b = std::mem::replace(buf, AllocatedBuffer::null());
+ destroy_cpu_buffer(device, allocator, &mut b);
+ }
+ device.destroy_sampler(self.sampler, None);
+ device.destroy_descriptor_pool(self.descriptor_pool, None);
+ device.destroy_descriptor_set_layout(self.descriptor_set_layout, None);
+ device.destroy_pipeline(self.pipeline, None);
+ device.destroy_pipeline_layout(self.pipeline_layout, None);
+ device.destroy_shader_module(self.shader_module, None);
+ }
+ }
+}
diff --git a/src/vk/mod.rs b/src/vk/mod.rs
index d85f0d2..d7f7dff 100644
--- a/src/vk/mod.rs
+++ b/src/vk/mod.rs
@@ -23,10 +23,14 @@
//! naga's ADJUST_COORDINATE_SPACE — a shader-side flip would reverse winding
//! and break the 3D pipeline's back-face culling.
+mod core;
+pub mod image;
mod renderer;
mod scene;
mod text;
+pub use core::VkCore;
+pub use image::{free_image, upload_rgba, ImageQuad};
pub use renderer::{Batch2D, Frame2D, VkRenderer};
pub use scene::{MeshId, SceneDraw, Vertex3D};
pub use text::TextSpan;
diff --git a/src/vk/renderer.rs b/src/vk/renderer.rs
index 3d0db38..42f5ce7 100644
--- a/src/vk/renderer.rs
+++ b/src/vk/renderer.rs
@@ -5,16 +5,17 @@
//! is a 1x1 placeholder until the blur-behind path is wired to a real framebuffer
//! copy at cutover.
-use std::ffi::{c_void, CStr, CString};
+use std::ffi::c_void;
use ash::vk;
use gpu_allocator::vulkan::{
- Allocation, AllocationCreateDesc, AllocationScheme, Allocator, AllocatorCreateDesc,
+ Allocation, AllocationCreateDesc, AllocationScheme, Allocator,
};
use gpu_allocator::MemoryLocation;
use crate::engine::Vertex;
+use super::image::{ImageQuad, ImageStage};
use super::scene::{MeshId, SceneDraw, SceneStage, Vertex3D};
use super::text::{TextSpan, TextStage};
@@ -33,11 +34,12 @@ pub struct Frame2D<'a> {
pub verts: &'a [Vertex],
pub batches: &'a [Batch2D],
pub overlay_verts: &'a [Vertex],
+ /// User images drawn interleaved with `verts` by each quad's `z_before`.
+ pub images: &'a [ImageQuad],
pub clear_color: [f32; 4],
}
const FRAMES_IN_FLIGHT: usize = 2;
-const VALIDATION_LAYER: &CStr = c"VK_LAYER_KHRONOS_validation";
pub(crate) struct AllocatedBuffer {
pub(crate) buffer: vk::Buffer,
@@ -119,15 +121,7 @@ struct Frame {
}
pub struct VkRenderer {
- _entry: ash::Entry,
- instance: ash::Instance,
- debug: Option<(ash::ext::debug_utils::Instance, vk::DebugUtilsMessengerEXT)>,
- surface_loader: ash::khr::surface::Instance,
surface: vk::SurfaceKHR,
- physical_device: vk::PhysicalDevice,
- device: ash::Device,
- queue: vk::Queue,
- allocator: Option<Allocator>,
swapchain_loader: ash::khr::swapchain::Device,
swapchain: vk::SwapchainKHR,
@@ -154,15 +148,19 @@ pub struct VkRenderer {
backdrop_sampler: vk::Sampler,
window_info: AllocatedBuffer,
- command_pool: vk::CommandPool,
frames: Vec<Frame>,
frame_index: usize,
text: TextStage,
scene: SceneStage,
+ image: ImageStage,
desired_extent: vk::Extent2D,
corner_radius_px: f32,
swapchain_dirty: bool,
+
+ // Declared last: everything above must be destroyed before the device/
+ // instance the core tears down in its own Drop.
+ core: super::core::VkCore,
}
/// Compile WGSL to SPIR-V. The Y-flip between wgpu NDC (Y-up) and Vulkan NDC
@@ -281,31 +279,6 @@ pub(crate) fn flipped_viewport(extent: vk::Extent2D) -> vk::Viewport {
}
}
-unsafe extern "system" fn debug_callback(
- severity: vk::DebugUtilsMessageSeverityFlagsEXT,
- _types: vk::DebugUtilsMessageTypeFlagsEXT,
- data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>,
- _user_data: *mut c_void,
-) -> vk::Bool32 {
- if data.is_null() {
- return vk::FALSE;
- }
- let message = unsafe {
- let p = (*data).p_message;
- if p.is_null() {
- return vk::FALSE;
- }
- CStr::from_ptr(p).to_string_lossy()
- };
- if severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::ERROR) {
- log::error!("[vulkan] {message}");
- } else if severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::WARNING) {
- log::warn!("[vulkan] {message}");
- } else {
- log::debug!("[vulkan] {message}");
- }
- vk::FALSE
-}
impl VkRenderer {
/// # Safety
@@ -318,157 +291,16 @@ impl VkRenderer {
height: u32,
corner_radius_px: f32,
) -> Self {
- let entry = ash::Entry::load().expect("Failed to load libvulkan");
-
- // Instance, with validation when available (debug builds or CCE_VK_VALIDATION=1).
- let want_validation =
- cfg!(debug_assertions) || std::env::var_os("CCE_VK_VALIDATION").is_some();
- let validation_available = want_validation
- && entry
- .enumerate_instance_layer_properties()
- .map(|layers| {
- layers.iter().any(|l| {
- CStr::from_ptr(l.layer_name.as_ptr()) == VALIDATION_LAYER
- })
- })
- .unwrap_or(false);
- if want_validation && !validation_available {
- log::warn!("Vulkan validation requested but VK_LAYER_KHRONOS_validation is not installed");
- }
-
- let api_version = match entry.try_enumerate_instance_version().ok().flatten() {
- Some(v) if v >= vk::API_VERSION_1_2 => vk::API_VERSION_1_2,
- Some(v) => v,
- None => vk::API_VERSION_1_0,
- };
- let app_name = c"cce-ui";
- let app_info = vk::ApplicationInfo::default()
- .application_name(app_name)
- .engine_name(app_name)
- .api_version(api_version);
-
- let mut extension_names = vec![
- ash::khr::surface::NAME.as_ptr(),
- ash::khr::wayland_surface::NAME.as_ptr(),
- ];
- if validation_available {
- extension_names.push(ash::ext::debug_utils::NAME.as_ptr());
- }
- let layer_names_owned: Vec<CString> = if validation_available {
- vec![VALIDATION_LAYER.to_owned()]
- } else {
- Vec::new()
- };
- let layer_names: Vec<*const i8> =
- layer_names_owned.iter().map(|l| l.as_ptr()).collect();
-
- let instance = entry
- .create_instance(
- &vk::InstanceCreateInfo::default()
- .application_info(&app_info)
- .enabled_extension_names(&extension_names)
- .enabled_layer_names(&layer_names),
- None,
- )
- .expect("Failed to create Vulkan instance");
-
- let debug = if validation_available {
- let loader = ash::ext::debug_utils::Instance::new(&entry, &instance);
- let messenger = loader
- .create_debug_utils_messenger(
- &vk::DebugUtilsMessengerCreateInfoEXT::default()
- .message_severity(
- vk::DebugUtilsMessageSeverityFlagsEXT::ERROR
- | vk::DebugUtilsMessageSeverityFlagsEXT::WARNING,
- )
- .message_type(
- vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
- | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION
- | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE,
- )
- .pfn_user_callback(Some(debug_callback)),
- None,
- )
- .expect("Failed to create debug messenger");
- log::info!("Vulkan validation layers enabled");
- Some((loader, messenger))
- } else {
- None
- };
-
- // Wayland surface from the same raw pointers WgpuAdapter uses.
- let wayland_loader = ash::khr::wayland_surface::Instance::new(&entry, &instance);
- let surface = wayland_loader
- .create_wayland_surface(
- &vk::WaylandSurfaceCreateInfoKHR::default()
- .display(display_ptr)
- .surface(surface_ptr),
- None,
- )
- .expect("Failed to create Wayland surface");
- let surface_loader = ash::khr::surface::Instance::new(&entry, &instance);
-
- // Physical device + queue family: graphics with present support on this
- // surface. Prefer integrated (matches WgpuAdapter's LowPower preference).
- let mut candidates: Vec<(vk::PhysicalDevice, u32, i32)> = Vec::new();
- for pd in instance
- .enumerate_physical_devices()
- .expect("No Vulkan physical devices")
- {
- let families = instance.get_physical_device_queue_family_properties(pd);
- let family = families.iter().enumerate().find_map(|(i, f)| {
- let graphics = f.queue_flags.contains(vk::QueueFlags::GRAPHICS);
- let present = surface_loader
- .get_physical_device_surface_support(pd, i as u32, surface)
- .unwrap_or(false);
- (graphics && present).then_some(i as u32)
- });
- if let Some(family) = family {
- let props = instance.get_physical_device_properties(pd);
- let rank = match props.device_type {
- vk::PhysicalDeviceType::INTEGRATED_GPU => 0,
- vk::PhysicalDeviceType::DISCRETE_GPU => 1,
- vk::PhysicalDeviceType::VIRTUAL_GPU => 2,
- _ => 3,
- };
- candidates.push((pd, family, rank));
- }
- }
- candidates.sort_by_key(|&(_, _, rank)| rank);
- let (physical_device, queue_family, _) = *candidates
- .first()
- .expect("No Vulkan device supports this Wayland surface");
- {
- let props = instance.get_physical_device_properties(physical_device);
- let name = CStr::from_ptr(props.device_name.as_ptr()).to_string_lossy();
- log::info!("Vulkan device: {name}");
- }
-
- let queue_priorities = [1.0f32];
- let queue_infos = [vk::DeviceQueueCreateInfo::default()
- .queue_family_index(queue_family)
- .queue_priorities(&queue_priorities)];
- let device_extensions = [ash::khr::swapchain::NAME.as_ptr()];
- let device = instance
- .create_device(
- physical_device,
- &vk::DeviceCreateInfo::default()
- .queue_create_infos(&queue_infos)
- .enabled_extension_names(&device_extensions),
- None,
- )
- .expect("Failed to create Vulkan device");
- let queue = device.get_device_queue(queue_family, 0);
-
- let mut allocator = Allocator::new(&AllocatorCreateDesc {
- instance: instance.clone(),
- device: device.clone(),
- physical_device,
- debug_settings: Default::default(),
- buffer_device_address: false,
- allocation_sizes: Default::default(),
- })
- .expect("Failed to create GPU allocator");
+ let (mut core, surface) =
+ super::core::VkCore::new_for_wayland_surface(display_ptr, surface_ptr);
+ // Locals over the core for the setup below (methods use self.core.*).
+ let device = core.device.clone();
+ let queue = core.queue;
+ let command_pool = core.command_pool;
+ let physical_device = core.physical_device;
+ let min_uniform_align = core.min_uniform_align;
+ let surface_loader = core.surface_loader.clone();
+ let allocator = core.allocator.as_mut().unwrap();
// Surface format: prefer sRGB (wgpu's get_default_config sorts sRGB first,
// so this matches the colors the app renders today).
@@ -671,25 +503,12 @@ impl VkRenderer {
)
.expect("Failed to create graphics pipeline")[0];
- let command_pool = device
- .create_command_pool(
- &vk::CommandPoolCreateInfo::default()
- .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER)
- .queue_family_index(queue_family),
- None,
- )
- .expect("Failed to create command pool");
-
// Full-size backdrop + depth live in the scene stage: the 3D pass renders
// into the backdrop, and the UI pass samples it for blur-behind plates.
- let min_uniform_align = instance
- .get_physical_device_properties(physical_device)
- .limits
- .min_uniform_buffer_offset_alignment;
let initial_extent = vk::Extent2D { width: width.max(1), height: height.max(1) };
let scene = SceneStage::new(
&device,
- &mut allocator,
+ allocator,
surface_format.format,
initial_extent,
FRAMES_IN_FLIGHT,
@@ -713,7 +532,7 @@ impl VkRenderer {
let window_info = create_cpu_buffer(
&device,
- &mut allocator,
+ allocator,
16,
vk::BufferUsageFlags::UNIFORM_BUFFER,
"window-info",
@@ -799,7 +618,7 @@ impl VkRenderer {
.unwrap(),
vertex: create_cpu_buffer(
&device,
- &mut allocator,
+ allocator,
64 * 1024,
vk::BufferUsageFlags::VERTEX_BUFFER,
"vertices",
@@ -810,19 +629,12 @@ impl VkRenderer {
})
.collect();
- let text = TextStage::new(&device, &mut allocator, render_pass, FRAMES_IN_FLIGHT);
+ let text = TextStage::new(&device, allocator, render_pass, FRAMES_IN_FLIGHT);
+ let image = ImageStage::new(&device, allocator, render_pass, FRAMES_IN_FLIGHT);
- let swapchain_loader = ash::khr::swapchain::Device::new(&instance, &device);
+ let swapchain_loader = ash::khr::swapchain::Device::new(&core.instance, &device);
let mut renderer = Self {
- _entry: entry,
- instance,
- debug,
- surface_loader,
surface,
- physical_device,
- device,
- queue,
- allocator: Some(allocator),
swapchain_loader,
swapchain: vk::SwapchainKHR::null(),
swapchain_images: Vec::new(),
@@ -841,14 +653,15 @@ impl VkRenderer {
descriptor_set,
backdrop_sampler,
window_info,
- command_pool,
frames,
frame_index: 0,
text,
scene,
+ image,
desired_extent: vk::Extent2D { width: width.max(1), height: height.max(1) },
corner_radius_px,
swapchain_dirty: false,
+ core,
};
renderer.create_swapchain();
renderer.write_window_info();
@@ -874,23 +687,23 @@ impl VkRenderer {
fn destroy_swapchain_resources(&mut self) {
unsafe {
for fb in self.framebuffers.drain(..) {
- self.device.destroy_framebuffer(fb, None);
+ self.core.device.destroy_framebuffer(fb, None);
}
for view in self.swapchain_views.drain(..) {
- self.device.destroy_image_view(view, None);
+ self.core.device.destroy_image_view(view, None);
}
self.swapchain_images.clear();
for sem in self.render_finished.drain(..) {
- self.device.destroy_semaphore(sem, None);
+ self.core.device.destroy_semaphore(sem, None);
}
}
}
fn create_swapchain(&mut self) {
unsafe {
- let caps = self
+ let caps = self.core
.surface_loader
- .get_physical_device_surface_capabilities(self.physical_device, self.surface)
+ .get_physical_device_surface_capabilities(self.core.physical_device, self.surface)
.expect("Failed to query surface capabilities");
// Wayland reports "extent defined by the swapchain" (u32::MAX); use the
@@ -968,7 +781,7 @@ impl VkRenderer {
.base_array_layer(0)
.layer_count(1);
for image in &images {
- let view = self
+ let view = self.core
.device
.create_image_view(
&vk::ImageViewCreateInfo::default()
@@ -981,7 +794,7 @@ impl VkRenderer {
.expect("Failed to create swapchain view");
self.swapchain_views.push(view);
let attachments = [view];
- let fb = self
+ let fb = self.core
.device
.create_framebuffer(
&vk::FramebufferCreateInfo::default()
@@ -995,7 +808,7 @@ impl VkRenderer {
.expect("Failed to create framebuffer");
self.framebuffers.push(fb);
self.render_finished.push(
- self.device
+ self.core.device
.create_semaphore(&vk::SemaphoreCreateInfo::default(), None)
.unwrap(),
);
@@ -1005,7 +818,7 @@ impl VkRenderer {
fn recreate_swapchain(&mut self) {
unsafe {
- let _ = self.device.device_wait_idle();
+ let _ = self.core.device.device_wait_idle();
}
self.destroy_swapchain_resources();
self.create_swapchain();
@@ -1018,21 +831,21 @@ impl VkRenderer {
/// legal to sample.
fn sync_backdrop_targets(&mut self) {
self.scene.resize(
- &self.device,
- self.allocator.as_mut().unwrap(),
+ &self.core.device,
+ self.core.allocator.as_mut().unwrap(),
self.extent,
);
clear_image_to_shader_read(
- &self.device,
- self.queue,
- self.command_pool,
+ &self.core.device,
+ self.core.queue,
+ self.core.command_pool,
self.scene.backdrop_image,
);
let image_infos = [vk::DescriptorImageInfo::default()
.image_view(self.scene.backdrop_view)
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)];
unsafe {
- self.device.update_descriptor_sets(
+ self.core.device.update_descriptor_sets(
&[vk::WriteDescriptorSet::default()
.dst_set(self.descriptor_set)
.dst_binding(0)
@@ -1047,7 +860,7 @@ impl VkRenderer {
/// renderer's lifetime.
pub fn create_mesh(&mut self, verts: &[Vertex3D]) -> MeshId {
self.scene
- .create_mesh(&self.device, self.allocator.as_mut().unwrap(), verts)
+ .create_mesh(&self.core.device, self.core.allocator.as_mut().unwrap(), verts)
}
/// Replace a mesh's vertices. Waits for the GPU to go idle first — geometry
@@ -1055,10 +868,10 @@ impl VkRenderer {
#[allow(dead_code)] // cutover API: the app's rebuild_scene_geometry path
pub fn update_mesh(&mut self, id: MeshId, verts: &[Vertex3D]) {
unsafe {
- let _ = self.device.device_wait_idle();
+ let _ = self.core.device.device_wait_idle();
}
self.scene
- .update_mesh(&self.device, self.allocator.as_mut().unwrap(), id, verts);
+ .update_mesh(&self.core.device, self.core.allocator.as_mut().unwrap(), id, verts);
}
/// Stage the 3D scene for the next `draw_frame`. Draws render into the
@@ -1108,6 +921,7 @@ impl VkRenderer {
verts,
batches: &[],
overlay_verts: &[],
+ images: &[],
clear_color: [0.0; 4],
})
}
@@ -1128,7 +942,7 @@ impl VkRenderer {
let f = &self.frames[frame_index];
(f.in_flight, f.image_available)
};
- self.device
+ self.core.device
.wait_for_fences(&[in_flight], true, u64::MAX)
.expect("Fence wait failed");
@@ -1154,7 +968,7 @@ impl VkRenderer {
}
};
- self.device.reset_fences(&[in_flight]).unwrap();
+ self.core.device.reset_fences(&[in_flight]).unwrap();
// Upload display-list + overlay vertices into this frame's buffer
// (its fence has signaled, so the GPU is done with it; growing swaps
@@ -1165,10 +979,10 @@ impl VkRenderer {
if needed > self.frames[frame_index].vertex.size {
let mut old =
std::mem::replace(&mut self.frames[frame_index].vertex, AllocatedBuffer::null());
- let allocator = self.allocator.as_mut().unwrap();
- destroy_cpu_buffer(&self.device, allocator, &mut old);
+ let allocator = self.core.allocator.as_mut().unwrap();
+ destroy_cpu_buffer(&self.core.device, allocator, &mut old);
self.frames[frame_index].vertex = create_cpu_buffer(
- &self.device,
+ &self.core.device,
allocator,
needed.next_power_of_two(),
vk::BufferUsageFlags::VERTEX_BUFFER,
@@ -1191,27 +1005,40 @@ impl VkRenderer {
self.frames[frame_index].overlay_start = frame2d.verts.len() as u32;
self.frames[frame_index].overlay_count = frame2d.overlay_verts.len() as u32;
self.text.write_frame_buffers(
- &self.device,
- self.allocator.as_mut().unwrap(),
+ &self.core.device,
+ self.core.allocator.as_mut().unwrap(),
frame_index,
);
+ self.image.process_pending(
+ &self.core.device,
+ self.core.allocator.as_mut().unwrap(),
+ self.core.queue,
+ self.core.command_pool,
+ );
+ self.image.write_frame_buffer(
+ &self.core.device,
+ self.core.allocator.as_mut().unwrap(),
+ frame_index,
+ frame2d.images,
+ self.extent,
+ );
self.scene.write_frame_uniforms(
- &self.device,
- self.allocator.as_mut().unwrap(),
+ &self.core.device,
+ self.core.allocator.as_mut().unwrap(),
frame_index,
self.corner_radius_px,
);
// Record.
let cmd = self.frames[frame_index].cmd;
- self.device
+ self.core.device
.begin_command_buffer(cmd, &vk::CommandBufferBeginInfo::default())
.unwrap();
- self.text.record_upload(&self.device, cmd, frame_index);
+ self.text.record_upload(&self.core.device, cmd, frame_index);
// Offscreen 3D pass (only when a scene was staged); leaves the
// backdrop in TRANSFER_SRC.
- let scene_recorded = self.scene.record(&self.device, cmd, frame_index);
+ let scene_recorded = self.scene.record(&self.core.device, cmd, frame_index);
// With a valid backdrop, replay it under the UI: copy it into the
// swapchain image and open the UI pass with LOAD instead of CLEAR.
@@ -1219,7 +1046,7 @@ impl VkRenderer {
if use_backdrop {
if !scene_recorded {
// Reused backdrop is in SHADER_READ_ONLY from last frame.
- self.device.cmd_pipeline_barrier(
+ self.core.device.cmd_pipeline_barrier(
cmd,
vk::PipelineStageFlags::FRAGMENT_SHADER,
vk::PipelineStageFlags::TRANSFER,
@@ -1238,7 +1065,7 @@ impl VkRenderer {
);
}
let swapchain_image = self.swapchain_images[image_index as usize];
- self.device.cmd_pipeline_barrier(
+ self.core.device.cmd_pipeline_barrier(
cmd,
vk::PipelineStageFlags::TOP_OF_PIPE,
vk::PipelineStageFlags::TRANSFER,
@@ -1258,7 +1085,7 @@ impl VkRenderer {
let subresource = vk::ImageSubresourceLayers::default()
.aspect_mask(vk::ImageAspectFlags::COLOR)
.layer_count(1);
- self.device.cmd_copy_image(
+ self.core.device.cmd_copy_image(
cmd,
self.scene.backdrop_image,
vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
@@ -1274,7 +1101,7 @@ impl VkRenderer {
})],
);
// Backdrop back to sampleable for the UI pass's blur plates.
- self.device.cmd_pipeline_barrier(
+ self.core.device.cmd_pipeline_barrier(
cmd,
vk::PipelineStageFlags::TRANSFER,
vk::PipelineStageFlags::FRAGMENT_SHADER,
@@ -1302,7 +1129,7 @@ impl VkRenderer {
} else {
(self.render_pass, &clear_values)
};
- self.device.cmd_begin_render_pass(
+ self.core.device.cmd_begin_render_pass(
cmd,
&vk::RenderPassBeginInfo::default()
.render_pass(ui_pass)
@@ -1314,9 +1141,9 @@ impl VkRenderer {
.clear_values(ui_clear_values),
vk::SubpassContents::INLINE,
);
- self.device
+ self.core.device
.cmd_set_viewport(cmd, 0, &[flipped_viewport(self.extent)]);
- self.device.cmd_set_scissor(
+ self.core.device.cmd_set_scissor(
cmd,
0,
&[vk::Rect2D {
@@ -1328,61 +1155,111 @@ impl VkRenderer {
offset: vk::Offset2D { x: 0, y: 0 },
extent: self.extent,
};
- if frame.vertex_count > 0 {
- self.device
- .cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline);
- self.device.cmd_bind_descriptor_sets(
- cmd,
- vk::PipelineBindPoint::GRAPHICS,
- self.pipeline_layout,
- 0,
- &[self.descriptor_set],
- &[],
- );
- self.device
- .cmd_bind_vertex_buffers(cmd, 0, &[frame.vertex.buffer], &[0]);
- if frame2d.batches.is_empty() {
- self.device.cmd_draw(cmd, frame.vertex_count, 1, 0, 0);
- } else {
- // Each batch draws its range under its own scissor.
- for batch in frame2d.batches {
- if batch.end <= batch.start {
- continue;
- }
- match batch.scissor {
- Some((bx, by, bw, bh)) => {
- if bx >= self.extent.width || by >= self.extent.height {
- continue;
- }
+ // Display-list geometry interleaved with user images: each image
+ // quad draws before the vertex its `z_before` names, so it sits
+ // above earlier geometry and below later geometry.
+ {
+ let images = frame2d.images;
+ let mut order: Vec<usize> = (0..images.len()).collect();
+ order.sort_by_key(|&k| images[k].z_before);
+ let mut img_i = 0usize;
+
+ let default_batch =
+ [Batch2D { scissor: None, start: 0, end: frame.vertex_count }];
+ let batches: &[Batch2D] =
+ if frame2d.batches.is_empty() { &default_batch } else { frame2d.batches };
+
+ for batch in batches {
+ // Resolve the batch scissor; a degenerate one skips the
+ // vertex draws (images still process on their own clips).
+ let batch_scissor: Option<vk::Rect2D> = match batch.scissor {
+ Some((bx, by, bw, bh)) => {
+ if bx >= self.extent.width || by >= self.extent.height {
+ None
+ } else {
let bw = bw.min(self.extent.width - bx);
let bh = bh.min(self.extent.height - by);
if bw == 0 || bh == 0 {
- continue;
- }
- self.device.cmd_set_scissor(
- cmd,
- 0,
- &[vk::Rect2D {
+ None
+ } else {
+ Some(vk::Rect2D {
offset: vk::Offset2D { x: bx as i32, y: by as i32 },
extent: vk::Extent2D { width: bw, height: bh },
- }],
- );
+ })
+ }
}
- None => self.device.cmd_set_scissor(cmd, 0, &[full_scissor]),
}
- self.device
- .cmd_draw(cmd, batch.end - batch.start, 1, batch.start, 0);
+ None => Some(full_scissor),
+ };
+
+ let mut cursor = batch.start;
+ while cursor < batch.end {
+ let next_z =
+ order.get(img_i).map(|&k| images[k].z_before).unwrap_or(u32::MAX);
+ if next_z <= cursor {
+ let k = order[img_i];
+ img_i += 1;
+ let q = &images[k];
+ let img_scissor = match q.clip {
+ Some((cx, cy, cw, ch)) => vk::Rect2D {
+ offset: vk::Offset2D { x: cx as i32, y: cy as i32 },
+ extent: vk::Extent2D {
+ width: cw.min(self.extent.width.saturating_sub(cx)),
+ height: ch.min(self.extent.height.saturating_sub(cy)),
+ },
+ },
+ None => full_scissor,
+ };
+ self.core.device.cmd_set_scissor(cmd, 0, &[img_scissor]);
+ self.image.record_quad(&self.core.device, cmd, frame_index, k, q.image);
+ continue;
+ }
+ let upto = next_z.min(batch.end);
+ if let Some(scissor) = batch_scissor {
+ self.core.device
+ .cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline);
+ self.core.device.cmd_bind_descriptor_sets(
+ cmd,
+ vk::PipelineBindPoint::GRAPHICS,
+ self.pipeline_layout,
+ 0,
+ &[self.descriptor_set],
+ &[],
+ );
+ self.core.device
+ .cmd_bind_vertex_buffers(cmd, 0, &[frame.vertex.buffer], &[0]);
+ self.core.device.cmd_set_scissor(cmd, 0, &[scissor]);
+ self.core.device.cmd_draw(cmd, upto - cursor, 1, cursor, 0);
+ }
+ cursor = upto;
}
- // Restore for the text/overlay draws.
- self.device.cmd_set_scissor(cmd, 0, &[full_scissor]);
}
+ // Images sorting after all geometry.
+ while let Some(&k) = order.get(img_i) {
+ img_i += 1;
+ let q = &images[k];
+ let img_scissor = match q.clip {
+ Some((cx, cy, cw, ch)) => vk::Rect2D {
+ offset: vk::Offset2D { x: cx as i32, y: cy as i32 },
+ extent: vk::Extent2D {
+ width: cw.min(self.extent.width.saturating_sub(cx)),
+ height: ch.min(self.extent.height.saturating_sub(cy)),
+ },
+ },
+ None => full_scissor,
+ };
+ self.core.device.cmd_set_scissor(cmd, 0, &[img_scissor]);
+ self.image.record_quad(&self.core.device, cmd, frame_index, k, q.image);
+ }
+ // Restore for the text/overlay draws.
+ self.core.device.cmd_set_scissor(cmd, 0, &[full_scissor]);
}
- self.text.record_draw(&self.device, cmd, frame_index);
+ self.text.record_draw(&self.core.device, cmd, frame_index);
if frame.overlay_count > 0 {
// The text pass bound its own pipeline; rebind for the overlay.
- self.device
+ self.core.device
.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline);
- self.device.cmd_bind_descriptor_sets(
+ self.core.device.cmd_bind_descriptor_sets(
cmd,
vk::PipelineBindPoint::GRAPHICS,
self.pipeline_layout,
@@ -1390,13 +1267,13 @@ impl VkRenderer {
&[self.descriptor_set],
&[],
);
- self.device
+ self.core.device
.cmd_bind_vertex_buffers(cmd, 0, &[frame.vertex.buffer], &[0]);
- self.device
+ self.core.device
.cmd_draw(cmd, frame.overlay_count, 1, frame.overlay_start, 0);
}
- self.device.cmd_end_render_pass(cmd);
- self.device.end_command_buffer(cmd).unwrap();
+ self.core.device.cmd_end_render_pass(cmd);
+ self.core.device.end_command_buffer(cmd).unwrap();
// Submit + present. The acquire semaphore gates the swapchain image's
// first use: the backdrop copy (TRANSFER) or the UI pass (COLOR).
@@ -1410,8 +1287,8 @@ impl VkRenderer {
.wait_dst_stage_mask(&wait_stages)
.command_buffers(&cmds)
.signal_semaphores(&signal_semaphores);
- self.device
- .queue_submit(self.queue, &[submit], in_flight)
+ self.core.device
+ .queue_submit(self.core.queue, &[submit], in_flight)
.expect("Queue submit failed");
let swapchains = [self.swapchain];
@@ -1420,7 +1297,7 @@ impl VkRenderer {
.wait_semaphores(&signal_semaphores)
.swapchains(&swapchains)
.image_indices(&image_indices);
- match self.swapchain_loader.queue_present(self.queue, &present) {
+ match self.swapchain_loader.queue_present(self.core.queue, &present) {
Ok(suboptimal) => {
if suboptimal {
self.swapchain_dirty = true;
@@ -1441,15 +1318,15 @@ impl VkRenderer {
impl Drop for VkRenderer {
fn drop(&mut self) {
unsafe {
- let _ = self.device.device_wait_idle();
+ let _ = self.core.device.device_wait_idle();
let mut frames = std::mem::take(&mut self.frames);
for frame in &mut frames {
- self.device.destroy_semaphore(frame.image_available, None);
- self.device.destroy_fence(frame.in_flight, None);
+ self.core.device.destroy_semaphore(frame.image_available, None);
+ self.core.device.destroy_fence(frame.in_flight, None);
let mut vertex = std::mem::replace(&mut frame.vertex, AllocatedBuffer::null());
- if let Some(allocator) = self.allocator.as_mut() {
- destroy_cpu_buffer(&self.device, allocator, &mut vertex);
+ if let Some(allocator) = self.core.allocator.as_mut() {
+ destroy_cpu_buffer(&self.core.device, allocator, &mut vertex);
}
}
@@ -1458,38 +1335,31 @@ impl Drop for VkRenderer {
self.swapchain_loader.destroy_swapchain(self.swapchain, None);
}
- if let Some(allocator) = self.allocator.as_mut() {
- self.text.destroy(&self.device, allocator);
+ if let Some(allocator) = self.core.allocator.as_mut() {
+ self.text.destroy(&self.core.device, allocator);
}
- self.device.destroy_sampler(self.backdrop_sampler, None);
- if let Some(allocator) = self.allocator.as_mut() {
- self.scene.destroy(&self.device, allocator);
+ self.core.device.destroy_sampler(self.backdrop_sampler, None);
+ if let Some(allocator) = self.core.allocator.as_mut() {
+ self.scene.destroy(&self.core.device, allocator);
+ self.image.destroy(&self.core.device, allocator);
}
let mut window_info = std::mem::replace(&mut self.window_info, AllocatedBuffer::null());
- if let Some(allocator) = self.allocator.as_mut() {
- destroy_cpu_buffer(&self.device, allocator, &mut window_info);
+ if let Some(allocator) = self.core.allocator.as_mut() {
+ destroy_cpu_buffer(&self.core.device, allocator, &mut window_info);
}
- self.device.destroy_descriptor_pool(self.descriptor_pool, None);
- self.device
+ self.core.device.destroy_descriptor_pool(self.descriptor_pool, None);
+ self.core.device
.destroy_descriptor_set_layout(self.descriptor_set_layout, None);
- self.device.destroy_pipeline(self.pipeline, None);
- self.device.destroy_pipeline_layout(self.pipeline_layout, None);
- self.device.destroy_shader_module(self.shader_module, None);
- self.device.destroy_render_pass(self.render_pass, None);
- self.device.destroy_render_pass(self.render_pass_load, None);
- self.device.destroy_command_pool(self.command_pool, None);
-
- // The allocator must go before the device it allocates from.
- drop(self.allocator.take());
-
- self.device.destroy_device(None);
- self.surface_loader.destroy_surface(self.surface, None);
- if let Some((loader, messenger)) = self.debug.take() {
- loader.destroy_debug_utils_messenger(messenger, None);
- }
- self.instance.destroy_instance(None);
+ self.core.device.destroy_pipeline(self.pipeline, None);
+ self.core.device.destroy_pipeline_layout(self.pipeline_layout, None);
+ self.core.device.destroy_shader_module(self.shader_module, None);
+ self.core.device.destroy_render_pass(self.render_pass, None);
+ self.core.device.destroy_render_pass(self.render_pass_load, None);
+ self.core.surface_loader.destroy_surface(self.surface, None);
+ // The rest (allocator, command pool, device, instance) is the
+ // core's Drop, which runs after this body.
}
}
}
diff --git a/src/widget/model.rs b/src/widget/model.rs
index f97c9b9..3b69a78 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -1238,6 +1238,7 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
Prim::Arc { cx, cy, radius, thickness, start, end, color } => ctx.arc(cx, cy, radius, thickness, start, end, color),
Prim::Vector { x1, y1, x2, y2, thickness, color, cap } => ctx.vector(x1, y1, x2, y2, thickness, color, cap),
Prim::Circle { cx, cy, radius, color } => ctx.circle(cx, cy, radius, color),
+ Prim::Image { image, rect, alpha } => ctx.image(image, rect, alpha),
}
}
// The legacy default `paint_self` drained `all_quads`, which carries the focus