diff --git a/sugarloaf/src/context/vulkan.rs b/sugarloaf/src/context/vulkan.rs index 6d94eeea..84765467 100644 --- a/sugarloaf/src/context/vulkan.rs +++ b/sugarloaf/src/context/vulkan.rs @@ -1353,11 +1353,17 @@ fn create_device( let device_extensions = [khr::swapchain::NAME.as_ptr()]; - // Enable dynamic rendering up front — it's Vulkan 1.3 core. We're - // not using it yet in the clear-only path, but enabling it here - // avoids having to recreate the device when pipelines land. - let mut vk13_features = - vk::PhysicalDeviceVulkan13Features::default().dynamic_rendering(true); + // Enable dynamic_rendering and synchronization2 — both are Vulkan + // 1.3 core. `synchronization2` is required for `vkCmdPipelineBarrier2`, + // which the swapchain layout transitions + // (UNDEFINED → COLOR_ATTACHMENT_OPTIMAL and COLOR_ATTACHMENT_OPTIMAL + // → PRESENT_SRC_KHR) and atlas image transitions go through. Without + // the feature enabled, the v2 barrier calls are silently ignored, + // the swapchain image stays in UNDEFINED layout, and the compositor + // discards the present — visible as "first frame and stops." + let mut vk13_features = vk::PhysicalDeviceVulkan13Features::default() + .dynamic_rendering(true) + .synchronization2(true); let queue_infos = [queue_info]; let create_info = vk::DeviceCreateInfo::default() diff --git a/sugarloaf/src/grid/vulkan.rs b/sugarloaf/src/grid/vulkan.rs index 319106f8..c09f564d 100644 --- a/sugarloaf/src/grid/vulkan.rs +++ b/sugarloaf/src/grid/vulkan.rs @@ -109,13 +109,45 @@ impl VulkanGlyphAtlas { format, vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED, ); + + // One-shot UNDEFINED → SHADER_READ_ONLY_OPTIMAL transition so the + // atlas matches the layout declared in its descriptor write + // (`SHADER_READ_ONLY_OPTIMAL`) from the moment it's bound. + // Without this the validation layer flags + // `vkQueueSubmit() expects ... SHADER_READ_ONLY_OPTIMAL — current + // layout is UNDEFINED` even when the text pipeline does no actual + // sampling (the descriptor binding alone is enough for validation + // to assume a read may happen). On real drivers it's UB; in + // practice it shows up as an empty / black first frame after + // a70b774 because the GPU may discard the draw entirely. + let img_handle = image.handle(); + ctx.submit_oneshot(|cmd| unsafe { + let to_read = vk::ImageMemoryBarrier2::default() + .src_stage_mask(vk::PipelineStageFlags2::TOP_OF_PIPE) + .src_access_mask(vk::AccessFlags2::empty()) + .dst_stage_mask(vk::PipelineStageFlags2::FRAGMENT_SHADER) + .dst_access_mask(vk::AccessFlags2::SHADER_READ) + .old_layout(vk::ImageLayout::UNDEFINED) + .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(img_handle) + .subresource_range(color_subresource_range()); + let barriers = [to_read]; + let dep = vk::DependencyInfo::default().image_memory_barriers(&barriers); + ctx.shared().raw.cmd_pipeline_barrier2(cmd, &dep); + }); + Self { image, allocator: AtlasAllocator::new(ATLAS_SIZE, ATLAS_SIZE), slots: FxHashMap::default(), bytes_per_pixel, pending: Vec::new(), - initialized: false, + // Atlas is already in SHADER_READ_ONLY_OPTIMAL — `flush_uploads` + // can use its initialized branch (SHADER_READ → TRANSFER_DST → + // SHADER_READ) from the very first upload. + initialized: true, staging: std::array::from_fn(|_| None), staging_capacity: [0; FRAMES_IN_FLIGHT], }