graphic design tool
git clone https://git.lucas.co/cce-designer.git
refactor: consume cce_ui::vk — the local vk module graduates to the toolkit
The renderer/text/scene stages and shaders now live in cce-ui (commit d84e138
there); this crate keeps only its usage. src/vk/, shader.wgsl, and
shader_3d.wgsl are deleted; app/render import cce_ui::vk; vk-smoke drives the
library module instead of a #[path] copy; ash/gpu-allocator/naga arrive
transitively through cce-ui.
The only behavioral delta is the merged 2D shader, which adds the engine
dialect's wavy-blob sentinel branch (inert unless a vertex carries the -999
clip_circle marker). Verified: designer launches clean on the shared module.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RFkXq68hDwVDMKnu9fckz3
Cargo.toml | 5 -
src/app.rs | 18 +-
src/main.rs | 1 -
src/render.rs | 2 +-
src/shader.wgsl | 106 ----
src/shader_3d.wgsl | 72 ---
src/vk/glyph.wgsl | 42 --
src/vk/mod.rs | 19 -
src/vk/renderer.rs | 1400 ----------------------------------------------------
src/vk/scene.rs | 726 ---------------------------
src/vk/text.rs | 771 -----------------------------
src/vk_smoke.rs | 3 +-
12 files changed, 11 insertions(+), 3154 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 481f74f..b33b36a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,11 +24,6 @@ opencl3 = "0.9"
libc = "0.2"
log = "0.4"
env_logger = "0.11"
-# Raw-Vulkan migration (versions match what wgpu 24 already pulls in, so the
-# dependency tree gains no duplicate builds while both stacks coexist).
-ash = "0.38"
-gpu-allocator = { version = "0.27", default-features = false, features = ["vulkan"] }
-naga = { version = "24", features = ["wgsl-in", "spv-out"] }
[[bin]]
name = "cce-designer"
diff --git a/src/app.rs b/src/app.rs
index f35cf67..3c86817 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -43,7 +43,7 @@ use glam::{Mat4, Vec3};
use crate::geometry::*;
use crate::shortcut::{ShortcutManager, Action};
-use crate::vk::{SceneDraw, TextSpan};
+use cce_ui::vk::{SceneDraw, TextSpan};
use cce_ui::engine::Vertex;
use crate::window::{AppState, WindowEvent};
@@ -963,19 +963,19 @@ pub struct ViewportUniforms {
}
pub struct State {
- pub renderer: crate::vk::VkRenderer,
+ pub renderer: cce_ui::vk::VkRenderer,
pub font_system: FontSystem,
pub swash_cache: glyphon::SwashCache,
pub window: XdgWindow,
pub wl_surface: wl_surface::WlSurface,
pub vertex_data: Vec<Vertex>,
- pub mesh_cube: crate::vk::MeshId,
- pub mesh_viewport_bg: crate::vk::MeshId,
- pub mesh_spheres: crate::vk::MeshId,
- pub mesh_grid: crate::vk::MeshId,
- pub mesh_origin: crate::vk::MeshId,
- pub mesh_pivot: crate::vk::MeshId,
+ pub mesh_cube: cce_ui::vk::MeshId,
+ pub mesh_viewport_bg: cce_ui::vk::MeshId,
+ pub mesh_spheres: cce_ui::vk::MeshId,
+ pub mesh_grid: cce_ui::vk::MeshId,
+ pub mesh_origin: cce_ui::vk::MeshId,
+ pub mesh_pivot: cce_ui::vk::MeshId,
pub vertex_count_spheres: u32,
pub node_color: [f32; 3],
pub grid_color: [f32; 3],
@@ -2447,7 +2447,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
let surface_ptr = wl_surface.id().as_ptr() as *mut std::ffi::c_void;
let corner_radius = cce_ui::color::backplate_corner_radius() * scale as f32;
let mut renderer = unsafe {
- crate::vk::VkRenderer::new(display_ptr, surface_ptr, pw, ph, corner_radius)
+ cce_ui::vk::VkRenderer::new(display_ptr, surface_ptr, pw, ph, corner_radius)
};
// Bundled fonts only (the designer's UI uses bundled families).
let font_system = cce_ui::create_font_system();
diff --git a/src/main.rs b/src/main.rs
index 964ba9e..463da13 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -42,7 +42,6 @@ use glam::{Mat4, Vec3};
pub mod app;
pub mod viewport_3d;
-pub mod vk;
pub mod api;
pub mod window;
pub mod geometry;
diff --git a/src/render.rs b/src/render.rs
index 3390111..5fef997 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -17,7 +17,7 @@ use cce_ui::engine::Vertex;
use crate::geometry::{
network_sphere_vertices_with_errors,
};
-use crate::vk::TextSpan;
+use cce_ui::vk::TextSpan;
use cce_ui::engine::{
push_widget_vertices, push_extra_quad_vertices,
push_extra_quad_vertices_clipped, push_arc_background_vertices,
diff --git a/src/shader.wgsl b/src/shader.wgsl
deleted file mode 100644
index 2050430..0000000
--- a/src/shader.wgsl
+++ /dev/null
@@ -1,106 +0,0 @@
-@group(0) @binding(0) var t_backdrop: texture_2d<f32>;
-@group(0) @binding(1) var s_backdrop: sampler;
-
-struct WindowInfo {
- window_size: vec2<f32>,
- corner_radius: f32,
- padding: f32,
-}
-
-@group(0) @binding(2) var<uniform> window_info: WindowInfo;
-
-fn is_outside_window_corners(pos: vec2<f32>) -> bool {
- let w = window_info.window_size.x;
- let h = window_info.window_size.y;
- let r = window_info.corner_radius;
-
- // Top-left
- if (pos.x < r && pos.y < r) {
- let dx = pos.x - r;
- let dy = pos.y - r;
- return (dx * dx + dy * dy) > r * r;
- }
- // Top-right
- if (pos.x > w - r && pos.y < r) {
- let dx = pos.x - (w - r);
- let dy = pos.y - r;
- return (dx * dx + dy * dy) > r * r;
- }
- // Bottom-left
- if (pos.x < r && pos.y > h - r) {
- let dx = pos.x - r;
- let dy = pos.y - (h - r);
- return (dx * dx + dy * dy) > r * r;
- }
- // Bottom-right
- if (pos.x > w - r && pos.y > h - r) {
- let dx = pos.x - (w - r);
- let dy = pos.y - (h - r);
- return (dx * dx + dy * dy) > r * r;
- }
- // Boundary check
- if (pos.x < 0.0 || pos.x > w || pos.y < 0.0 || pos.y > h) {
- return true;
- }
- return false;
-}
-
-struct VertexOutput {
- @builtin(position) clip_position: vec4f,
- @location(0) color: vec4f,
- @location(1) clip_circle: vec3f,
-}
-
-@vertex
-fn vs_main(
- @location(0) position: vec2f,
- @location(1) color: vec4f,
- @location(2) clip_circle: vec3f,
-) -> VertexOutput {
- var out: VertexOutput;
- out.clip_position = vec4f(position, 0.0, 1.0);
- out.color = color;
- out.clip_circle = clip_circle;
- return out;
-}
-
-@fragment
-fn fs_main(in: VertexOutput) -> @location(0) vec4f {
- if (is_outside_window_corners(in.clip_position.xy)) {
- discard;
- }
- if (in.clip_circle.z > 0.0) {
- let dx = in.clip_position.x - in.clip_circle.x;
- let dy = in.clip_position.y - in.clip_circle.y;
- if (dx * dx + dy * dy > in.clip_circle.z * in.clip_circle.z) {
- discard;
- }
- }
-
- if (in.color.a < 0.0) {
- let tex_size = vec2f(textureDimensions(t_backdrop));
- let clean_backdrop = textureSample(t_backdrop, s_backdrop, in.clip_position.xy / tex_size);
-
- var blurred = vec4f(0.0);
- var total_weight = 0.0;
-
- // 7x7 Gaussian blur kernel
- for (var x = -3.0; x <= 3.0; x += 1.0) {
- for (var y = -3.0; y <= 3.0; y += 1.0) {
- let offset = vec2f(x, y) * 2.0; // sample every 2 pixels for a wider blur
- let sample_uv = (in.clip_position.xy + offset) / tex_size;
- let weight = exp(-(x*x + y*y) / (2.0 * 2.0 * 2.0));
- blurred += textureSample(t_backdrop, s_backdrop, sample_uv) * weight;
- total_weight += weight;
- }
- }
-
- let backdrop_color = blurred / total_weight;
- let opacity = -in.color.a;
- let plate_color = vec4f(in.color.rgb, 1.0);
- let blurred_plate = mix(backdrop_color, plate_color, opacity);
- return mix(clean_backdrop, blurred_plate, opacity);
- }
-
- return in.color;
-}
diff --git a/src/shader_3d.wgsl b/src/shader_3d.wgsl
deleted file mode 100644
index 193dcda..0000000
--- a/src/shader_3d.wgsl
+++ /dev/null
@@ -1,72 +0,0 @@
-struct Uniforms {
- mvp: mat4x4<f32>,
- window_size: vec2<f32>,
- window_radius: f32,
- padding: f32,
-}
-
-@group(0) @binding(0) var<uniform> uniforms: Uniforms;
-
-fn is_outside_window_corners(pos: vec2<f32>) -> bool {
- let w = uniforms.window_size.x;
- let h = uniforms.window_size.y;
- let r = uniforms.window_radius;
-
- // Top-left
- if (pos.x < r && pos.y < r) {
- let dx = pos.x - r;
- let dy = pos.y - r;
- return (dx * dx + dy * dy) > r * r;
- }
- // Top-right
- if (pos.x > w - r && pos.y < r) {
- let dx = pos.x - (w - r);
- let dy = pos.y - r;
- return (dx * dx + dy * dy) > r * r;
- }
- // Bottom-left
- if (pos.x < r && pos.y > h - r) {
- let dx = pos.x - r;
- let dy = pos.y - (h - r);
- return (dx * dx + dy * dy) > r * r;
- }
- // Bottom-right
- if (pos.x > w - r && pos.y > h - r) {
- let dx = pos.x - (w - r);
- let dy = pos.y - (h - r);
- return (dx * dx + dy * dy) > r * r;
- }
- // Boundary check
- if (pos.x < 0.0 || pos.x > w || pos.y < 0.0 || pos.y > h) {
- return true;
- }
- return false;
-}
-
-struct VertexOutput {
- @builtin(position) position: vec4f,
- @location(0) color: vec3f,
-};
-
-@vertex
-fn vs_main(
- @location(0) position: vec3f,
- @location(1) color: vec3f,
-) -> VertexOutput {
- var out: VertexOutput;
- if (abs(position.z - 9.99) < 0.01) {
- out.position = vec4f(position.xy, 0.9999, 1.0);
- } else {
- out.position = uniforms.mvp * vec4f(position, 1.0);
- }
- out.color = color;
- return out;
-}
-
-@fragment
-fn fs_main(in: VertexOutput) -> @location(0) vec4f {
- if (is_outside_window_corners(in.position.xy)) {
- discard;
- }
- return vec4f(in.color, 1.0);
-}
diff --git a/src/vk/glyph.wgsl b/src/vk/glyph.wgsl
deleted file mode 100644
index 8cd26e0..0000000
--- a/src/vk/glyph.wgsl
+++ /dev/null
@@ -1,42 +0,0 @@
-// Glyph-atlas pipeline for the ash text stage. Mask glyphs are stored as
-// white-with-alpha texels, color (emoji) glyphs as-is with a white vertex
-// color — one multiply covers both.
-
-@group(0) @binding(0) var t_atlas: texture_2d<f32>;
-@group(0) @binding(1) var s_atlas: sampler;
-
-struct VertexOutput {
- @builtin(position) clip_position: vec4f,
- @location(0) uv: vec2f,
- @location(1) color: vec4f,
- @location(2) clip_circle: vec3f,
-}
-
-@vertex
-fn vs_main(
- @location(0) position: vec2f,
- @location(1) uv: vec2f,
- @location(2) color: vec4f,
- @location(3) clip_circle: vec3f,
-) -> VertexOutput {
- var out: VertexOutput;
- out.clip_position = vec4f(position, 0.0, 1.0);
- out.uv = uv;
- out.color = color;
- out.clip_circle = clip_circle;
- return out;
-}
-
-@fragment
-fn fs_main(in: VertexOutput) -> @location(0) vec4f {
- // Same circle clip as shader.wgsl (framebuffer px): used by the circular
- // network pane's curved rim labels.
- if (in.clip_circle.z > 0.0) {
- let dx = in.clip_position.x - in.clip_circle.x;
- let dy = in.clip_position.y - in.clip_circle.y;
- if (dx * dx + dy * dy > in.clip_circle.z * in.clip_circle.z) {
- discard;
- }
- }
- return in.color * textureSample(t_atlas, s_atlas, in.uv);
-}
diff --git a/src/vk/mod.rs b/src/vk/mod.rs
deleted file mode 100644
index 7527939..0000000
--- a/src/vk/mod.rs
+++ /dev/null
@@ -1,19 +0,0 @@
-//! Raw-Vulkan (ash) rendering foundation — milestone 1 of the wgpu → ash migration.
-//!
-//! `VkRenderer` reproduces the designer's 2D UI pipeline (`shader.wgsl`: NDC quads
-//! with window-corner rounding, circle clipping, and the blur-behind branch) directly
-//! on Vulkan: instance (+ validation layers when available), VK_KHR_wayland_surface,
-//! swapchain, gpu-allocator memory, and the WGSL compiled to SPIR-V through naga at
-//! startup — so `shader.wgsl` stays the single source of truth for both stacks.
-//!
-//! The 3D pass, glyphon text, and curved-text passes stay on wgpu until the cutover;
-//! `vk-smoke` is the standalone proof of this renderer.
-
-mod renderer;
-mod scene;
-mod text;
-
-pub use renderer::VkRenderer;
-#[allow(unused_imports)] // MeshId is cutover API; smoke uses ids via inference
-pub use scene::{MeshId, SceneDraw, Vertex3D};
-pub use text::TextSpan;
diff --git a/src/vk/renderer.rs b/src/vk/renderer.rs
deleted file mode 100644
index 054f7a8..0000000
--- a/src/vk/renderer.rs
+++ /dev/null
@@ -1,1400 +0,0 @@
-//! The ash renderer. One graphics queue, a classic render pass, two frames in
-//! flight, FIFO (vsync) presentation. Memory goes through gpu-allocator; the
-//! descriptor set mirrors `shader.wgsl`'s @group(0): sampled backdrop texture
-//! (binding 0), sampler (binding 1), WindowInfo uniform (binding 2). The backdrop
-//! 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 ash::vk;
-use gpu_allocator::vulkan::{
- Allocation, AllocationCreateDesc, AllocationScheme, Allocator, AllocatorCreateDesc,
-};
-use gpu_allocator::MemoryLocation;
-
-use cce_ui::engine::Vertex;
-
-use super::scene::{MeshId, SceneDraw, SceneStage, Vertex3D};
-use super::text::{TextSpan, TextStage};
-
-const FRAMES_IN_FLIGHT: usize = 2;
-const VALIDATION_LAYER: &CStr = c"VK_LAYER_KHRONOS_validation";
-
-pub(crate) struct AllocatedBuffer {
- pub(crate) buffer: vk::Buffer,
- pub(crate) allocation: Option<Allocation>,
- pub(crate) size: vk::DeviceSize,
-}
-
-impl AllocatedBuffer {
- pub(crate) fn null() -> Self {
- AllocatedBuffer { buffer: vk::Buffer::null(), allocation: None, size: 0 }
- }
-}
-
-/// Create a host-visible buffer bound to gpu-allocator memory.
-pub(crate) fn create_cpu_buffer(
- device: &ash::Device,
- allocator: &mut Allocator,
- size: vk::DeviceSize,
- usage: vk::BufferUsageFlags,
- name: &str,
-) -> AllocatedBuffer {
- unsafe {
- let buffer = device
- .create_buffer(
- &vk::BufferCreateInfo::default()
- .size(size)
- .usage(usage)
- .sharing_mode(vk::SharingMode::EXCLUSIVE),
- None,
- )
- .expect("Failed to create buffer");
- let requirements = device.get_buffer_memory_requirements(buffer);
- let allocation = allocator
- .allocate(&AllocationCreateDesc {
- name,
- requirements,
- location: MemoryLocation::CpuToGpu,
- linear: true,
- allocation_scheme: AllocationScheme::GpuAllocatorManaged,
- })
- .expect("Failed to allocate buffer memory");
- device
- .bind_buffer_memory(buffer, allocation.memory(), allocation.offset())
- .expect("Failed to bind buffer memory");
- AllocatedBuffer { buffer, allocation: Some(allocation), size }
- }
-}
-
-/// Destroy a buffer and return its memory to the allocator.
-pub(crate) fn destroy_cpu_buffer(
- device: &ash::Device,
- allocator: &mut Allocator,
- buf: &mut AllocatedBuffer,
-) {
- unsafe {
- self::destroy_buffer_handle(device, buf.buffer);
- }
- if let Some(allocation) = buf.allocation.take() {
- let _ = allocator.free(allocation);
- }
- buf.buffer = vk::Buffer::null();
- buf.size = 0;
-}
-
-unsafe fn destroy_buffer_handle(device: &ash::Device, buffer: vk::Buffer) {
- if buffer != vk::Buffer::null() {
- device.destroy_buffer(buffer, None);
- }
-}
-
-struct Frame {
- cmd: vk::CommandBuffer,
- image_available: vk::Semaphore,
- in_flight: vk::Fence,
- vertex: AllocatedBuffer,
- vertex_count: u32,
-}
-
-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,
- surface_format: vk::SurfaceFormatKHR,
- extent: vk::Extent2D,
- swapchain_images: Vec<vk::Image>,
- swapchain_views: Vec<vk::ImageView>,
- framebuffers: Vec<vk::Framebuffer>,
- // One per swapchain image (not per frame in flight): present waits on the
- // semaphore tied to the image being presented.
- render_finished: Vec<vk::Semaphore>,
-
- render_pass: vk::RenderPass,
- /// UI pass over a backdrop copy: loadOp LOAD, initial layout TRANSFER_DST.
- /// Framebuffers are shared with `render_pass` (compatible attachments).
- render_pass_load: vk::RenderPass,
- descriptor_set_layout: vk::DescriptorSetLayout,
- pipeline_layout: vk::PipelineLayout,
- pipeline: vk::Pipeline,
- shader_module: vk::ShaderModule,
-
- descriptor_pool: vk::DescriptorPool,
- descriptor_set: vk::DescriptorSet,
- backdrop_sampler: vk::Sampler,
- window_info: AllocatedBuffer,
-
- command_pool: vk::CommandPool,
- frames: Vec<Frame>,
- frame_index: usize,
- text: TextStage,
- scene: SceneStage,
-
- desired_extent: vk::Extent2D,
- corner_radius_px: f32,
- swapchain_dirty: bool,
-}
-
-/// Compile WGSL to SPIR-V. The Y-flip between wgpu NDC (Y-up) and Vulkan NDC
-/// (Y-down) is handled with a negative-height viewport (like wgpu-hal), NOT in
-/// the shader — flipping in the shader would reverse screen-space winding and
-/// break the 3D pipeline's back-face culling.
-pub(crate) fn compile_wgsl(source: &str) -> Vec<u32> {
- let module = naga::front::wgsl::parse_str(source).expect("WGSL parse failed");
- let info = naga::valid::Validator::new(
- naga::valid::ValidationFlags::all(),
- naga::valid::Capabilities::empty(),
- )
- .validate(&module)
- .expect("WGSL validation failed");
- let options = naga::back::spv::Options {
- lang_version: (1, 0),
- flags: naga::back::spv::WriterFlags::LABEL_VARYINGS,
- ..Default::default()
- };
- naga::back::spv::write_vec(&module, &info, &options, None).expect("SPIR-V write failed")
-}
-
-const COLOR_RANGE: vk::ImageSubresourceRange = vk::ImageSubresourceRange {
- aspect_mask: vk::ImageAspectFlags::COLOR,
- base_mip_level: 0,
- level_count: 1,
- base_array_layer: 0,
- layer_count: 1,
-};
-
-/// One-time submit: clear a color image and leave it in SHADER_READ_ONLY, so a
-/// freshly created backdrop is always legal to sample.
-pub(crate) fn clear_image_to_shader_read(
- device: &ash::Device,
- queue: vk::Queue,
- command_pool: vk::CommandPool,
- image: vk::Image,
-) {
- unsafe {
- 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 init 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(COLOR_RANGE)],
- );
- device.cmd_clear_color_image(
- cmd,
- image,
- vk::ImageLayout::TRANSFER_DST_OPTIMAL,
- &vk::ClearColorValue { float32: [0.0, 0.0, 0.0, 0.0] },
- &[COLOR_RANGE],
- );
- 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(COLOR_RANGE)],
- );
- device.end_command_buffer(cmd).unwrap();
- let cmds = [cmd];
- let submit = vk::SubmitInfo::default().command_buffers(&cmds);
- device
- .queue_submit(queue, &[submit], vk::Fence::null())
- .expect("Init submit failed");
- device.queue_wait_idle(queue).expect("Init wait failed");
- device.free_command_buffers(command_pool, &cmds);
- }
-}
-
-/// The wgpu-convention viewport: Y flipped via negative height (Vulkan >= 1.1).
-pub(crate) fn flipped_viewport(extent: vk::Extent2D) -> vk::Viewport {
- vk::Viewport {
- x: 0.0,
- y: extent.height as f32,
- width: extent.width as f32,
- height: -(extent.height as f32),
- min_depth: 0.0,
- max_depth: 1.0,
- }
-}
-
-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
- /// `display_ptr` and `surface_ptr` must be live `wl_display` / `wl_surface`
- /// pointers that outlive the renderer (same contract as `WgpuAdapter::new`).
- pub unsafe fn new(
- display_ptr: *mut c_void,
- surface_ptr: *mut c_void,
- width: u32,
- 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-designer";
- 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");
-
- // Surface format: prefer sRGB (wgpu's get_default_config sorts sRGB first,
- // so this matches the colors the app renders today).
- let formats = surface_loader
- .get_physical_device_surface_formats(physical_device, surface)
- .expect("No surface formats");
- let surface_format = formats
- .iter()
- .copied()
- .find(|f| {
- (f.format == vk::Format::B8G8R8A8_SRGB || f.format == vk::Format::R8G8B8A8_SRGB)
- && f.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR
- })
- .unwrap_or(formats[0]);
-
- // Render pass: one color attachment, clear -> present.
- let attachments = [vk::AttachmentDescription::default()
- .format(surface_format.format)
- .samples(vk::SampleCountFlags::TYPE_1)
- .load_op(vk::AttachmentLoadOp::CLEAR)
- .store_op(vk::AttachmentStoreOp::STORE)
- .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
- .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
- .initial_layout(vk::ImageLayout::UNDEFINED)
- .final_layout(vk::ImageLayout::PRESENT_SRC_KHR)];
- let color_refs = [vk::AttachmentReference::default()
- .attachment(0)
- .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)];
- let subpasses = [vk::SubpassDescription::default()
- .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
- .color_attachments(&color_refs)];
- // One dependency shared VERBATIM by both UI pass variants: framebuffer
- // compatibility requires identical dependencies (only load/store ops and
- // image layouts may differ), so this unions the clear case (previous
- // frame's color output) with the load case (the backdrop copy's write).
- let dependencies = [vk::SubpassDependency::default()
- .src_subpass(vk::SUBPASS_EXTERNAL)
- .dst_subpass(0)
- .src_stage_mask(
- vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
- | vk::PipelineStageFlags::TRANSFER,
- )
- .src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
- .dst_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
- .dst_access_mask(
- vk::AccessFlags::COLOR_ATTACHMENT_READ | vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
- )];
- let render_pass = device
- .create_render_pass(
- &vk::RenderPassCreateInfo::default()
- .attachments(&attachments)
- .subpasses(&subpasses)
- .dependencies(&dependencies),
- None,
- )
- .expect("Failed to create render pass");
-
- // Variant used when a backdrop copy precedes the UI pass: keep the copied
- // pixels (LOAD) and take the image from the copy's TRANSFER_DST layout.
- let attachments_load = [vk::AttachmentDescription::default()
- .format(surface_format.format)
- .samples(vk::SampleCountFlags::TYPE_1)
- .load_op(vk::AttachmentLoadOp::LOAD)
- .store_op(vk::AttachmentStoreOp::STORE)
- .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
- .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
- .initial_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
- .final_layout(vk::ImageLayout::PRESENT_SRC_KHR)];
- let render_pass_load = device
- .create_render_pass(
- &vk::RenderPassCreateInfo::default()
- .attachments(&attachments_load)
- .subpasses(&subpasses)
- .dependencies(&dependencies),
- None,
- )
- .expect("Failed to create load render pass");
-
- // Descriptor set layout mirroring shader.wgsl @group(0): naga maps WGSL
- // texture/sampler/uniform bindings 1:1 onto set 0 descriptor bindings.
- 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),
- vk::DescriptorSetLayoutBinding::default()
- .binding(2)
- .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
- .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 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 pipeline layout");
-
- // Pipeline from shader.wgsl (both entry points live in one SPIR-V module).
- let spirv = compile_wgsl(include_str!("../shader.wgsl"));
- let shader_module = device
- .create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&spirv), None)
- .expect("Failed to create 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"),
- ];
-
- // Vertex layout = cce_ui::engine::Vertex: pos vec2f, color vec4f, clip vec3f.
- let vertex_bindings = [vk::VertexInputBindingDescription::default()
- .binding(0)
- .stride(std::mem::size_of::<Vertex>() 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::R32G32B32A32_SFLOAT)
- .offset(8),
- vk::VertexInputAttributeDescription::default()
- .location(2)
- .binding(0)
- .format(vk::Format::R32G32B32_SFLOAT)
- .offset(24),
- ];
- 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);
- // wgpu::BlendState::ALPHA_BLENDING.
- 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 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,
- surface_format.format,
- initial_extent,
- FRAMES_IN_FLIGHT,
- min_uniform_align,
- );
- clear_image_to_shader_read(&device, queue, command_pool, scene.backdrop_image);
-
- // Matches the wgpu backdrop sampler: linear, clamp-to-edge.
- let backdrop_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 sampler");
-
- let window_info = create_cpu_buffer(
- &device,
- &mut allocator,
- 16,
- vk::BufferUsageFlags::UNIFORM_BUFFER,
- "window-info",
- );
-
- let pool_sizes = [
- vk::DescriptorPoolSize::default()
- .ty(vk::DescriptorType::SAMPLED_IMAGE)
- .descriptor_count(1),
- vk::DescriptorPoolSize::default()
- .ty(vk::DescriptorType::SAMPLER)
- .descriptor_count(1),
- vk::DescriptorPoolSize::default()
- .ty(vk::DescriptorType::UNIFORM_BUFFER)
- .descriptor_count(1),
- ];
- let descriptor_pool = device
- .create_descriptor_pool(
- &vk::DescriptorPoolCreateInfo::default()
- .max_sets(1)
- .pool_sizes(&pool_sizes),
- None,
- )
- .expect("Failed to create descriptor pool");
- let descriptor_set = device
- .allocate_descriptor_sets(
- &vk::DescriptorSetAllocateInfo::default()
- .descriptor_pool(descriptor_pool)
- .set_layouts(&set_layouts),
- )
- .expect("Failed to allocate descriptor set")[0];
-
- let image_infos = [vk::DescriptorImageInfo::default()
- .image_view(scene.backdrop_view)
- .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)];
- let sampler_infos = [vk::DescriptorImageInfo::default().sampler(backdrop_sampler)];
- let buffer_infos = [vk::DescriptorBufferInfo::default()
- .buffer(window_info.buffer)
- .offset(0)
- .range(16)];
- 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),
- vk::WriteDescriptorSet::default()
- .dst_set(descriptor_set)
- .dst_binding(2)
- .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
- .buffer_info(&buffer_infos),
- ],
- &[],
- );
-
- // Per-frame command buffers, sync, and vertex buffers.
- let cmds = device
- .allocate_command_buffers(
- &vk::CommandBufferAllocateInfo::default()
- .command_pool(command_pool)
- .level(vk::CommandBufferLevel::PRIMARY)
- .command_buffer_count(FRAMES_IN_FLIGHT as u32),
- )
- .expect("Failed to allocate command buffers");
- let frames = cmds
- .into_iter()
- .map(|cmd| Frame {
- cmd,
- image_available: device
- .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)
- .unwrap(),
- in_flight: device
- .create_fence(
- &vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED),
- None,
- )
- .unwrap(),
- vertex: create_cpu_buffer(
- &device,
- &mut allocator,
- 64 * 1024,
- vk::BufferUsageFlags::VERTEX_BUFFER,
- "vertices",
- ),
- vertex_count: 0,
- })
- .collect();
-
- let text = TextStage::new(&device, &mut allocator, render_pass, FRAMES_IN_FLIGHT);
-
- let swapchain_loader = ash::khr::swapchain::Device::new(&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(),
- surface_format,
- extent: vk::Extent2D { width: width.max(1), height: height.max(1) },
- swapchain_views: Vec::new(),
- framebuffers: Vec::new(),
- render_finished: Vec::new(),
- render_pass,
- render_pass_load,
- descriptor_set_layout,
- pipeline_layout,
- pipeline,
- shader_module,
- descriptor_pool,
- descriptor_set,
- backdrop_sampler,
- window_info,
- command_pool,
- frames,
- frame_index: 0,
- text,
- scene,
- desired_extent: vk::Extent2D { width: width.max(1), height: height.max(1) },
- corner_radius_px,
- swapchain_dirty: false,
- };
- renderer.create_swapchain();
- renderer.write_window_info();
- // The swapchain may have settled on a different extent than requested;
- // keep the backdrop targets in lockstep.
- renderer.sync_backdrop_targets();
- renderer
- }
-
- fn write_window_info(&mut self) {
- let data = [
- self.extent.width as f32,
- self.extent.height as f32,
- self.corner_radius_px,
- 0.0f32,
- ];
- if let Some(allocation) = self.window_info.allocation.as_mut() {
- allocation.mapped_slice_mut().unwrap()[..16]
- .copy_from_slice(bytemuck::cast_slice(&data));
- }
- }
-
- fn destroy_swapchain_resources(&mut self) {
- unsafe {
- for fb in self.framebuffers.drain(..) {
- self.device.destroy_framebuffer(fb, None);
- }
- for view in self.swapchain_views.drain(..) {
- self.device.destroy_image_view(view, None);
- }
- self.swapchain_images.clear();
- for sem in self.render_finished.drain(..) {
- self.device.destroy_semaphore(sem, None);
- }
- }
- }
-
- fn create_swapchain(&mut self) {
- unsafe {
- let caps = self
- .surface_loader
- .get_physical_device_surface_capabilities(self.physical_device, self.surface)
- .expect("Failed to query surface capabilities");
-
- // Wayland reports "extent defined by the swapchain" (u32::MAX); use the
- // size the configure events gave us.
- let extent = if caps.current_extent.width != u32::MAX {
- caps.current_extent
- } else {
- vk::Extent2D {
- width: self
- .desired_extent
- .width
- .clamp(caps.min_image_extent.width, caps.max_image_extent.width.max(1)),
- height: self
- .desired_extent
- .height
- .clamp(caps.min_image_extent.height, caps.max_image_extent.height.max(1)),
- }
- };
-
- let mut image_count = caps.min_image_count + 1;
- if caps.max_image_count > 0 {
- image_count = image_count.min(caps.max_image_count);
- }
-
- // Prefer premultiplied (what the DE's other clients pick), else opaque,
- // else whatever the surface offers.
- let composite_alpha = [
- vk::CompositeAlphaFlagsKHR::PRE_MULTIPLIED,
- vk::CompositeAlphaFlagsKHR::OPAQUE,
- vk::CompositeAlphaFlagsKHR::POST_MULTIPLIED,
- vk::CompositeAlphaFlagsKHR::INHERIT,
- ]
- .into_iter()
- .find(|&mode| caps.supported_composite_alpha.contains(mode))
- .unwrap_or(vk::CompositeAlphaFlagsKHR::OPAQUE);
-
- let old_swapchain = self.swapchain;
- self.swapchain = self
- .swapchain_loader
- .create_swapchain(
- &vk::SwapchainCreateInfoKHR::default()
- .surface(self.surface)
- .min_image_count(image_count)
- .image_format(self.surface_format.format)
- .image_color_space(self.surface_format.color_space)
- .image_extent(extent)
- .image_array_layers(1)
- .image_usage(
- vk::ImageUsageFlags::COLOR_ATTACHMENT
- | vk::ImageUsageFlags::TRANSFER_DST,
- )
- .image_sharing_mode(vk::SharingMode::EXCLUSIVE)
- .pre_transform(caps.current_transform)
- .composite_alpha(composite_alpha)
- .present_mode(vk::PresentModeKHR::FIFO)
- .clipped(true)
- .old_swapchain(old_swapchain),
- None,
- )
- .expect("Failed to create swapchain");
- if old_swapchain != vk::SwapchainKHR::null() {
- self.swapchain_loader.destroy_swapchain(old_swapchain, None);
- }
- self.extent = extent;
-
- let images = self
- .swapchain_loader
- .get_swapchain_images(self.swapchain)
- .expect("Failed to get swapchain images");
- self.swapchain_images = images.clone();
- let subresource_range = vk::ImageSubresourceRange::default()
- .aspect_mask(vk::ImageAspectFlags::COLOR)
- .base_mip_level(0)
- .level_count(1)
- .base_array_layer(0)
- .layer_count(1);
- for image in &images {
- let view = self
- .device
- .create_image_view(
- &vk::ImageViewCreateInfo::default()
- .image(*image)
- .view_type(vk::ImageViewType::TYPE_2D)
- .format(self.surface_format.format)
- .subresource_range(subresource_range),
- None,
- )
- .expect("Failed to create swapchain view");
- self.swapchain_views.push(view);
- let attachments = [view];
- let fb = self
- .device
- .create_framebuffer(
- &vk::FramebufferCreateInfo::default()
- .render_pass(self.render_pass)
- .attachments(&attachments)
- .width(extent.width)
- .height(extent.height)
- .layers(1),
- None,
- )
- .expect("Failed to create framebuffer");
- self.framebuffers.push(fb);
- self.render_finished.push(
- self.device
- .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)
- .unwrap(),
- );
- }
- }
- }
-
- fn recreate_swapchain(&mut self) {
- unsafe {
- let _ = self.device.device_wait_idle();
- }
- self.destroy_swapchain_resources();
- self.create_swapchain();
- self.write_window_info();
- self.sync_backdrop_targets();
- }
-
- /// Recreate backdrop + depth at the surface size (device must be idle),
- /// re-point the UI descriptor at the new view, and make the fresh image
- /// legal to sample.
- fn sync_backdrop_targets(&mut self) {
- self.scene.resize(
- &self.device,
- self.allocator.as_mut().unwrap(),
- self.extent,
- );
- clear_image_to_shader_read(
- &self.device,
- self.queue,
- self.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(
- &[vk::WriteDescriptorSet::default()
- .dst_set(self.descriptor_set)
- .dst_binding(0)
- .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
- .image_info(&image_infos)],
- &[],
- );
- }
- }
-
- /// Upload a 3D mesh (Vertex3D: position + color); the id is stable for the
- /// renderer's lifetime.
- pub fn create_mesh(&mut self, verts: &[Vertex3D]) -> MeshId {
- self.scene
- .create_mesh(&self.device, self.allocator.as_mut().unwrap(), verts)
- }
-
- /// Replace a mesh's vertices. Waits for the GPU to go idle first — geometry
- /// updates are rare (settings changes, graph rebuilds), matching the app.
- #[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();
- }
- self.scene
- .update_mesh(&self.device, self.allocator.as_mut().unwrap(), id, verts);
- }
-
- /// Stage the 3D scene for the next `draw_frame`. Draws render into the
- /// backdrop image (scissored to the viewport pane, physical pixels), which
- /// is copied beneath the UI and doubles as the blur-behind source. Frames
- /// with no staged scene reuse the previous backdrop — the ash equivalent of
- /// the app's viewport-changed cache.
- pub fn stage_scene(&mut self, scissor: (u32, u32, u32, u32), draws: Vec<SceneDraw>) {
- self.scene.stage(scissor, draws);
- }
-
- /// Request a new physical size (from xdg configure / scale changes). Applied
- /// lazily on the next `draw_frame`.
- pub fn resize(&mut self, width: u32, height: u32) {
- let extent = vk::Extent2D { width: width.max(1), height: height.max(1) };
- if extent.width != self.extent.width || extent.height != self.extent.height {
- self.desired_extent = extent;
- self.swapchain_dirty = true;
- }
- }
-
- // Used at cutover, when scale changes re-derive the radius; vk-smoke fixes it at init.
- #[allow(dead_code)]
- pub fn set_corner_radius(&mut self, radius_px: f32) {
- self.corner_radius_px = radius_px;
- // Written on the next swapchain rebuild or draw-idle moment; a mapped write
- // here would race in-flight frames, so route it through the dirty path.
- self.swapchain_dirty = true;
- }
-
- /// Stage text for the next `draw_frame`: shape-cache misses are rasterized
- /// into the glyph atlas and vertices are built against the current extent.
- /// Mirrors `glyphon::TextRenderer::prepare`.
- pub fn prepare_text(
- &mut self,
- font_system: &mut glyphon::FontSystem,
- swash_cache: &mut glyphon::SwashCache,
- spans: &[TextSpan<'_>],
- ) {
- self.text.prepare(font_system, swash_cache, spans, self.extent);
- }
-
- /// Render one frame: 2D geometry, then any text staged via `prepare_text`.
- /// Returns false if the frame was skipped (swapchain rebuild); the caller
- /// just draws again next tick.
- pub fn draw_frame(&mut self, verts: &[Vertex]) -> bool {
- if self.swapchain_dirty {
- self.swapchain_dirty = false;
- self.recreate_swapchain();
- }
-
- unsafe {
- let frame_index = self.frame_index;
- let (in_flight, image_available) = {
- let f = &self.frames[frame_index];
- (f.in_flight, f.image_available)
- };
- self.device
- .wait_for_fences(&[in_flight], true, u64::MAX)
- .expect("Fence wait failed");
-
- let image_index = match self.swapchain_loader.acquire_next_image(
- self.swapchain,
- u64::MAX,
- image_available,
- vk::Fence::null(),
- ) {
- Ok((index, suboptimal)) => {
- if suboptimal {
- self.swapchain_dirty = true;
- }
- index
- }
- Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
- self.swapchain_dirty = true;
- return false;
- }
- Err(e) => {
- log::error!("acquire_next_image failed: {e:?}");
- return false;
- }
- };
-
- self.device.reset_fences(&[in_flight]).unwrap();
-
- // Upload vertices into this frame's buffer (its fence has signaled, so
- // the GPU is done with it; growing swaps in a fresh buffer).
- let bytes: &[u8] = bytemuck::cast_slice(verts);
- let needed = bytes.len() as vk::DeviceSize;
- 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);
- self.frames[frame_index].vertex = create_cpu_buffer(
- &self.device,
- allocator,
- needed.next_power_of_two(),
- vk::BufferUsageFlags::VERTEX_BUFFER,
- "vertices",
- );
- }
- if !bytes.is_empty() {
- self.frames[frame_index]
- .vertex
- .allocation
- .as_mut()
- .unwrap()
- .mapped_slice_mut()
- .unwrap()[..bytes.len()]
- .copy_from_slice(bytes);
- }
- self.frames[frame_index].vertex_count = verts.len() as u32;
- self.text.write_frame_buffers(
- &self.device,
- self.allocator.as_mut().unwrap(),
- frame_index,
- );
- self.scene.write_frame_uniforms(
- &self.device,
- self.allocator.as_mut().unwrap(),
- frame_index,
- self.corner_radius_px,
- );
-
- // Record.
- let cmd = self.frames[frame_index].cmd;
- self.device
- .begin_command_buffer(cmd, &vk::CommandBufferBeginInfo::default())
- .unwrap();
- self.text.record_upload(&self.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);
-
- // 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.
- let use_backdrop = self.scene.backdrop_valid;
- if use_backdrop {
- if !scene_recorded {
- // Reused backdrop is in SHADER_READ_ONLY from last frame.
- self.device.cmd_pipeline_barrier(
- cmd,
- vk::PipelineStageFlags::FRAGMENT_SHADER,
- vk::PipelineStageFlags::TRANSFER,
- vk::DependencyFlags::empty(),
- &[],
- &[],
- &[vk::ImageMemoryBarrier::default()
- .src_access_mask(vk::AccessFlags::SHADER_READ)
- .dst_access_mask(vk::AccessFlags::TRANSFER_READ)
- .old_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
- .new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
- .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
- .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
- .image(self.scene.backdrop_image)
- .subresource_range(COLOR_RANGE)],
- );
- }
- let swapchain_image = self.swapchain_images[image_index as usize];
- self.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(swapchain_image)
- .subresource_range(COLOR_RANGE)],
- );
- let subresource = vk::ImageSubresourceLayers::default()
- .aspect_mask(vk::ImageAspectFlags::COLOR)
- .layer_count(1);
- self.device.cmd_copy_image(
- cmd,
- self.scene.backdrop_image,
- vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
- swapchain_image,
- vk::ImageLayout::TRANSFER_DST_OPTIMAL,
- &[vk::ImageCopy::default()
- .src_subresource(subresource)
- .dst_subresource(subresource)
- .extent(vk::Extent3D {
- width: self.extent.width,
- height: self.extent.height,
- depth: 1,
- })],
- );
- // Backdrop back to sampleable for the UI pass's blur plates.
- self.device.cmd_pipeline_barrier(
- cmd,
- vk::PipelineStageFlags::TRANSFER,
- vk::PipelineStageFlags::FRAGMENT_SHADER,
- vk::DependencyFlags::empty(),
- &[],
- &[],
- &[vk::ImageMemoryBarrier::default()
- .src_access_mask(vk::AccessFlags::TRANSFER_READ)
- .dst_access_mask(vk::AccessFlags::SHADER_READ)
- .old_layout(vk::ImageLayout::TRANSFER_SRC_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(self.scene.backdrop_image)
- .subresource_range(COLOR_RANGE)],
- );
- }
-
- let frame = &self.frames[frame_index];
- let clear_values = [vk::ClearValue {
- color: vk::ClearColorValue { float32: [0.0, 0.0, 0.0, 0.0] },
- }];
- let (ui_pass, ui_clear_values): (vk::RenderPass, &[vk::ClearValue]) = if use_backdrop {
- (self.render_pass_load, &[])
- } else {
- (self.render_pass, &clear_values)
- };
- self.device.cmd_begin_render_pass(
- cmd,
- &vk::RenderPassBeginInfo::default()
- .render_pass(ui_pass)
- .framebuffer(self.framebuffers[image_index as usize])
- .render_area(vk::Rect2D {
- offset: vk::Offset2D { x: 0, y: 0 },
- extent: self.extent,
- })
- .clear_values(ui_clear_values),
- vk::SubpassContents::INLINE,
- );
- self.device
- .cmd_set_viewport(cmd, 0, &[flipped_viewport(self.extent)]);
- self.device.cmd_set_scissor(
- cmd,
- 0,
- &[vk::Rect2D {
- 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]);
- self.device.cmd_draw(cmd, frame.vertex_count, 1, 0, 0);
- }
- self.text.record_draw(&self.device, cmd, frame_index);
- self.device.cmd_end_render_pass(cmd);
- self.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).
- let wait_semaphores = [image_available];
- let wait_stages = [vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
- | vk::PipelineStageFlags::TRANSFER];
- let cmds = [cmd];
- let signal_semaphores = [self.render_finished[image_index as usize]];
- let submit = vk::SubmitInfo::default()
- .wait_semaphores(&wait_semaphores)
- .wait_dst_stage_mask(&wait_stages)
- .command_buffers(&cmds)
- .signal_semaphores(&signal_semaphores);
- self.device
- .queue_submit(self.queue, &[submit], in_flight)
- .expect("Queue submit failed");
-
- let swapchains = [self.swapchain];
- let image_indices = [image_index];
- let present = vk::PresentInfoKHR::default()
- .wait_semaphores(&signal_semaphores)
- .swapchains(&swapchains)
- .image_indices(&image_indices);
- match self.swapchain_loader.queue_present(self.queue, &present) {
- Ok(suboptimal) => {
- if suboptimal {
- self.swapchain_dirty = true;
- }
- }
- Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
- self.swapchain_dirty = true;
- }
- Err(e) => log::error!("queue_present failed: {e:?}"),
- }
-
- self.frame_index = (self.frame_index + 1) % FRAMES_IN_FLIGHT;
- }
- true
- }
-}
-
-impl Drop for VkRenderer {
- fn drop(&mut self) {
- unsafe {
- let _ = self.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);
- 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);
- }
- }
-
- self.destroy_swapchain_resources();
- if self.swapchain != vk::SwapchainKHR::null() {
- self.swapchain_loader.destroy_swapchain(self.swapchain, None);
- }
-
- if let Some(allocator) = self.allocator.as_mut() {
- self.text.destroy(&self.device, allocator);
- }
-
- self.device.destroy_sampler(self.backdrop_sampler, None);
- if let Some(allocator) = self.allocator.as_mut() {
- self.scene.destroy(&self.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);
- }
-
- self.device.destroy_descriptor_pool(self.descriptor_pool, None);
- self.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);
- }
- }
-}
diff --git a/src/vk/scene.rs b/src/vk/scene.rs
deleted file mode 100644
index ca79cdd..0000000
--- a/src/vk/scene.rs
+++ /dev/null
@@ -1,726 +0,0 @@
-//! 3D scene stage: the ash port of the app's "3D canvas render pass". Draws
-//! Vertex3D meshes (shader_3d.wgsl: mvp transform, z=9.99 background-quad
-//! special case, window-corner discard) into the full-size backdrop image with
-//! a depth buffer, scissored to the viewport pane. The renderer then copies the
-//! backdrop into the swapchain image and draws the UI pass over it — the same
-//! image doubles as the blur-behind source for the 2D shader, replacing
-//! milestone 1's 1x1 placeholder.
-//!
-//! Meshes are handle-based (`MeshId`); per-draw uniforms (mvp + window info) go
-//! into one dynamic-offset uniform buffer per frame in flight, so a frame's
-//! draws share a single descriptor set.
-
-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};
-
-/// Layout-identical to the app's `geometry::Vertex3D` (bytemuck-castable at cutover).
-#[repr(C)]
-#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
-pub struct Vertex3D {
- pub position: [f32; 3],
- pub color: [f32; 3],
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct MeshId(usize);
-
-/// One draw in the staged scene: a mesh under an mvp. The window-size/radius
-/// tail of shader_3d's uniform block is filled in by the renderer.
-pub struct SceneDraw {
- pub mesh: MeshId,
- pub mvp: [[f32; 4]; 4],
-}
-
-/// shader_3d.wgsl's uniform block.
-#[repr(C)]
-#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
-struct SceneUniforms {
- mvp: [[f32; 4]; 4],
- window_size: [f32; 2],
- window_radius: f32,
- _padding: f32,
-}
-
-const UNIFORM_SIZE: vk::DeviceSize = std::mem::size_of::<SceneUniforms>() as vk::DeviceSize;
-
-struct Mesh {
- buffer: AllocatedBuffer,
- count: u32,
-}
-
-struct StagedScene {
- scissor: (u32, u32, u32, u32),
- draws: Vec<SceneDraw>,
-}
-
-struct SceneFrame {
- uniforms: AllocatedBuffer,
- descriptor_set: vk::DescriptorSet,
- draw_count: u32,
-}
-
-pub(crate) struct SceneStage {
- render_pass: vk::RenderPass,
- pipeline: vk::Pipeline,
- pipeline_layout: vk::PipelineLayout,
- descriptor_set_layout: vk::DescriptorSetLayout,
- descriptor_pool: vk::DescriptorPool,
- shader_module: vk::ShaderModule,
- uniform_stride: vk::DeviceSize,
-
- format: vk::Format,
- extent: vk::Extent2D,
- pub(crate) backdrop_image: vk::Image,
- pub(crate) backdrop_view: vk::ImageView,
- backdrop_allocation: Option<Allocation>,
- depth_image: vk::Image,
- depth_view: vk::ImageView,
- depth_allocation: Option<Allocation>,
- framebuffer: vk::Framebuffer,
-
- meshes: Vec<Mesh>,
- frames: Vec<SceneFrame>,
- staged: Option<StagedScene>,
- /// True once the backdrop holds rendered content worth copying to screen.
- pub(crate) backdrop_valid: bool,
-}
-
-impl SceneStage {
- pub(crate) fn new(
- device: &ash::Device,
- allocator: &mut Allocator,
- format: vk::Format,
- extent: vk::Extent2D,
- frames_in_flight: usize,
- min_uniform_align: vk::DeviceSize,
- ) -> Self {
- unsafe {
- // Offscreen pass: color -> TRANSFER_SRC (copied to the swapchain
- // right after), depth is transient.
- let attachments = [
- vk::AttachmentDescription::default()
- .format(format)
- .samples(vk::SampleCountFlags::TYPE_1)
- .load_op(vk::AttachmentLoadOp::CLEAR)
- .store_op(vk::AttachmentStoreOp::STORE)
- .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
- .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
- .initial_layout(vk::ImageLayout::UNDEFINED)
- .final_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL),
- vk::AttachmentDescription::default()
- .format(vk::Format::D32_SFLOAT)
- .samples(vk::SampleCountFlags::TYPE_1)
- .load_op(vk::AttachmentLoadOp::CLEAR)
- .store_op(vk::AttachmentStoreOp::DONT_CARE)
- .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
- .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
- .initial_layout(vk::ImageLayout::UNDEFINED)
- .final_layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL),
- ];
- let color_refs = [vk::AttachmentReference::default()
- .attachment(0)
- .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)];
- let depth_ref = vk::AttachmentReference::default()
- .attachment(1)
- .layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
- let subpasses = [vk::SubpassDescription::default()
- .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
- .color_attachments(&color_refs)
- .depth_stencil_attachment(&depth_ref)];
- let dependencies = [
- // Prior frame sampled the backdrop (blur plates) and used the depth
- // image; execution dependency before we overwrite from UNDEFINED.
- vk::SubpassDependency::default()
- .src_subpass(vk::SUBPASS_EXTERNAL)
- .dst_subpass(0)
- .src_stage_mask(
- vk::PipelineStageFlags::FRAGMENT_SHADER
- | vk::PipelineStageFlags::LATE_FRAGMENT_TESTS,
- )
- .src_access_mask(vk::AccessFlags::empty())
- .dst_stage_mask(
- vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
- | vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS,
- )
- .dst_access_mask(
- vk::AccessFlags::COLOR_ATTACHMENT_WRITE
- | vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE,
- ),
- // The copy to the swapchain reads the color attachment right after.
- vk::SubpassDependency::default()
- .src_subpass(0)
- .dst_subpass(vk::SUBPASS_EXTERNAL)
- .src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
- .src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE)
- .dst_stage_mask(vk::PipelineStageFlags::TRANSFER)
- .dst_access_mask(vk::AccessFlags::TRANSFER_READ),
- ];
- let render_pass = device
- .create_render_pass(
- &vk::RenderPassCreateInfo::default()
- .attachments(&attachments)
- .subpasses(&subpasses)
- .dependencies(&dependencies),
- None,
- )
- .expect("Failed to create scene render pass");
-
- let bindings = [vk::DescriptorSetLayoutBinding::default()
- .binding(0)
- .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC)
- .descriptor_count(1)
- .stage_flags(vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT)];
- let descriptor_set_layout = device
- .create_descriptor_set_layout(
- &vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
- None,
- )
- .expect("Failed to create scene descriptor set layout");
- let set_layouts_one = [descriptor_set_layout];
- let pipeline_layout = device
- .create_pipeline_layout(
- &vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts_one),
- None,
- )
- .expect("Failed to create scene pipeline layout");
-
- let spirv = compile_wgsl(include_str!("../shader_3d.wgsl"));
- let shader_module = device
- .create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&spirv), None)
- .expect("Failed to create 3D 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::<Vertex3D>() as u32)
- .input_rate(vk::VertexInputRate::VERTEX)];
- let vertex_attributes = [
- vk::VertexInputAttributeDescription::default()
- .location(0)
- .binding(0)
- .format(vk::Format::R32G32B32_SFLOAT)
- .offset(0),
- vk::VertexInputAttributeDescription::default()
- .location(1)
- .binding(0)
- .format(vk::Format::R32G32B32_SFLOAT)
- .offset(12),
- ];
- 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);
- // wgpu pipeline_3d: CCW front, back-face culling. Winding survives
- // because the renderer flips Y via negative viewport height (like
- // wgpu-hal), not in the shader.
- let rasterization = vk::PipelineRasterizationStateCreateInfo::default()
- .polygon_mode(vk::PolygonMode::FILL)
- .cull_mode(vk::CullModeFlags::BACK)
- .front_face(vk::FrontFace::COUNTER_CLOCKWISE)
- .line_width(1.0);
- let multisample = vk::PipelineMultisampleStateCreateInfo::default()
- .rasterization_samples(vk::SampleCountFlags::TYPE_1);
- let depth_stencil = vk::PipelineDepthStencilStateCreateInfo::default()
- .depth_test_enable(true)
- .depth_write_enable(true)
- .depth_compare_op(vk::CompareOp::LESS);
- 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)
- .depth_stencil_state(&depth_stencil)
- .color_blend_state(&color_blend)
- .dynamic_state(&dynamic_state)
- .layout(pipeline_layout)
- .render_pass(render_pass)
- .subpass(0)],
- None,
- )
- .expect("Failed to create 3D pipeline")[0];
-
- let uniform_stride = UNIFORM_SIZE.next_multiple_of(min_uniform_align.max(1));
-
- let pool_sizes = [vk::DescriptorPoolSize::default()
- .ty(vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC)
- .descriptor_count(frames_in_flight as u32)];
- let descriptor_pool = device
- .create_descriptor_pool(
- &vk::DescriptorPoolCreateInfo::default()
- .max_sets(frames_in_flight as u32)
- .pool_sizes(&pool_sizes),
- None,
- )
- .expect("Failed to create scene descriptor pool");
- let set_layouts: Vec<vk::DescriptorSetLayout> =
- vec![descriptor_set_layout; frames_in_flight];
- let sets = device
- .allocate_descriptor_sets(
- &vk::DescriptorSetAllocateInfo::default()
- .descriptor_pool(descriptor_pool)
- .set_layouts(&set_layouts),
- )
- .expect("Failed to allocate scene descriptor sets");
- let frames: Vec<SceneFrame> = sets
- .into_iter()
- .map(|descriptor_set| {
- let uniforms = create_cpu_buffer(
- device,
- allocator,
- uniform_stride * 16,
- vk::BufferUsageFlags::UNIFORM_BUFFER,
- "scene-uniforms",
- );
- SceneFrame { uniforms, descriptor_set, draw_count: 0 }
- })
- .collect();
- for frame in &frames {
- Self::write_descriptor(device, frame);
- }
-
- let mut stage = SceneStage {
- render_pass,
- pipeline,
- pipeline_layout,
- descriptor_set_layout,
- descriptor_pool,
- shader_module,
- uniform_stride,
- format,
- extent: vk::Extent2D { width: 0, height: 0 },
- backdrop_image: vk::Image::null(),
- backdrop_view: vk::ImageView::null(),
- backdrop_allocation: None,
- depth_image: vk::Image::null(),
- depth_view: vk::ImageView::null(),
- depth_allocation: None,
- framebuffer: vk::Framebuffer::null(),
- meshes: Vec::new(),
- frames,
- staged: None,
- backdrop_valid: false,
- };
- stage.resize(device, allocator, extent);
- stage
- }
- }
-
- fn write_descriptor(device: &ash::Device, frame: &SceneFrame) {
- let buffer_infos = [vk::DescriptorBufferInfo::default()
- .buffer(frame.uniforms.buffer)
- .offset(0)
- .range(UNIFORM_SIZE)];
- unsafe {
- device.update_descriptor_sets(
- &[vk::WriteDescriptorSet::default()
- .dst_set(frame.descriptor_set)
- .dst_binding(0)
- .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC)
- .buffer_info(&buffer_infos)],
- &[],
- );
- }
- }
-
- fn destroy_targets(&mut self, device: &ash::Device, allocator: &mut Allocator) {
- unsafe {
- if self.framebuffer != vk::Framebuffer::null() {
- device.destroy_framebuffer(self.framebuffer, None);
- self.framebuffer = vk::Framebuffer::null();
- }
- if self.backdrop_view != vk::ImageView::null() {
- device.destroy_image_view(self.backdrop_view, None);
- device.destroy_image(self.backdrop_image, None);
- self.backdrop_view = vk::ImageView::null();
- self.backdrop_image = vk::Image::null();
- }
- if self.depth_view != vk::ImageView::null() {
- device.destroy_image_view(self.depth_view, None);
- device.destroy_image(self.depth_image, None);
- self.depth_view = vk::ImageView::null();
- self.depth_image = vk::Image::null();
- }
- }
- if let Some(a) = self.backdrop_allocation.take() {
- let _ = allocator.free(a);
- }
- if let Some(a) = self.depth_allocation.take() {
- let _ = allocator.free(a);
- }
- }
-
- /// (Re)create the backdrop + depth targets at `extent`. Caller must have the
- /// device idle (the renderer's swapchain-rebuild path guarantees it) and must
- /// re-point the UI descriptor at the new `backdrop_view` and re-init its layout.
- pub(crate) fn resize(
- &mut self,
- device: &ash::Device,
- allocator: &mut Allocator,
- extent: vk::Extent2D,
- ) {
- if extent == self.extent && self.framebuffer != vk::Framebuffer::null() {
- return;
- }
- self.destroy_targets(device, allocator);
- self.extent = extent;
- self.backdrop_valid = false;
- unsafe {
- let backdrop_image = device
- .create_image(
- &vk::ImageCreateInfo::default()
- .image_type(vk::ImageType::TYPE_2D)
- .format(self.format)
- .extent(vk::Extent3D {
- width: extent.width,
- height: extent.height,
- depth: 1,
- })
- .mip_levels(1)
- .array_layers(1)
- .samples(vk::SampleCountFlags::TYPE_1)
- .tiling(vk::ImageTiling::OPTIMAL)
- .usage(
- vk::ImageUsageFlags::COLOR_ATTACHMENT
- | vk::ImageUsageFlags::SAMPLED
- | vk::ImageUsageFlags::TRANSFER_SRC
- | vk::ImageUsageFlags::TRANSFER_DST,
- )
- .initial_layout(vk::ImageLayout::UNDEFINED),
- None,
- )
- .expect("Failed to create backdrop image");
- let requirements = device.get_image_memory_requirements(backdrop_image);
- let allocation = allocator
- .allocate(&AllocationCreateDesc {
- name: "backdrop",
- requirements,
- location: MemoryLocation::GpuOnly,
- linear: false,
- allocation_scheme: AllocationScheme::GpuAllocatorManaged,
- })
- .expect("Failed to allocate backdrop memory");
- device
- .bind_image_memory(backdrop_image, allocation.memory(), allocation.offset())
- .expect("Failed to bind backdrop memory");
- let backdrop_view = device
- .create_image_view(
- &vk::ImageViewCreateInfo::default()
- .image(backdrop_image)
- .view_type(vk::ImageViewType::TYPE_2D)
- .format(self.format)
- .subresource_range(
- vk::ImageSubresourceRange::default()
- .aspect_mask(vk::ImageAspectFlags::COLOR)
- .level_count(1)
- .layer_count(1),
- ),
- None,
- )
- .expect("Failed to create backdrop view");
- self.backdrop_image = backdrop_image;
- self.backdrop_view = backdrop_view;
- self.backdrop_allocation = Some(allocation);
-
- let depth_image = device
- .create_image(
- &vk::ImageCreateInfo::default()
- .image_type(vk::ImageType::TYPE_2D)
- .format(vk::Format::D32_SFLOAT)
- .extent(vk::Extent3D {
- width: extent.width,
- height: extent.height,
- depth: 1,
- })
- .mip_levels(1)
- .array_layers(1)
- .samples(vk::SampleCountFlags::TYPE_1)
- .tiling(vk::ImageTiling::OPTIMAL)
- .usage(vk::ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT)
- .initial_layout(vk::ImageLayout::UNDEFINED),
- None,
- )
- .expect("Failed to create depth image");
- let requirements = device.get_image_memory_requirements(depth_image);
- let allocation = allocator
- .allocate(&AllocationCreateDesc {
- name: "depth",
- requirements,
- location: MemoryLocation::GpuOnly,
- linear: false,
- allocation_scheme: AllocationScheme::GpuAllocatorManaged,
- })
- .expect("Failed to allocate depth memory");
- device
- .bind_image_memory(depth_image, allocation.memory(), allocation.offset())
- .expect("Failed to bind depth memory");
- let depth_view = device
- .create_image_view(
- &vk::ImageViewCreateInfo::default()
- .image(depth_image)
- .view_type(vk::ImageViewType::TYPE_2D)
- .format(vk::Format::D32_SFLOAT)
- .subresource_range(
- vk::ImageSubresourceRange::default()
- .aspect_mask(vk::ImageAspectFlags::DEPTH)
- .level_count(1)
- .layer_count(1),
- ),
- None,
- )
- .expect("Failed to create depth view");
- self.depth_image = depth_image;
- self.depth_view = depth_view;
- self.depth_allocation = Some(allocation);
-
- let attachments = [self.backdrop_view, self.depth_view];
- self.framebuffer = device
- .create_framebuffer(
- &vk::FramebufferCreateInfo::default()
- .render_pass(self.render_pass)
- .attachments(&attachments)
- .width(extent.width)
- .height(extent.height)
- .layers(1),
- None,
- )
- .expect("Failed to create scene framebuffer");
- }
- }
-
- pub(crate) fn create_mesh(
- &mut self,
- device: &ash::Device,
- allocator: &mut Allocator,
- verts: &[Vertex3D],
- ) -> MeshId {
- let bytes: &[u8] = bytemuck::cast_slice(verts);
- let mut buffer = create_cpu_buffer(
- device,
- allocator,
- (bytes.len() as vk::DeviceSize).max(64),
- vk::BufferUsageFlags::VERTEX_BUFFER,
- "mesh",
- );
- if !bytes.is_empty() {
- buffer.allocation.as_mut().unwrap().mapped_slice_mut().unwrap()[..bytes.len()]
- .copy_from_slice(bytes);
- }
- self.meshes.push(Mesh { buffer, count: verts.len() as u32 });
- MeshId(self.meshes.len() - 1)
- }
-
- /// Replace a mesh's vertices. Caller must have the device idle: meshes may be
- /// referenced by in-flight frames (geometry updates are rare — settings
- /// changes and graph rebuilds — so a wait is acceptable here).
- #[allow(dead_code)] // cutover API: the app's rebuild_scene_geometry path
- pub(crate) fn update_mesh(
- &mut self,
- device: &ash::Device,
- allocator: &mut Allocator,
- id: MeshId,
- verts: &[Vertex3D],
- ) {
- let mesh = &mut self.meshes[id.0];
- let bytes: &[u8] = bytemuck::cast_slice(verts);
- let needed = bytes.len() as vk::DeviceSize;
- if needed > mesh.buffer.size {
- let mut old = std::mem::replace(&mut mesh.buffer, AllocatedBuffer::null());
- destroy_cpu_buffer(device, allocator, &mut old);
- mesh.buffer = create_cpu_buffer(
- device,
- allocator,
- needed.next_power_of_two(),
- vk::BufferUsageFlags::VERTEX_BUFFER,
- "mesh",
- );
- }
- if !bytes.is_empty() {
- mesh.buffer.allocation.as_mut().unwrap().mapped_slice_mut().unwrap()[..bytes.len()]
- .copy_from_slice(bytes);
- }
- mesh.count = verts.len() as u32;
- }
-
- pub(crate) fn stage(&mut self, scissor: (u32, u32, u32, u32), draws: Vec<SceneDraw>) {
- self.staged = Some(StagedScene { scissor, draws });
- }
-
- /// After the frame fence: write this frame's per-draw uniforms (mvp + the
- /// window-corner info shader_3d shares with the 2D shader).
- pub(crate) fn write_frame_uniforms(
- &mut self,
- device: &ash::Device,
- allocator: &mut Allocator,
- frame_index: usize,
- corner_radius_px: f32,
- ) {
- let Some(staged) = &self.staged else {
- self.frames[frame_index].draw_count = 0;
- return;
- };
- let frame = &mut self.frames[frame_index];
- let needed = self.uniform_stride * staged.draws.len().max(1) as vk::DeviceSize;
- if needed > frame.uniforms.size {
- let mut old = std::mem::replace(&mut frame.uniforms, AllocatedBuffer::null());
- destroy_cpu_buffer(device, allocator, &mut old);
- frame.uniforms = create_cpu_buffer(
- device,
- allocator,
- needed.next_power_of_two(),
- vk::BufferUsageFlags::UNIFORM_BUFFER,
- "scene-uniforms",
- );
- Self::write_descriptor(device, frame);
- }
- let window_size = [self.extent.width as f32, self.extent.height as f32];
- let mapped = frame.uniforms.allocation.as_mut().unwrap().mapped_slice_mut().unwrap();
- for (i, draw) in staged.draws.iter().enumerate() {
- let uniforms = SceneUniforms {
- mvp: draw.mvp,
- window_size,
- window_radius: corner_radius_px,
- _padding: 0.0,
- };
- let offset = (self.uniform_stride as usize) * i;
- mapped[offset..offset + UNIFORM_SIZE as usize]
- .copy_from_slice(bytemuck::bytes_of(&uniforms));
- }
- frame.draw_count = staged.draws.len() as u32;
- }
-
- /// Record the offscreen scene pass. Consumes the staged scene; afterwards the
- /// backdrop is in TRANSFER_SRC layout, ready for the swapchain copy. Returns
- /// false if nothing was staged.
- pub(crate) fn record(
- &mut self,
- device: &ash::Device,
- cmd: vk::CommandBuffer,
- frame_index: usize,
- ) -> bool {
- let Some(staged) = self.staged.take() else {
- return false;
- };
- let frame = &self.frames[frame_index];
- unsafe {
- let clear_values = [
- vk::ClearValue { color: vk::ClearColorValue { float32: [0.0, 0.0, 0.0, 0.0] } },
- vk::ClearValue {
- depth_stencil: vk::ClearDepthStencilValue { depth: 1.0, stencil: 0 },
- },
- ];
- device.cmd_begin_render_pass(
- cmd,
- &vk::RenderPassBeginInfo::default()
- .render_pass(self.render_pass)
- .framebuffer(self.framebuffer)
- .render_area(vk::Rect2D {
- offset: vk::Offset2D { x: 0, y: 0 },
- extent: self.extent,
- })
- .clear_values(&clear_values),
- vk::SubpassContents::INLINE,
- );
- // Negative-height viewport: wgpu's Y-up NDC without touching winding.
- device.cmd_set_viewport(
- cmd,
- 0,
- &[vk::Viewport {
- x: 0.0,
- y: self.extent.height as f32,
- width: self.extent.width as f32,
- height: -(self.extent.height as f32),
- min_depth: 0.0,
- max_depth: 1.0,
- }],
- );
- let (sx, sy, sw, sh) = staged.scissor;
- let sx = sx.min(self.extent.width);
- let sy = sy.min(self.extent.height);
- device.cmd_set_scissor(
- cmd,
- 0,
- &[vk::Rect2D {
- offset: vk::Offset2D { x: sx as i32, y: sy as i32 },
- extent: vk::Extent2D {
- width: sw.min(self.extent.width - sx),
- height: sh.min(self.extent.height - sy),
- },
- }],
- );
- device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline);
- for (i, draw) in staged.draws.iter().enumerate() {
- let mesh = &self.meshes[draw.mesh.0];
- if mesh.count == 0 {
- continue;
- }
- device.cmd_bind_descriptor_sets(
- cmd,
- vk::PipelineBindPoint::GRAPHICS,
- self.pipeline_layout,
- 0,
- &[frame.descriptor_set],
- &[(self.uniform_stride as u32) * i as u32],
- );
- device.cmd_bind_vertex_buffers(cmd, 0, &[mesh.buffer.buffer], &[0]);
- device.cmd_draw(cmd, mesh.count, 1, 0, 0);
- }
- device.cmd_end_render_pass(cmd);
- }
- self.backdrop_valid = true;
- true
- }
-
- pub(crate) fn destroy(&mut self, device: &ash::Device, allocator: &mut Allocator) {
- self.destroy_targets(device, allocator);
- unsafe {
- for frame in &mut self.frames {
- let mut uniforms = std::mem::replace(&mut frame.uniforms, AllocatedBuffer::null());
- destroy_cpu_buffer(device, allocator, &mut uniforms);
- }
- for mesh in &mut self.meshes {
- let mut buffer = std::mem::replace(&mut mesh.buffer, AllocatedBuffer::null());
- destroy_cpu_buffer(device, allocator, &mut buffer);
- }
- 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);
- device.destroy_render_pass(self.render_pass, None);
- }
- }
-}
diff --git a/src/vk/text.rs b/src/vk/text.rs
deleted file mode 100644
index ecc5c63..0000000
--- a/src/vk/text.rs
+++ /dev/null
@@ -1,771 +0,0 @@
-//! Text on ash: cosmic-text shaping (reached through glyphon's re-export, so the
-//! shaping behavior and fonts are byte-identical to the wgpu path) + swash
-//! rasterization into a self-managed RGBA glyph atlas, drawn by the glyph.wgsl
-//! pipeline inside the renderer's render pass.
-//!
-//! `TextSpan` mirrors `glyphon::TextArea` (buffer + position + scale + bounds +
-//! default color) so the eventual cutover from `text_renderer.prepare(...)` is
-//! mechanical.
-//!
-//! Atlas strategy: shelf packing into a 1024² RGBA8 image with a CPU mirror.
-//! When new glyphs land, the whole mirror is re-uploaded before the next render
-//! pass (bounded 4 MiB, and only on glyph-miss frames); if the atlas fills, it is
-//! cleared and repacked with just the current frame's glyphs. Mask glyphs are
-//! stored white-with-alpha, color (emoji) glyphs as-is drawn with a white vertex
-//! color — glyph.wgsl multiplies either by the vertex color.
-
-use std::collections::HashMap;
-
-use ash::vk;
-use gpu_allocator::vulkan::{
- Allocation, AllocationCreateDesc, AllocationScheme, Allocator,
-};
-use gpu_allocator::MemoryLocation;
-
-use glyphon::cosmic_text::{Buffer as TextBuffer, CacheKey, SwashContent};
-use glyphon::{FontSystem, SwashCache};
-
-use super::renderer::{compile_wgsl, create_cpu_buffer, destroy_cpu_buffer, AllocatedBuffer};
-
-const ATLAS_SIZE: u32 = 1024;
-const ATLAS_PAD: u32 = 1;
-
-/// One shaped text run to draw. `left`/`top` are physical pixels and `scale`
-/// multiplies the shaped (logical) glyph positions — the same contract as
-/// glyphon::TextArea, where callers pass `label.x * scale`.
-pub struct TextSpan<'a> {
- pub buffer: &'a TextBuffer,
- pub left: f32,
- pub top: f32,
- pub scale: f32,
- /// Physical-pixel clip rect (left, top, right, bottom); None = whole surface.
- pub bounds: Option<[i32; 4]>,
- /// 0..=1 sRGB + alpha, applied to glyphs without their own color.
- pub default_color: [f32; 4],
- /// Rotate the span's glyph quads by (radians, center_x, center_y) in
- /// physical pixels — the circular network pane's curved rim labels.
- pub rotation: Option<(f32, f32, f32)>,
- /// Fragment circle clip (center_x, center_y, radius) in physical pixels;
- /// zero radius disables (matches shader.wgsl's clip_circle).
- pub clip_circle: [f32; 3],
-}
-
-#[repr(C)]
-#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
-struct GlyphVertex {
- position: [f32; 2],
- uv: [f32; 2],
- color: [f32; 4],
- clip_circle: [f32; 3],
-}
-
-#[derive(Clone, Copy)]
-struct GlyphEntry {
- /// Atlas texel rect.
- u: u32,
- v: u32,
- w: u32,
- h: u32,
- /// Raster placement offsets (from swash).
- left: i32,
- top: i32,
- is_color: bool,
- /// Zero-sized raster (spaces): nothing to draw, but cached to skip re-rastering.
- empty: bool,
-}
-
-struct Shelf {
- cursor_x: u32,
- cursor_y: u32,
- row_height: u32,
-}
-
-impl Shelf {
- fn new() -> Self {
- Shelf { cursor_x: ATLAS_PAD, cursor_y: ATLAS_PAD, row_height: 0 }
- }
-
- fn insert(&mut self, w: u32, h: u32) -> Option<(u32, u32)> {
- if w > ATLAS_SIZE - 2 * ATLAS_PAD || h > ATLAS_SIZE - 2 * ATLAS_PAD {
- return None;
- }
- if self.cursor_x + w + ATLAS_PAD > ATLAS_SIZE {
- self.cursor_x = ATLAS_PAD;
- self.cursor_y += self.row_height + ATLAS_PAD;
- self.row_height = 0;
- }
- if self.cursor_y + h + ATLAS_PAD > ATLAS_SIZE {
- return None;
- }
- let pos = (self.cursor_x, self.cursor_y);
- self.cursor_x += w + ATLAS_PAD;
- self.row_height = self.row_height.max(h);
- Some(pos)
- }
-}
-
-struct TextFrame {
- vertex: AllocatedBuffer,
- vertex_count: u32,
- staging: AllocatedBuffer,
- /// Atlas generation this frame's staging buffer last uploaded.
- uploaded_generation: u64,
-}
-
-pub(crate) struct TextStage {
- pipeline: vk::Pipeline,
- pipeline_layout: vk::PipelineLayout,
- descriptor_set_layout: vk::DescriptorSetLayout,
- descriptor_pool: vk::DescriptorPool,
- descriptor_set: vk::DescriptorSet,
- shader_module: vk::ShaderModule,
- sampler: vk::Sampler,
-
- atlas_image: vk::Image,
- atlas_view: vk::ImageView,
- atlas_allocation: Option<Allocation>,
- /// CPU mirror of the atlas (RGBA8, ATLAS_SIZE²).
- atlas_cpu: Vec<u8>,
- atlas_initialized: bool,
- generation: u64,
-
- glyphs: HashMap<CacheKey, GlyphEntry>,
- shelf: Shelf,
-
- pending_vertices: Vec<GlyphVertex>,
- frames: Vec<TextFrame>,
-}
-
-impl TextStage {
- 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 text 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 text pipeline layout");
-
- 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 glyph 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::<GlyphVertex>() 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 glyph pipeline")[0];
-
- let atlas_image = device
- .create_image(
- &vk::ImageCreateInfo::default()
- .image_type(vk::ImageType::TYPE_2D)
- .format(vk::Format::R8G8B8A8_UNORM)
- .extent(vk::Extent3D { width: ATLAS_SIZE, height: ATLAS_SIZE, 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 atlas image");
- let requirements = device.get_image_memory_requirements(atlas_image);
- let atlas_allocation = allocator
- .allocate(&AllocationCreateDesc {
- name: "glyph-atlas",
- requirements,
- location: MemoryLocation::GpuOnly,
- linear: false,
- allocation_scheme: AllocationScheme::GpuAllocatorManaged,
- })
- .expect("Failed to allocate atlas memory");
- device
- .bind_image_memory(atlas_image, atlas_allocation.memory(), atlas_allocation.offset())
- .expect("Failed to bind atlas memory");
- let atlas_view = device
- .create_image_view(
- &vk::ImageViewCreateInfo::default()
- .image(atlas_image)
- .view_type(vk::ImageViewType::TYPE_2D)
- .format(vk::Format::R8G8B8A8_UNORM)
- .subresource_range(
- vk::ImageSubresourceRange::default()
- .aspect_mask(vk::ImageAspectFlags::COLOR)
- .level_count(1)
- .layer_count(1),
- ),
- None,
- )
- .expect("Failed to create atlas view");
-
- // Glyphs are sampled 1:1; NEAREST keeps them crisp.
- let sampler = device
- .create_sampler(
- &vk::SamplerCreateInfo::default()
- .mag_filter(vk::Filter::NEAREST)
- .min_filter(vk::Filter::NEAREST)
- .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 atlas sampler");
-
- let pool_sizes = [
- vk::DescriptorPoolSize::default()
- .ty(vk::DescriptorType::SAMPLED_IMAGE)
- .descriptor_count(1),
- vk::DescriptorPoolSize::default()
- .ty(vk::DescriptorType::SAMPLER)
- .descriptor_count(1),
- ];
- let descriptor_pool = device
- .create_descriptor_pool(
- &vk::DescriptorPoolCreateInfo::default()
- .max_sets(1)
- .pool_sizes(&pool_sizes),
- None,
- )
- .expect("Failed to create text descriptor pool");
- let descriptor_set = device
- .allocate_descriptor_sets(
- &vk::DescriptorSetAllocateInfo::default()
- .descriptor_pool(descriptor_pool)
- .set_layouts(&set_layouts),
- )
- .expect("Failed to allocate text descriptor set")[0];
- let image_infos = [vk::DescriptorImageInfo::default()
- .image_view(atlas_view)
- .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)];
- let sampler_infos = [vk::DescriptorImageInfo::default().sampler(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),
- ],
- &[],
- );
-
- let atlas_bytes = (ATLAS_SIZE * ATLAS_SIZE * 4) as vk::DeviceSize;
- let frames = (0..frames_in_flight)
- .map(|_| TextFrame {
- vertex: create_cpu_buffer(
- device,
- allocator,
- 64 * 1024,
- vk::BufferUsageFlags::VERTEX_BUFFER,
- "glyph-vertices",
- ),
- vertex_count: 0,
- staging: create_cpu_buffer(
- device,
- allocator,
- atlas_bytes,
- vk::BufferUsageFlags::TRANSFER_SRC,
- "atlas-staging",
- ),
- uploaded_generation: 0,
- })
- .collect();
-
- TextStage {
- pipeline,
- pipeline_layout,
- descriptor_set_layout,
- descriptor_pool,
- descriptor_set,
- shader_module,
- sampler,
- atlas_image,
- atlas_view,
- atlas_allocation: Some(atlas_allocation),
- atlas_cpu: vec![0u8; (ATLAS_SIZE * ATLAS_SIZE * 4) as usize],
- atlas_initialized: false,
- generation: 1,
- glyphs: HashMap::new(),
- shelf: Shelf::new(),
- pending_vertices: Vec::new(),
- frames,
- }
- }
- }
-
- /// Rasterize (on miss) and cache one glyph. Returns None when the atlas is full.
- fn ensure_glyph(
- &mut self,
- font_system: &mut FontSystem,
- swash_cache: &mut SwashCache,
- key: CacheKey,
- ) -> Option<GlyphEntry> {
- if let Some(entry) = self.glyphs.get(&key) {
- return Some(*entry);
- }
- let image = swash_cache.get_image_uncached(font_system, key)?;
- let w = image.placement.width;
- let h = image.placement.height;
- if w == 0 || h == 0 || image.data.is_empty() {
- let entry = GlyphEntry {
- u: 0, v: 0, w: 0, h: 0, left: 0, top: 0, is_color: false, empty: true,
- };
- self.glyphs.insert(key, entry);
- return Some(entry);
- }
- let (u, v) = self.shelf.insert(w, h)?;
-
- let is_color = !matches!(image.content, SwashContent::Mask);
- for row in 0..h {
- for col in 0..w {
- let dst = (((v + row) * ATLAS_SIZE + (u + col)) * 4) as usize;
- let texel = match image.content {
- SwashContent::Mask => {
- let a = image.data[(row * w + col) as usize];
- [255, 255, 255, a]
- }
- // Color and SubpixelMask rasters are RGBA.
- _ => {
- let src = ((row * w + col) * 4) as usize;
- [
- image.data[src],
- image.data[src + 1],
- image.data[src + 2],
- image.data[src + 3],
- ]
- }
- };
- self.atlas_cpu[dst..dst + 4].copy_from_slice(&texel);
- }
- }
- self.generation += 1;
-
- let entry = GlyphEntry {
- u,
- v,
- w,
- h,
- left: image.placement.left,
- top: image.placement.top,
- is_color,
- empty: false,
- };
- self.glyphs.insert(key, entry);
- Some(entry)
- }
-
- /// Build this frame's glyph vertices. Positions/bounds in physical pixels,
- /// NDC computed against `extent` (wgpu convention; the shader flips for Vulkan).
- pub(crate) fn prepare(
- &mut self,
- font_system: &mut FontSystem,
- swash_cache: &mut SwashCache,
- spans: &[TextSpan<'_>],
- extent: vk::Extent2D,
- ) {
- self.pending_vertices.clear();
- if !self.try_prepare(font_system, swash_cache, spans, extent) {
- // Atlas full: clear and repack with only the glyphs this frame needs.
- log::info!("glyph atlas full — clearing and repacking");
- self.glyphs.clear();
- self.shelf = Shelf::new();
- self.atlas_cpu.fill(0);
- self.generation += 1;
- self.pending_vertices.clear();
- if !self.try_prepare(font_system, swash_cache, spans, extent) {
- log::error!("glyph atlas full even after repack; text truncated this frame");
- }
- }
- }
-
- fn try_prepare(
- &mut self,
- font_system: &mut FontSystem,
- swash_cache: &mut SwashCache,
- spans: &[TextSpan<'_>],
- extent: vk::Extent2D,
- ) -> bool {
- let sw = extent.width as f32;
- let sh = extent.height as f32;
- for span in spans {
- for run in span.buffer.layout_runs() {
- let line_y = (run.line_y * span.scale).round() as i32;
- for glyph in run.glyphs.iter() {
- let physical = glyph.physical((span.left, span.top), span.scale);
- let Some(entry) =
- self.ensure_glyph(font_system, swash_cache, physical.cache_key)
- else {
- // Distinguish "atlas full" (retryable) from "unrasterizable"
- // (skip): a missing swash image caches as empty above, so a
- // None here means the shelf rejected it.
- if swash_cache
- .get_image_uncached(font_system, physical.cache_key)
- .is_some()
- {
- return false;
- }
- continue;
- };
- if entry.empty {
- continue;
- }
-
- // glyphon's placement formula, physical pixels.
- let mut x0 = (physical.x + entry.left) as f32;
- let mut y0 = (line_y + physical.y - entry.top) as f32;
- let mut x1 = x0 + entry.w as f32;
- let mut y1 = y0 + entry.h as f32;
- let mut u0 = entry.u as f32;
- let mut v0 = entry.v as f32;
- let mut u1 = u0 + entry.w as f32;
- let mut v1 = v0 + entry.h as f32;
-
- // CPU clip to span bounds, shrinking UVs proportionally.
- if let Some([bl, bt, br, bb]) = span.bounds {
- let (bl, bt, br, bb) = (bl as f32, bt as f32, br as f32, bb as f32);
- if x0 >= br || x1 <= bl || y0 >= bb || y1 <= bt {
- continue;
- }
- if x0 < bl {
- u0 += bl - x0;
- x0 = bl;
- }
- if x1 > br {
- u1 -= x1 - br;
- x1 = br;
- }
- if y0 < bt {
- v0 += bt - y0;
- y0 = bt;
- }
- if y1 > bb {
- v1 -= y1 - bb;
- y1 = bb;
- }
- }
-
- let color = if entry.is_color {
- [1.0, 1.0, 1.0, 1.0]
- } else if let Some(c) = glyph.color_opt {
- [
- c.r() as f32 / 255.0,
- c.g() as f32 / 255.0,
- c.b() as f32 / 255.0,
- c.a() as f32 / 255.0,
- ]
- } else {
- span.default_color
- };
-
- // Corner positions, optionally rotated about the span's center
- // (physical px) before the NDC mapping.
- let corners = match span.rotation {
- None => [[x0, y0], [x1, y0], [x0, y1], [x1, y1]],
- Some((angle, cx, cy)) => {
- let (sin_a, cos_a) = angle.sin_cos();
- let rot = |px: f32, py: f32| {
- let (dx, dy) = (px - cx, py - cy);
- [cx + dx * cos_a - dy * sin_a, cy + dx * sin_a + dy * cos_a]
- };
- [rot(x0, y0), rot(x1, y0), rot(x0, y1), rot(x1, y1)]
- }
- };
- let ndc = |p: [f32; 2]| {
- [(p[0] / sw) * 2.0 - 1.0, 1.0 - (p[1] / sh) * 2.0]
- };
- let uv = |u: f32, v: f32| [u / ATLAS_SIZE as f32, v / ATLAS_SIZE as f32];
- let clip_circle = span.clip_circle;
- let tl = GlyphVertex { position: ndc(corners[0]), uv: uv(u0, v0), color, clip_circle };
- let tr = GlyphVertex { position: ndc(corners[1]), uv: uv(u1, v0), color, clip_circle };
- let bl = GlyphVertex { position: ndc(corners[2]), uv: uv(u0, v1), color, clip_circle };
- let br = GlyphVertex { position: ndc(corners[3]), uv: uv(u1, v1), color, clip_circle };
- self.pending_vertices.extend([tl, tr, bl, tr, br, bl]);
- }
- }
- }
- true
- }
-
- /// Called after this frame's fence has been waited: move pending vertices into
- /// the frame's buffer and refresh its staging copy if the atlas changed.
- pub(crate) fn write_frame_buffers(
- &mut self,
- device: &ash::Device,
- allocator: &mut Allocator,
- frame_index: usize,
- ) {
- let vertices = std::mem::take(&mut self.pending_vertices);
- let frame = &mut self.frames[frame_index];
-
- let bytes: &[u8] = bytemuck::cast_slice(&vertices);
- let needed = bytes.len() as vk::DeviceSize;
- if needed > frame.vertex.size {
- let mut old = std::mem::replace(&mut frame.vertex, AllocatedBuffer::null());
- destroy_cpu_buffer(device, allocator, &mut old);
- frame.vertex = create_cpu_buffer(
- device,
- allocator,
- needed.next_power_of_two(),
- vk::BufferUsageFlags::VERTEX_BUFFER,
- "glyph-vertices",
- );
- }
- if !bytes.is_empty() {
- frame.vertex.allocation.as_mut().unwrap().mapped_slice_mut().unwrap()
- [..bytes.len()]
- .copy_from_slice(bytes);
- }
- frame.vertex_count = vertices.len() as u32;
- self.pending_vertices = vertices;
- self.pending_vertices.clear();
-
- let frame = &mut self.frames[frame_index];
- if frame.uploaded_generation != self.generation {
- frame.staging.allocation.as_mut().unwrap().mapped_slice_mut().unwrap()
- [..self.atlas_cpu.len()]
- .copy_from_slice(&self.atlas_cpu);
- }
- }
-
- /// Record the atlas upload (if this frame's staging is newer than the image).
- /// Must be called outside a render pass.
- pub(crate) fn record_upload(&mut self, device: &ash::Device, cmd: vk::CommandBuffer, frame_index: usize) {
- let frame = &mut self.frames[frame_index];
- if frame.uploaded_generation == self.generation {
- return;
- }
- frame.uploaded_generation = self.generation;
-
- let range = vk::ImageSubresourceRange::default()
- .aspect_mask(vk::ImageAspectFlags::COLOR)
- .level_count(1)
- .layer_count(1);
- let (old_layout, src_stage, src_access) = if self.atlas_initialized {
- (
- vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
- vk::PipelineStageFlags::FRAGMENT_SHADER,
- vk::AccessFlags::SHADER_READ,
- )
- } else {
- (
- vk::ImageLayout::UNDEFINED,
- vk::PipelineStageFlags::TOP_OF_PIPE,
- vk::AccessFlags::empty(),
- )
- };
- self.atlas_initialized = true;
-
- unsafe {
- device.cmd_pipeline_barrier(
- cmd,
- src_stage,
- vk::PipelineStageFlags::TRANSFER,
- vk::DependencyFlags::empty(),
- &[],
- &[],
- &[vk::ImageMemoryBarrier::default()
- .src_access_mask(src_access)
- .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
- .old_layout(old_layout)
- .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
- .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
- .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
- .image(self.atlas_image)
- .subresource_range(range)],
- );
- device.cmd_copy_buffer_to_image(
- cmd,
- frame.staging.buffer,
- self.atlas_image,
- vk::ImageLayout::TRANSFER_DST_OPTIMAL,
- &[vk::BufferImageCopy::default()
- .buffer_offset(0)
- .buffer_row_length(ATLAS_SIZE)
- .buffer_image_height(ATLAS_SIZE)
- .image_subresource(
- vk::ImageSubresourceLayers::default()
- .aspect_mask(vk::ImageAspectFlags::COLOR)
- .layer_count(1),
- )
- .image_extent(vk::Extent3D {
- width: ATLAS_SIZE,
- height: ATLAS_SIZE,
- 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(self.atlas_image)
- .subresource_range(range)],
- );
- }
- }
-
- /// Record the glyph draw. Must be called inside the render pass, after the
- /// 2D quads (text goes on top). Viewport/scissor are inherited (dynamic,
- /// already set by the caller).
- pub(crate) fn record_draw(&self, device: &ash::Device, cmd: vk::CommandBuffer, frame_index: usize) {
- let frame = &self.frames[frame_index];
- if frame.vertex_count == 0 || !self.atlas_initialized {
- return;
- }
- unsafe {
- device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline);
- device.cmd_bind_descriptor_sets(
- cmd,
- vk::PipelineBindPoint::GRAPHICS,
- self.pipeline_layout,
- 0,
- &[self.descriptor_set],
- &[],
- );
- device.cmd_bind_vertex_buffers(cmd, 0, &[frame.vertex.buffer], &[0]);
- device.cmd_draw(cmd, frame.vertex_count, 1, 0, 0);
- }
- }
-
- pub(crate) fn destroy(&mut self, device: &ash::Device, allocator: &mut Allocator) {
- unsafe {
- for frame in &mut self.frames {
- let mut vertex = std::mem::replace(&mut frame.vertex, AllocatedBuffer::null());
- destroy_cpu_buffer(device, allocator, &mut vertex);
- let mut staging = std::mem::replace(&mut frame.staging, AllocatedBuffer::null());
- destroy_cpu_buffer(device, allocator, &mut staging);
- }
- device.destroy_sampler(self.sampler, None);
- device.destroy_image_view(self.atlas_view, None);
- device.destroy_image(self.atlas_image, None);
- if let Some(allocation) = self.atlas_allocation.take() {
- let _ = allocator.free(allocation);
- }
- 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_smoke.rs b/src/vk_smoke.rs
index a4b11cd..5e7c94d 100644
--- a/src/vk_smoke.rs
+++ b/src/vk_smoke.rs
@@ -6,8 +6,7 @@
//! Run inside a Wayland session:
//! cargo run -p cce-designer --bin vk-smoke
-#[path = "vk/mod.rs"]
-mod vk;
+use cce_ui::vk;
use smithay_client_toolkit::{
compositor::{CompositorHandler, CompositorState},