From d02aee79a24abf01a56f7c87658baf70cc6e8564 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 17:49:18 +0000 Subject: [PATCH 1/2] API improvements for crate release - Change `wait_for` to return `Result` (#285) Distinguishes timeout (Ok(false)) from device lost (Err) across all three backends (Vulkan, Metal, GLES). - Add `memory_stats()` API with VK_EXT_memory_budget support (#61) Reports device-local VRAM budget and usage on Vulkan, uses MTLDevice properties on Metal, returns defaults on GLES. - Set VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT on depth+stencil textures (#283) Allows creating texture views with different compatible formats (e.g. Depth32Float view of Depth32FloatStencil8Uint texture). - Handle surface acquire errors gracefully (#290) Replace panic on unexpected vkAcquireNextImageKHR errors with log + empty frame, matching the existing OUT_OF_DATE handling. - Add debug bounds check to BufferPiece::data() (#260) debug_assert that offset doesn't exceed buffer size. - Add Buffer::size() accessor to all backends GLES already had the field; added to Vulkan and Metal. - Implement Display + Error for NotSupportedError and DeviceError https://claude.ai/code/session_01W9jSvpnbXEXmoHFuUVFdUN --- blade-egui/src/lib.rs | 2 +- blade-graphics/src/gles/mod.rs | 16 +++++++-- blade-graphics/src/lib.rs | 49 +++++++++++++++++++++++++++ blade-graphics/src/metal/mod.rs | 24 ++++++++++--- blade-graphics/src/traits.rs | 2 +- blade-graphics/src/vulkan/init.rs | 46 +++++++++++++++++++++++++ blade-graphics/src/vulkan/mod.rs | 23 +++++++++++-- blade-graphics/src/vulkan/resource.rs | 8 +++++ blade-graphics/src/vulkan/surface.rs | 12 ++++++- blade-render/src/raster/mod.rs | 2 +- blade-render/src/render/mod.rs | 2 +- blade-render/src/util/frame_pacer.rs | 2 +- blade-util/src/belt.rs | 2 +- examples/bunnymark/example.rs | 2 +- examples/bunnymark/main.rs | 4 +-- examples/matmul/main.rs | 2 +- examples/particle/main.rs | 8 ++--- examples/ray-query/example.rs | 2 +- examples/ray-query/main.rs | 4 +-- tests/gpu_examples.rs | 4 +-- tests/snapshot.rs | 2 +- 21 files changed, 187 insertions(+), 31 deletions(-) diff --git a/blade-egui/src/lib.rs b/blade-egui/src/lib.rs index 18f0ec60..27be14ee 100644 --- a/blade-egui/src/lib.rs +++ b/blade-egui/src/lib.rs @@ -221,7 +221,7 @@ impl GuiPainter { let valid_pos = self .textures_to_delete .iter() - .position(|&(_, ref sp)| !context.wait_for(sp, 0)) + .position(|&(_, ref sp)| !context.wait_for(sp, 0).unwrap_or(true)) .unwrap_or_default(); for (texture, _) in self.textures_to_delete.drain(..valid_pos) { context.destroy_texture_view(texture.view); diff --git a/blade-graphics/src/gles/mod.rs b/blade-graphics/src/gles/mod.rs index 82a77878..3a619077 100644 --- a/blade-graphics/src/gles/mod.rs +++ b/blade-graphics/src/gles/mod.rs @@ -61,6 +61,10 @@ impl Buffer { pub fn data(&self) -> *mut u8 { self.data } + + pub fn size(&self) -> u64 { + self.size + } } #[derive(Clone, Copy, Debug, Hash, PartialEq)] @@ -457,6 +461,10 @@ impl Context { pub fn device_information(&self) -> &crate::DeviceInformation { &self.device_information } + + pub fn memory_stats(&self) -> crate::MemoryStats { + crate::MemoryStats::default() + } } #[hidden_trait::expose] @@ -559,7 +567,7 @@ impl crate::traits::CommandDevice for Context { SyncPoint { fence } } - fn wait_for(&self, sp: &SyncPoint, timeout_ms: u32) -> bool { + fn wait_for(&self, sp: &SyncPoint, timeout_ms: u32) -> Result { use glow::HasContext as _; let gl = self.lock(); @@ -574,8 +582,10 @@ impl crate::traits::CommandDevice for Context { let status = unsafe { gl.client_wait_sync(sp.fence, glow::SYNC_FLUSH_COMMANDS_BIT, timeout_ns_i32) }; match status { - glow::ALREADY_SIGNALED | glow::CONDITION_SATISFIED => true, - _ => false, + glow::ALREADY_SIGNALED | glow::CONDITION_SATISFIED => Ok(true), + glow::TIMEOUT_EXPIRED => Ok(false), + glow::WAIT_FAILED => Err(crate::DeviceError::DeviceLost), + _ => Ok(false), } } } diff --git a/blade-graphics/src/lib.rs b/blade-graphics/src/lib.rs index 192e1217..07a1db99 100644 --- a/blade-graphics/src/lib.rs +++ b/blade-graphics/src/lib.rs @@ -146,12 +146,55 @@ pub enum NotSupportedError { PlatformNotSupported, } +impl fmt::Display for NotSupportedError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Platform(e) => write!(f, "platform error: {:?}", e), + Self::NoSupportedDeviceFound => f.write_str("no supported device found"), + Self::PlatformNotSupported => f.write_str("platform not supported"), + } + } +} + +impl std::error::Error for NotSupportedError {} + impl From for NotSupportedError { fn from(error: PlatformError) -> Self { Self::Platform(error) } } +/// Error indicating a GPU device failure. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum DeviceError { + /// The GPU device has been lost and can no longer be used. + DeviceLost, + /// The GPU ran out of memory. + OutOfMemory, +} + +impl fmt::Display for DeviceError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::DeviceLost => f.write_str("device lost"), + Self::OutOfMemory => f.write_str("out of memory"), + } + } +} + +impl std::error::Error for DeviceError {} + +/// GPU memory usage statistics. +#[derive(Clone, Copy, Debug, Default)] +pub struct MemoryStats { + /// Total memory budget across all device-local heaps (bytes). + /// Zero if the backend doesn't support memory budget queries. + pub budget: u64, + /// Current memory usage across all device-local heaps (bytes). + /// Zero if the backend doesn't support memory budget queries. + pub usage: u64, +} + #[derive(Clone, Debug, Default, PartialEq)] pub struct Capabilities { /// Support binding arrays of handles. @@ -258,6 +301,12 @@ impl BufferPiece { pub fn data(&self) -> *mut u8 { let base = self.buffer.data(); assert!(!base.is_null()); + debug_assert!( + self.offset <= self.buffer.size(), + "BufferPiece offset {} exceeds buffer size {}", + self.offset, + self.buffer.size(), + ); unsafe { base.offset(self.offset as isize) } } } diff --git a/blade-graphics/src/metal/mod.rs b/blade-graphics/src/metal/mod.rs index 6f9e0c5f..57943ae6 100644 --- a/blade-graphics/src/metal/mod.rs +++ b/blade-graphics/src/metal/mod.rs @@ -90,6 +90,11 @@ impl Buffer { use metal::MTLBuffer as _; self.as_ref().contents().as_ptr() as *mut u8 } + + pub fn size(&self) -> u64 { + use metal::MTLResource as _; + self.as_ref().allocatedSize() as u64 + } } #[derive(Clone, Copy, Debug, Hash, PartialEq)] @@ -549,6 +554,15 @@ impl Context { pub fn metal_device(&self) -> Retained> { self.device.lock().unwrap().clone() } + + pub fn memory_stats(&self) -> crate::MemoryStats { + use metal::MTLDevice as _; + let device = self.device.lock().unwrap(); + crate::MemoryStats { + budget: device.recommendedMaxWorkingSetSize() as u64, + usage: device.currentAllocatedSize() as u64, + } + } } #[hidden_trait::expose] @@ -609,15 +623,17 @@ impl crate::traits::CommandDevice for Context { SyncPoint { cmd_buf } } - fn wait_for(&self, sp: &SyncPoint, timeout_ms: u32) -> bool { + fn wait_for(&self, sp: &SyncPoint, timeout_ms: u32) -> Result { use metal::MTLCommandBuffer as _; let start = time::Instant::now(); loop { - if let metal::MTLCommandBufferStatus::Completed = sp.cmd_buf.status() { - return true; + match sp.cmd_buf.status() { + metal::MTLCommandBufferStatus::Completed => return Ok(true), + metal::MTLCommandBufferStatus::Error => return Err(crate::DeviceError::DeviceLost), + _ => {} } if start.elapsed().as_millis() >= timeout_ms as u128 { - return false; + return Ok(false); } thread::sleep(time::Duration::from_millis(1)); } diff --git a/blade-graphics/src/traits.rs b/blade-graphics/src/traits.rs index cc5f23c4..4d936335 100644 --- a/blade-graphics/src/traits.rs +++ b/blade-graphics/src/traits.rs @@ -44,7 +44,7 @@ pub trait CommandDevice { fn create_command_encoder(&self, desc: super::CommandEncoderDesc) -> Self::CommandEncoder; fn destroy_command_encoder(&self, encoder: &mut Self::CommandEncoder); fn submit(&self, encoder: &mut Self::CommandEncoder) -> Self::SyncPoint; - fn wait_for(&self, sp: &Self::SyncPoint, timeout_ms: u32) -> bool; + fn wait_for(&self, sp: &Self::SyncPoint, timeout_ms: u32) -> Result; } pub trait CommandEncoder { diff --git a/blade-graphics/src/vulkan/init.rs b/blade-graphics/src/vulkan/init.rs index 726606cf..1bab4a4f 100644 --- a/blade-graphics/src/vulkan/init.rs +++ b/blade-graphics/src/vulkan/init.rs @@ -72,6 +72,7 @@ struct AdapterCapabilities { timing: bool, dual_source_blending: bool, cooperative_matrix: bool, + memory_budget: bool, bugs: SystemBugs, } @@ -402,6 +403,7 @@ unsafe fn inspect_adapter( let buffer_marker = supported_extensions.contains(&vk::AMD_BUFFER_MARKER_NAME); let shader_info = supported_extensions.contains(&vk::AMD_SHADER_INFO_NAME); let full_screen_exclusive = supported_extensions.contains(&vk::EXT_FULL_SCREEN_EXCLUSIVE_NAME); + let memory_budget = supported_extensions.contains(&vk::EXT_MEMORY_BUDGET_NAME); let device_information = crate::DeviceInformation { is_software_emulated: properties.device_type == vk::PhysicalDeviceType::CPU, @@ -433,6 +435,7 @@ unsafe fn inspect_adapter( timing, dual_source_blending, cooperative_matrix, + memory_budget, bugs, }) } @@ -741,6 +744,9 @@ impl super::Context { device_extensions.push(vk::KHR_VULKAN_MEMORY_MODEL_NAME); } } + if capabilities.memory_budget { + device_extensions.push(vk::EXT_MEMORY_BUDGET_NAME); + } let str_pointers = device_extensions .iter() @@ -1119,6 +1125,7 @@ impl super::Context { dual_source_blending: capabilities.dual_source_blending, cooperative_matrix: capabilities.cooperative_matrix, binding_array: capabilities.binding_array, + memory_budget: capabilities.memory_budget, instance, entry, xr, @@ -1153,6 +1160,45 @@ impl super::Context { pub fn device_information(&self) -> &crate::DeviceInformation { &self.device.device_information } + + pub fn memory_stats(&self) -> crate::MemoryStats { + if !self.memory_budget { + return crate::MemoryStats::default(); + } + + let mut budget_properties = vk::PhysicalDeviceMemoryBudgetPropertiesEXT::default(); + let mut mem_properties2 = + vk::PhysicalDeviceMemoryProperties2::default().push_next(&mut budget_properties); + + unsafe { + self.instance + .get_physical_device_properties2 + .get_physical_device_memory_properties2(self.physical_device, &mut mem_properties2); + } + + // Copy what we need before accessing budget_properties + let heap_count = mem_properties2.memory_properties.memory_heap_count as usize; + let heap_flags: Vec<_> = mem_properties2.memory_properties.memory_heaps[..heap_count] + .iter() + .map(|h| h.flags) + .collect(); + // Now mem_properties2 borrow is released, we can access budget_properties + drop(mem_properties2); + + let mut total_budget = 0u64; + let mut total_usage = 0u64; + for (i, flags) in heap_flags.iter().enumerate() { + if flags.contains(vk::MemoryHeapFlags::DEVICE_LOCAL) { + total_budget += budget_properties.heap_budget[i]; + total_usage += budget_properties.heap_usage[i]; + } + } + + crate::MemoryStats { + budget: total_budget, + usage: total_usage, + } + } } impl Drop for super::Context { diff --git a/blade-graphics/src/vulkan/mod.rs b/blade-graphics/src/vulkan/mod.rs index ba6754a4..c6ca9cec 100644 --- a/blade-graphics/src/vulkan/mod.rs +++ b/blade-graphics/src/vulkan/mod.rs @@ -273,6 +273,7 @@ pub struct Context { dual_source_blending: bool, cooperative_matrix: bool, binding_array: bool, + memory_budget: bool, instance: Instance, entry: ash::Entry, xr: Option>, @@ -283,6 +284,7 @@ pub struct Buffer { raw: vk::Buffer, memory_handle: usize, mapped_data: *mut u8, + size: u64, external: Option, } @@ -292,6 +294,7 @@ impl Default for Buffer { raw: vk::Buffer::null(), memory_handle: !0, mapped_data: ptr::null_mut(), + size: 0, external: None, } } @@ -301,6 +304,10 @@ impl Buffer { pub fn data(&self) -> *mut u8 { self.mapped_data } + + pub fn size(&self) -> u64 { + self.size + } } unsafe impl Send for Buffer {} @@ -587,6 +594,7 @@ impl crate::traits::CommandDevice for Context { raw: scratch.raw, memory_handle: scratch.memory_handle, mapped_data: scratch.mapped, + size: 0, external: None, }); } @@ -748,7 +756,7 @@ impl crate::traits::CommandDevice for Context { SyncPoint { progress } } - fn wait_for(&self, sp: &SyncPoint, timeout_ms: u32) -> bool { + fn wait_for(&self, sp: &SyncPoint, timeout_ms: u32) -> Result { //Note: technically we could get away without locking the queue, // but also this isn't time-sensitive, so it's fine. let timeline_semaphore = self.queue.lock().unwrap().timeline_semaphore; @@ -758,11 +766,20 @@ impl crate::traits::CommandDevice for Context { .semaphores(&semaphores) .values(&semaphore_values); let timeout_ns = map_timeout(timeout_ms); - unsafe { + match unsafe { self.device .timeline_semaphore .wait_semaphores(&wait_info, timeout_ns) - .is_ok() + } { + Ok(()) => Ok(true), + Err(vk::Result::TIMEOUT) => Ok(false), + Err(vk::Result::ERROR_DEVICE_LOST) => Err(crate::DeviceError::DeviceLost), + Err(vk::Result::ERROR_OUT_OF_DEVICE_MEMORY) + | Err(vk::Result::ERROR_OUT_OF_HOST_MEMORY) => Err(crate::DeviceError::OutOfMemory), + Err(other) => { + log::error!("Unexpected wait_semaphores error: {:?}", other); + Err(crate::DeviceError::DeviceLost) + } } } } diff --git a/blade-graphics/src/vulkan/resource.rs b/blade-graphics/src/vulkan/resource.rs index bfc9e26a..b8d4efbe 100644 --- a/blade-graphics/src/vulkan/resource.rs +++ b/blade-graphics/src/vulkan/resource.rs @@ -346,6 +346,7 @@ impl crate::traits::ResourceDevice for super::Context { raw, memory_handle: allocation.handle, mapped_data: allocation.data, + size: desc.size, external: fetch_external_source(&self.device, allocation), } } @@ -371,6 +372,13 @@ impl crate::traits::ResourceDevice for super::Context { { create_flags |= vk::ImageCreateFlags::CUBE_COMPATIBLE; } + // Enable mutable format for multi-aspect textures (e.g. depth+stencil) + // so that views can select individual aspects with compatible formats. + if desc.format.aspects().contains(crate::TexelAspects::DEPTH) + && desc.format.aspects().contains(crate::TexelAspects::STENCIL) + { + create_flags |= vk::ImageCreateFlags::MUTABLE_FORMAT; + } let mut external_next = desc.external.map(|e| vk::ExternalMemoryImageCreateInfo { handle_types: external_source_handle_type(e), diff --git a/blade-graphics/src/vulkan/surface.rs b/blade-graphics/src/vulkan/surface.rs index 9d247701..049674b4 100644 --- a/blade-graphics/src/vulkan/surface.rs +++ b/blade-graphics/src/vulkan/surface.rs @@ -61,7 +61,17 @@ impl super::Surface { xr_views: [super::XrView::default(); super::MAX_XR_EYES], } } - Err(other) => panic!("Aquire image error {}", other), + Err(other) => { + log::error!("Acquire image error: {}", other); + super::Frame { + internal: self.frames[0], + swapchain: self.swapchain, + image_index: None, + xr_swapchain: 0, + xr_view_count: 0, + xr_views: [super::XrView::default(); super::MAX_XR_EYES], + } + } } } } diff --git a/blade-render/src/raster/mod.rs b/blade-render/src/raster/mod.rs index 65f9d06a..1431ca6e 100644 --- a/blade-render/src/raster/mod.rs +++ b/blade-render/src/raster/mod.rs @@ -247,7 +247,7 @@ impl Rasterizer { } log::info!("Hot reloading raster shaders"); - gpu.wait_for(sync_point, !0); + let _ = gpu.wait_for(sync_point, !0); for task in tasks { let _ = task.join(); } diff --git a/blade-render/src/render/mod.rs b/blade-render/src/render/mod.rs index a4306834..18661d03 100644 --- a/blade-render/src/render/mod.rs +++ b/blade-render/src/render/mod.rs @@ -782,7 +782,7 @@ impl RayTracer { } log::info!("Hot reloading shaders"); - gpu.wait_for(sync_point, !0); + let _ = gpu.wait_for(sync_point, !0); for task in tasks { let _ = task.join(); } diff --git a/blade-render/src/util/frame_pacer.rs b/blade-render/src/util/frame_pacer.rs index 7619a480..a5b75ac4 100644 --- a/blade-render/src/util/frame_pacer.rs +++ b/blade-render/src/util/frame_pacer.rs @@ -30,7 +30,7 @@ impl FramePacer { #[profiling::function] pub fn wait_for_previous_frame(&mut self, context: &blade_graphics::Context) { if let Some(sp) = self.prev_sync_point.take() { - context.wait_for(&sp, !0); + let _ = context.wait_for(&sp, !0); } for buffer in self.prev_resources.buffers.drain(..) { context.destroy_buffer(buffer); diff --git a/blade-util/src/belt.rs b/blade-util/src/belt.rs index 3711e88f..a42ef13f 100644 --- a/blade-util/src/belt.rs +++ b/blade-util/src/belt.rs @@ -58,7 +58,7 @@ impl BufferBelt { let index_maybe = self .buffers .iter() - .position(|(rb, sp)| size <= rb.size && gpu.wait_for(sp, 0)); + .position(|(rb, sp)| size <= rb.size && gpu.wait_for(sp, 0).unwrap_or(false)); if let Some(index) = index_maybe { let (rb, _) = self.buffers.remove(index); let piece = rb.raw.into(); diff --git a/examples/bunnymark/example.rs b/examples/bunnymark/example.rs index f0cb4ab2..ce8fb471 100644 --- a/examples/bunnymark/example.rs +++ b/examples/bunnymark/example.rs @@ -182,7 +182,7 @@ impl Example { transfer.copy_buffer_to_texture(upload_buffer.into(), 4, texture.into(), extent); } let sync_point = context.submit(&mut command_encoder); - context.wait_for(&sync_point, !0); + let _ = context.wait_for(&sync_point, !0); context.destroy_command_encoder(&mut command_encoder); context.destroy_buffer(upload_buffer); diff --git a/examples/bunnymark/main.rs b/examples/bunnymark/main.rs index cddea8f6..bbaddde0 100644 --- a/examples/bunnymark/main.rs +++ b/examples/bunnymark/main.rs @@ -175,7 +175,7 @@ impl winit::application::ApplicationHandler for App { command_encoder.present(frame); let sync_point = context.submit(command_encoder); if let Some(sp) = self.prev_sync_point.take() { - context.wait_for(&sp, !0); + let _ = context.wait_for(&sp, !0); } self.prev_sync_point = Some(sync_point); } @@ -204,7 +204,7 @@ fn main() { let context = app.context.as_ref().unwrap(); if let Some(sp) = app.prev_sync_point.take() { - context.wait_for(&sp, !0); + let _ = context.wait_for(&sp, !0); } if let Some(mut example) = app.example.take() { example.deinit(context); diff --git a/examples/matmul/main.rs b/examples/matmul/main.rs index f146ed9a..c4f15a4e 100644 --- a/examples/matmul/main.rs +++ b/examples/matmul/main.rs @@ -128,7 +128,7 @@ fn main() { pe.dispatch([M / TILE, N / TILE, 1]); } let sp = context.submit(&mut encoder); - context.wait_for(&sp, !0); + let _ = context.wait_for(&sp, !0); // Read back results let result = diff --git a/examples/particle/main.rs b/examples/particle/main.rs index eeb51dff..1d5d2123 100644 --- a/examples/particle/main.rs +++ b/examples/particle/main.rs @@ -28,7 +28,7 @@ impl Example { let surface_info = self.surface.info(); if let Some(sp) = self.prev_sync_point.take() { - self.context.wait_for(&sp, !0); + let _ = self.context.wait_for(&sp, !0); } if let Some(msaa_view) = self.msaa_view.take() { @@ -192,7 +192,7 @@ impl Example { fn destroy(&mut self) { if let Some(sp) = self.prev_sync_point.take() { - self.context.wait_for(&sp, !0); + let _ = self.context.wait_for(&sp, !0); } self.context .destroy_command_encoder(&mut self.command_encoder); @@ -322,7 +322,7 @@ impl Example { self.gui_painter.after_submit(&sync_point); if let Some(sp) = self.prev_sync_point.take() { - self.context.wait_for(&sp, !0); + let _ = self.context.wait_for(&sp, !0); } self.prev_sync_point = Some(sync_point); } @@ -354,7 +354,7 @@ impl Example { .changed() { if let Some(sp) = self.prev_sync_point.take() { - self.context.wait_for(&sp, !0); + let _ = self.context.wait_for(&sp, !0); } let old_effect = self.particle_system.effect.clone(); diff --git a/examples/ray-query/example.rs b/examples/ray-query/example.rs index 329ae3f8..493a9b77 100644 --- a/examples/ray-query/example.rs +++ b/examples/ray-query/example.rs @@ -201,7 +201,7 @@ impl Example { } let sync_point = context.submit(&mut command_encoder); - context.wait_for(&sync_point, !0); + let _ = context.wait_for(&sync_point, !0); context.destroy_command_encoder(&mut command_encoder); context.destroy_buffer(vertex_buf); context.destroy_buffer(index_buf); diff --git a/examples/ray-query/main.rs b/examples/ray-query/main.rs index da987e19..faff40b3 100644 --- a/examples/ray-query/main.rs +++ b/examples/ray-query/main.rs @@ -109,7 +109,7 @@ impl winit::application::ApplicationHandler for App { let sync_point = context.submit(command_encoder); if let Some(sp) = self.prev_sync_point.take() { - context.wait_for(&sp, !0); + let _ = context.wait_for(&sp, !0); } self.prev_sync_point = Some(sync_point); } @@ -138,7 +138,7 @@ fn main() { let context = app.context.as_ref().unwrap(); if let Some(sp) = app.prev_sync_point.take() { - context.wait_for(&sp, !0); + let _ = context.wait_for(&sp, !0); } if let Some(example) = app.example.take() { example.deinit(context); diff --git a/tests/gpu_examples.rs b/tests/gpu_examples.rs index 83155ee6..450b52c8 100644 --- a/tests/gpu_examples.rs +++ b/tests/gpu_examples.rs @@ -257,7 +257,7 @@ fn dispatch_gpu_test() { } let sync_point = context.submit(&mut command_encoder); - assert!(context.wait_for(&sync_point, 2000)); + assert!(context.wait_for(&sync_point, 2000).unwrap()); let actual = unsafe { slice::from_raw_parts(output.data() as *const u32, 4) }; let expected = [3, 5, 7, 9]; @@ -320,7 +320,7 @@ fn env_map_gpu_test() { } let sync_point = context.submit(&mut command_encoder); - assert!(context.wait_for(&sync_point, 2000)); + assert!(context.wait_for(&sync_point, 2000).unwrap()); let actual = unsafe { slice::from_raw_parts(readback.data(), 8) }; assert!( diff --git a/tests/snapshot.rs b/tests/snapshot.rs index 44ba1a5e..c2afad0c 100644 --- a/tests/snapshot.rs +++ b/tests/snapshot.rs @@ -66,7 +66,7 @@ impl OffscreenTarget { } let sync_point = context.submit(encoder); assert!( - context.wait_for(&sync_point, 5000), + context.wait_for(&sync_point, 5000).unwrap(), "GPU timed out during snapshot readback" ); let byte_count = (self.size.width * self.size.height * 4) as usize; From b7e77fa3348e4f1dada1507d418baba86056ad7a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 17:57:20 +0000 Subject: [PATCH 2/2] Unify PlatformError, add ComputePipelineBase trait, update changelog - Unify PlatformError into a single opaque struct across all backends, replacing per-backend enum/type-alias definitions. This makes error handling consistent regardless of compilation target. - Add ComputePipelineBase trait with get_workgroup_size(), exposed via hidden_trait on all backends so generic code can query workgroup size. - Add changelog entries for all 0.8 API changes. https://claude.ai/code/session_01W9jSvpnbXEXmoHFuUVFdUN --- blade-graphics/src/gles/egl.rs | 24 ++++++++++-------------- blade-graphics/src/gles/mod.rs | 7 +++---- blade-graphics/src/gles/web.rs | 2 -- blade-graphics/src/lib.rs | 23 ++++++++++++++++++++++- blade-graphics/src/metal/mod.rs | 7 +++---- blade-graphics/src/traits.rs | 6 +++++- blade-graphics/src/vulkan/init.rs | 20 ++++++++++---------- blade-graphics/src/vulkan/mod.rs | 11 +++-------- blade-graphics/src/vulkan/surface.rs | 2 +- docs/CHANGELOG.md | 12 +++++++++++- 10 files changed, 68 insertions(+), 46 deletions(-) diff --git a/blade-graphics/src/gles/egl.rs b/blade-graphics/src/gles/egl.rs index ae558c14..9fbca770 100644 --- a/blade-graphics/src/gles/egl.rs +++ b/blade-graphics/src/gles/egl.rs @@ -97,12 +97,6 @@ const GBM_BO_USE_LINEAR: u32 = 1 << 4; type GlEglImageTargetTexture2dOesFun = unsafe extern "system" fn(target: u32, image: *mut ffi::c_void); -#[derive(Debug)] -pub enum PlatformError { - Loading(egl::LoadError), - Init(egl::Error), -} - #[derive(Clone, Copy, Debug)] enum SrgbFrameBufferKind { /// No support for SRGB surface @@ -279,7 +273,7 @@ impl super::Context { } else { egl::DynamicInstance::::load_required() }; - egl_result.map_err(PlatformError::Loading)? + egl_result.map_err(crate::PlatformError::loading)? }; let client_extensions = match egl.query_string(None, egl::EXTENSIONS) { @@ -828,7 +822,7 @@ impl super::Context { &[egl::ATTRIB_NONE], ) } - .map_err(PlatformError::Init)?, + .map_err(crate::PlatformError::init)?, Rdh::Xlib(handle) => unsafe { let display_ptr = match handle.display { Some(d) => d.as_ptr(), @@ -836,7 +830,7 @@ impl super::Context { }; egl1_5.get_platform_display(EGL_PLATFORM_X11_KHR, display_ptr, &[egl::ATTRIB_NONE]) } - .map_err(PlatformError::Init)?, + .map_err(crate::PlatformError::init)?, _ => { return Err(crate::NotSupportedError::NoSupportedDeviceFound); } @@ -845,7 +839,7 @@ impl super::Context { // Load a separate EGL instance for the presentation context so it // has its own library handle (EglContext takes ownership). let pres_egl_instance = unsafe { egl::DynamicInstance::::load_required() } - .map_err(PlatformError::Loading)?; + .map_err(crate::PlatformError::loading)?; let desc = crate::ContextDesc { presentation: true, @@ -914,7 +908,7 @@ impl super::Context { "Failed to create window surface on presentation display: {:?}", e ); - PlatformError::Init(e) + crate::PlatformError::init(e) })? }; @@ -1367,7 +1361,9 @@ impl EglContext { egl: EglInstance, display: egl::Display, ) -> Result { - let version = egl.initialize(display).map_err(PlatformError::Init)?; + let version = egl + .initialize(display) + .map_err(crate::PlatformError::init)?; let vendor = egl.query_string(Some(display), egl::VENDOR).unwrap(); let display_extensions = egl .query_string(Some(display), egl::EXTENSIONS) @@ -1438,7 +1434,7 @@ impl EglContext { Ok(context) => context, Err(e) => { log::warn!("unable to create GLES 3.x context: {:?}", e); - return Err(PlatformError::Init(e).into()); + return Err(crate::PlatformError::init(e).into()); } }; @@ -1454,7 +1450,7 @@ impl EglContext { .map(Some) .map_err(|e| { log::warn!("Error in create_pbuffer_surface: {:?}", e); - PlatformError::Init(e) + crate::PlatformError::init(e) })? }; diff --git a/blade-graphics/src/gles/mod.rs b/blade-graphics/src/gles/mod.rs index 3a619077..7e5928f2 100644 --- a/blade-graphics/src/gles/mod.rs +++ b/blade-graphics/src/gles/mod.rs @@ -12,8 +12,6 @@ const DEBUG_ID: u32 = 0; const MAX_TIMEOUT: u64 = 1_000_000_000; // MAX_CLIENT_WAIT_TIMEOUT_WEBGL; const MAX_QUERIES: usize = crate::limits::PASS_COUNT + 1; -pub use platform::PlatformError; - bitflags::bitflags! { struct Capabilities: u32 { const BUFFER_STORAGE = 1 << 0; @@ -136,8 +134,9 @@ pub struct ComputePipeline { wg_size: [u32; 3], } -impl ComputePipeline { - pub fn get_workgroup_size(&self) -> [u32; 3] { +#[hidden_trait::expose] +impl crate::traits::ComputePipelineBase for ComputePipeline { + fn get_workgroup_size(&self) -> [u32; 3] { self.wg_size } } diff --git a/blade-graphics/src/gles/web.rs b/blade-graphics/src/gles/web.rs index 7cb5e9cc..178eca8c 100644 --- a/blade-graphics/src/gles/web.rs +++ b/blade-graphics/src/gles/web.rs @@ -17,8 +17,6 @@ pub struct PlatformFrame { extent: crate::Extent, } -pub type PlatformError = (); - impl super::Surface { pub fn info(&self) -> crate::SurfaceInfo { self.platform.info diff --git a/blade-graphics/src/lib.rs b/blade-graphics/src/lib.rs index 07a1db99..534e3f98 100644 --- a/blade-graphics/src/lib.rs +++ b/blade-graphics/src/lib.rs @@ -90,6 +90,27 @@ pub const CANVAS_ID: &str = "blade"; use std::{fmt, num::NonZeroU32}; +/// Error from the underlying graphics platform during initialization. +#[derive(Debug)] +pub struct PlatformError(String); + +impl PlatformError { + pub(crate) fn loading(err: impl fmt::Debug) -> Self { + Self(format!("failed to load: {:?}", err)) + } + pub(crate) fn init(err: impl fmt::Debug) -> Self { + Self(format!("failed to initialize: {:?}", err)) + } +} + +impl fmt::Display for PlatformError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl std::error::Error for PlatformError {} + #[cfg(not(any( vulkan, windows, @@ -149,7 +170,7 @@ pub enum NotSupportedError { impl fmt::Display for NotSupportedError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Platform(e) => write!(f, "platform error: {:?}", e), + Self::Platform(e) => write!(f, "platform error: {}", e), Self::NoSupportedDeviceFound => f.write_str("no supported device found"), Self::PlatformNotSupported => f.write_str("platform not supported"), } diff --git a/blade-graphics/src/metal/mod.rs b/blade-graphics/src/metal/mod.rs index 57943ae6..dde980af 100644 --- a/blade-graphics/src/metal/mod.rs +++ b/blade-graphics/src/metal/mod.rs @@ -14,8 +14,6 @@ mod surface; const MAX_TIMESTAMPS: usize = crate::limits::PASS_COUNT * 2; -pub type PlatformError = (); - pub struct Surface { view: Option>, render_layer: Retained, @@ -253,8 +251,9 @@ pub struct ComputePipeline { unsafe impl Send for ComputePipeline {} unsafe impl Sync for ComputePipeline {} -impl ComputePipeline { - pub fn get_workgroup_size(&self) -> [u32; 3] { +#[hidden_trait::expose] +impl crate::traits::ComputePipelineBase for ComputePipeline { + fn get_workgroup_size(&self) -> [u32; 3] { [ self.wg_size.width as u32, self.wg_size.height as u32, diff --git a/blade-graphics/src/traits.rs b/blade-graphics/src/traits.rs index 4d936335..8c721de7 100644 --- a/blade-graphics/src/traits.rs +++ b/blade-graphics/src/traits.rs @@ -27,8 +27,12 @@ pub trait ResourceDevice { fn destroy_acceleration_structure(&self, acceleration_structure: Self::AccelerationStructure); } +pub trait ComputePipelineBase { + fn get_workgroup_size(&self) -> [u32; 3]; +} + pub trait ShaderDevice { - type ComputePipeline: Send + Sync; + type ComputePipeline: Send + Sync + ComputePipelineBase; type RenderPipeline: Send + Sync; fn create_compute_pipeline(&self, desc: super::ComputePipelineDesc) -> Self::ComputePipeline; diff --git a/blade-graphics/src/vulkan/init.rs b/blade-graphics/src/vulkan/init.rs index 1bab4a4f..9ddd70ad 100644 --- a/blade-graphics/src/vulkan/init.rs +++ b/blade-graphics/src/vulkan/init.rs @@ -446,7 +446,7 @@ impl super::Context { Ok(entry) => entry, Err(err) => { log::error!("Missing Vulkan entry points: {:?}", err); - return Err(super::PlatformError::Loading(err).into()); + return Err(crate::PlatformError::loading(err).into()); } }; let driver_api_version = match entry.try_enumerate_instance_version() { @@ -455,7 +455,7 @@ impl super::Context { Ok(None) => return Err(NotSupportedError::NoSupportedDeviceFound), Err(err) => { log::error!("try_enumerate_instance_version: {:?}", err); - return Err(super::PlatformError::Init(err).into()); + return Err(crate::PlatformError::init(err).into()); } }; @@ -480,7 +480,7 @@ impl super::Context { Ok(layers) => layers, Err(err) => { log::error!("enumerate_instance_layer_properties: {:?}", err); - return Err(super::PlatformError::Init(err).into()); + return Err(crate::PlatformError::init(err).into()); } }; let supported_layer_names = supported_layers @@ -509,7 +509,7 @@ impl super::Context { Ok(extensions) => extensions, Err(err) => { log::error!("enumerate_instance_extension_properties: {:?}", err); - return Err(super::PlatformError::Init(err).into()); + return Err(crate::PlatformError::init(err).into()); } }; let supported_instance_extensions = supported_instance_extension_properties @@ -591,7 +591,7 @@ impl super::Context { &create_info as *const _ as *const _, ) .map_err(|_| NotSupportedError::NoSupportedDeviceFound)? - .map_err(|raw| super::PlatformError::Init(vk::Result::from_raw(raw)))? + .map_err(|raw| crate::PlatformError::init(vk::Result::from_raw(raw)))? }; unsafe { ash::Instance::load( @@ -601,7 +601,7 @@ impl super::Context { } } else { unsafe { entry.create_instance(&create_info, None) } - .map_err(super::PlatformError::Init)? + .map_err(|e| crate::PlatformError::init(e))? } }; @@ -663,7 +663,7 @@ impl super::Context { instance .core .enumerate_physical_devices() - .map_err(super::PlatformError::Init)? + .map_err(|e| crate::PlatformError::init(e))? .into_iter() .find_map(|phd| { inspect_adapter( @@ -849,7 +849,7 @@ impl super::Context { &device_create_info as *const _ as *const _, ) .map_err(|_| NotSupportedError::NoSupportedDeviceFound)? - .map_err(|raw| super::PlatformError::Init(vk::Result::from_raw(raw)))? + .map_err(|raw| crate::PlatformError::init(vk::Result::from_raw(raw)))? }; unsafe { ash::Device::load( @@ -861,7 +861,7 @@ impl super::Context { instance .core .create_device(physical_device, &device_create_info, None) - .map_err(super::PlatformError::Init)? + .map_err(|e| crate::PlatformError::init(e))? } }; @@ -1183,7 +1183,7 @@ impl super::Context { .map(|h| h.flags) .collect(); // Now mem_properties2 borrow is released, we can access budget_properties - drop(mem_properties2); + let _ = mem_properties2; let mut total_budget = 0u64; let mut total_usage = 0u64; diff --git a/blade-graphics/src/vulkan/mod.rs b/blade-graphics/src/vulkan/mod.rs index c6ca9cec..7ac24bba 100644 --- a/blade-graphics/src/vulkan/mod.rs +++ b/blade-graphics/src/vulkan/mod.rs @@ -15,12 +15,6 @@ mod surface; const QUERY_POOL_SIZE: usize = crate::limits::PASS_COUNT + 1; const MAX_XR_EYES: usize = 2; -#[derive(Debug)] -pub enum PlatformError { - Loading(ash::LoadingError), - Init(vk::Result), -} - struct Instance { core: ash::Instance, _debug_utils: ash::ext::debug_utils::Instance, @@ -396,8 +390,9 @@ pub struct ComputePipeline { wg_size: [u32; 3], } -impl ComputePipeline { - pub fn get_workgroup_size(&self) -> [u32; 3] { +#[hidden_trait::expose] +impl crate::traits::ComputePipelineBase for ComputePipeline { + fn get_workgroup_size(&self) -> [u32; 3] { self.wg_size } } diff --git a/blade-graphics/src/vulkan/surface.rs b/blade-graphics/src/vulkan/surface.rs index 049674b4..32cc05c5 100644 --- a/blade-graphics/src/vulkan/surface.rs +++ b/blade-graphics/src/vulkan/surface.rs @@ -198,7 +198,7 @@ impl super::Context { window.window_handle().unwrap().as_raw(), None, ) - .map_err(super::PlatformError::Init)? + .map_err(|e| crate::PlatformError::init(e))? }; let khr_surface = self diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 25ce5fcb..00817a8d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -15,9 +15,19 @@ Changelog for *Blade* project - option to disable ray tracing initialization - separate `Capabilities` flag for binding arrays, including TLAS arrays - cooperative matrix operations support (auto-detected via `Capabilities`) + - `wait_for` now returns `Result` instead of `bool`, + distinguishing timeout from device-lost errors + - `memory_stats()` API for querying VRAM budget/usage (via `VK_EXT_memory_budget`) + - `Buffer::size()` accessor on all backends + - `PlatformError` is now a unified opaque type across all backends + - `ComputePipelineBase` trait exposes `get_workgroup_size()` for generic code + - `NotSupportedError`, `DeviceError`, and `PlatformError` implement `Display` + `Error` + - vk: set `MUTABLE_FORMAT` on depth+stencil textures for flexible view creation + - vk: graceful handling of surface acquire errors instead of panicking + - vk: reject GPUs that cannot present in Intel+NVIDIA PRIME configurations - egl: use DMA-BUF sharing with different displays for presentation - vk: uniform buffer fallback for buggy Qualcomm devices - - vk: reject GPUs that cannot present in Intel+NVIDIA PRIME configurations + - debug bounds check on `BufferPiece::data()` ## blade-graphics-0.7.1 (22 Feb 2025)