From 2b0c28a8a26e0680934067c9d3c3b6c8c809ca4f Mon Sep 17 00:00:00 2001 From: John Wells Date: Wed, 24 Jun 2026 06:06:54 -0400 Subject: [PATCH 1/7] Support custom images --- crates/vk-graph-imgui/src/lib.rs | 204 ++++++++++++++++++++++--------- 1 file changed, 145 insertions(+), 59 deletions(-) diff --git a/crates/vk-graph-imgui/src/lib.rs b/crates/vk-graph-imgui/src/lib.rs index 7bbda44e..e46e73ec 100644 --- a/crates/vk-graph-imgui/src/lib.rs +++ b/crates/vk-graph-imgui/src/lib.rs @@ -5,22 +5,22 @@ /// Common imports for applications using the ImGui integration. pub mod prelude { - pub use super::{Condition, ImGui, Ui, imgui}; + pub use super::{Condition, Frame, ImGui, Image, ImageSource, Ui, imgui}; } pub use imgui::{self, Condition, Ui}; -type DrawCmdInfo = (usize, [f32; 4], usize, usize); +type DrawCmdInfo = (usize, [f32; 4], usize, usize, TextureId); use { bytemuck::cast_slice, - imgui::{Context, DrawCmd, DrawCmdParams}, + imgui::{Context, DrawCmd, DrawCmdParams, TextureId}, imgui_winit_support::{ winit::{event::Event, window::Window}, {HiDpiMode, WinitPlatform}, }, log::warn, - std::{sync::Arc, time::Duration}, + std::{collections::HashMap, marker::PhantomData, sync::Arc, time::Duration}, vk_graph::{ Graph, cmd::{LoadOp, StoreOp}, @@ -29,11 +29,11 @@ use { buffer::{Buffer, BufferInfo}, device::Device, graphics::{BlendInfo, GraphicsPipeline, GraphicsPipelineInfo}, - image::{Image, ImageInfo}, + image::{Image as DriverImage, ImageInfo}, shader::Shader, sync::AccessType, }, - node::ImageLeaseNode, + node::{AnyImageNode, ImageLeaseNode, ImageNode}, pool::{Lease, Pool}, }, vk_shader_macros::include_glsl, @@ -43,9 +43,33 @@ use { #[derive(Debug)] pub struct ImGui { context: Context, - font_atlas_image: Option>>, + font_atlas_image: Option>>, + next_texture_id: usize, pipeline: GraphicsPipeline, platform: WinitPlatform, + user_images: HashMap, +} + +/// Frame-scoped helper for registering user images with ImGui draw commands. +pub struct Frame<'a> { + next_texture_id: &'a mut usize, + user_images: &'a mut HashMap, +} + +/// A frame-scoped ImGui image registration. +/// +/// Dropping this value releases the typed handle. The underlying texture binding remains valid +/// until the current ImGui frame is rendered, because ImGui consumes draw commands after UI code +/// returns. +pub struct Image<'a> { + id: TextureId, + _frame: PhantomData<&'a Frame<'a>>, +} + +/// Image source accepted by [`Frame::image`]. +#[derive(Clone, Copy, Debug)] +pub struct ImageSource { + image: AnyImageNode, } fn supported_draw_cmd(draw_cmd: DrawCmd) -> Option { @@ -57,9 +81,10 @@ fn supported_draw_cmd(draw_cmd: DrawCmd) -> Option { clip_rect, idx_offset, vtx_offset, + texture_id, .. }, - } => Some((count, clip_rect, idx_offset, vtx_offset)), + } => Some((count, clip_rect, idx_offset, vtx_offset, texture_id)), DrawCmd::ResetRenderState => { warn!("unsupported imgui draw command: reset render state"); None @@ -71,6 +96,53 @@ fn supported_draw_cmd(draw_cmd: DrawCmd) -> Option { } } +impl<'a> Frame<'a> { + /// Registers an image for use by ImGui widgets during this frame. + pub fn image(&mut self, image: impl Into) -> Image<'_> { + let id = TextureId::new(*self.next_texture_id); + *self.next_texture_id += 1; + self.user_images.insert(id, image.into().image); + + Image { + id, + _frame: PhantomData, + } + } +} + +impl Image<'_> { + /// Returns the ImGui texture id for this image. + pub const fn id(&self) -> TextureId { + self.id + } +} + +impl Drop for Image<'_> { + fn drop(&mut self) {} +} + +impl From for ImageSource { + fn from(image: AnyImageNode) -> Self { + Self { image } + } +} + +impl From for ImageSource { + fn from(image: ImageLeaseNode) -> Self { + Self { + image: image.into(), + } + } +} + +impl From for ImageSource { + fn from(image: ImageNode) -> Self { + Self { + image: image.into(), + } + } +} + impl ImGui { /// Creates a new ImGui renderer for the given device. pub fn new(device: &Device) -> Self { @@ -91,8 +163,10 @@ impl ImGui { Self { context, font_atlas_image: None, + next_texture_id: 1, pipeline, platform, + user_images: Default::default(), } } @@ -108,10 +182,10 @@ impl ImGui { window: &Window, pool: &mut P, graph: &mut Graph, - ui_func: impl FnOnce(&mut Ui, &mut P, &mut Graph), + ui_func: impl FnOnce(&mut Frame<'_>, &mut Ui, &mut P, &mut Graph), ) -> ImageLeaseNode where - P: Pool + Pool, + P: Pool + Pool, { let hidpi = self.platform.hidpi_factor(); @@ -133,10 +207,14 @@ impl ImGui { .prepare_frame(io, window) .expect("invalid imgui frame"); - // Let the caller draw the GUI + // Let the caller draw the GUI and register graph images used by image widgets. let ui = self.context.frame(); + let mut frame = Frame { + next_texture_id: &mut self.next_texture_id, + user_images: &mut self.user_images, + }; - ui_func(ui, pool, graph); + ui_func(&mut frame, ui, pool, graph); self.platform.prepare_render(ui, window); let draw_data = self.context.render(); @@ -216,59 +294,67 @@ impl ImGui { let window_height = self.platform.hidpi_factor() as f32 / window.inner_size().height as f32; - graph - .begin_cmd() - .debug_name("imgui") - .bind_pipeline(&self.pipeline) - .resource_access(index_buf, AccessType::IndexBuffer) - .resource_access(vertex_buf, AccessType::VertexBuffer) - .shader_resource_access( - 0, - font_atlas_image, - AccessType::FragmentShaderReadSampledImageOrUniformTexelBuffer, - ) - .color_attachment_image(0, image, LoadOp::Load, StoreOp::Store) - .record_cmd(move |cmd| { - cmd.push_constants(0, &window_width.to_ne_bytes()) - .push_constants(4, &window_height.to_ne_bytes()) - .bind_index_buffer(index_buf, 0, vk::IndexType::UINT16) - .bind_vertex_buffer(0, vertex_buf, 0); - - for (index_count, clip_rect, first_index, vertex_offset) in draw_cmds { - let clip_rect = [ - (clip_rect[0] - display_pos[0]) * framebuffer_scale[0], - (clip_rect[1] - display_pos[1]) * framebuffer_scale[1], - (clip_rect[2] - display_pos[0]) * framebuffer_scale[0], - (clip_rect[3] - display_pos[1]) * framebuffer_scale[1], - ]; - let x = clip_rect[0].floor() as i32; - let y = clip_rect[1].floor() as i32; - let width = (clip_rect[2] - clip_rect[0]).ceil() as u32; - let height = (clip_rect[3] - clip_rect[1]).ceil() as u32; - cmd.set_scissor( - 0, - &[vk::Rect2D { - offset: vk::Offset2D { x, y }, - extent: vk::Extent2D { width, height }, - }], - ) - .draw_indexed( - index_count as _, - 1, - first_index as _, - vertex_offset as _, - 0, - ); - } - }); + for (index_count, clip_rect, first_index, vertex_offset, texture_id) in draw_cmds { + let texture = self + .user_images + .get(&texture_id) + .copied() + .unwrap_or_else(|| font_atlas_image.into()); + let clip_rect = [ + (clip_rect[0] - display_pos[0]) * framebuffer_scale[0], + (clip_rect[1] - display_pos[1]) * framebuffer_scale[1], + (clip_rect[2] - display_pos[0]) * framebuffer_scale[0], + (clip_rect[3] - display_pos[1]) * framebuffer_scale[1], + ]; + let x = clip_rect[0].floor() as i32; + let y = clip_rect[1].floor() as i32; + let width = (clip_rect[2] - clip_rect[0]).ceil() as u32; + let height = (clip_rect[3] - clip_rect[1]).ceil() as u32; + + graph + .begin_cmd() + .debug_name("imgui") + .bind_pipeline(&self.pipeline) + .resource_access(index_buf, AccessType::IndexBuffer) + .resource_access(vertex_buf, AccessType::VertexBuffer) + .shader_resource_access( + 0, + texture, + AccessType::FragmentShaderReadSampledImageOrUniformTexelBuffer, + ) + .color_attachment_image(0, image, LoadOp::Load, StoreOp::Store) + .record_cmd(move |cmd| { + cmd.push_constants(0, &window_width.to_ne_bytes()) + .push_constants(4, &window_height.to_ne_bytes()) + .bind_index_buffer(index_buf, 0, vk::IndexType::UINT16) + .bind_vertex_buffer(0, vertex_buf, 0) + .set_scissor( + 0, + &[vk::Rect2D { + offset: vk::Offset2D { x, y }, + extent: vk::Extent2D { width, height }, + }], + ) + .draw_indexed( + index_count as _, + 1, + first_index as _, + vertex_offset as _, + 0, + ); + }); + } } + self.user_images.clear(); + self.next_texture_id = 1; + image } fn lease_font_atlas_image

(&mut self, pool: &mut P, graph: &mut Graph) where - P: Pool + Pool, + P: Pool + Pool, { use imgui::{FontConfig, FontGlyphRanges, FontSource}; @@ -355,7 +441,7 @@ mod test { assert_eq!( supported_draw_cmd(draw_cmd), - Some((42, [1.0, 2.0, 3.0, 4.0], 6, 5)) + Some((42, [1.0, 2.0, 3.0, 4.0], 6, 5, TextureId::new(7))) ); } From 02fa8fbf918085de083b9990adc9f0404fd80950 Mon Sep 17 00:00:00 2001 From: John Wells Date: Wed, 24 Jun 2026 06:07:40 -0400 Subject: [PATCH 2/7] Fix for multi-view --- src/driver/render_pass.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/driver/render_pass.rs b/src/driver/render_pass.rs index 4212b793..c51ebd9d 100644 --- a/src/driver/render_pass.rs +++ b/src/driver/render_pass.rs @@ -281,12 +281,20 @@ impl RenderPass { }; let key = entry.key(); - let layers = key - .attachments + let is_multiview = self + .info + .subpasses .iter() - .map(|attachment| attachment.layer_count) - .max() - .unwrap_or(1); + .any(|subpass| subpass.view_mask != 0); + let layers = if is_multiview { + 1 + } else { + key.attachments + .iter() + .map(|attachment| attachment.layer_count) + .max() + .unwrap_or(1) + }; let attachments = key .attachments .iter() From 4bbc3cacfcd4ddf0be16831f252a68035816ccdc Mon Sep 17 00:00:00 2001 From: John Wells Date: Wed, 24 Jun 2026 10:07:33 -0400 Subject: [PATCH 3/7] Allow squelching validation messages --- src/driver/instance.rs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/driver/instance.rs b/src/driver/instance.rs index 59add8bd..92d324f6 100644 --- a/src/driver/instance.rs +++ b/src/driver/instance.rs @@ -73,11 +73,23 @@ unsafe extern "system" fn debug_callback( if is_error { error!("{message}"); - } else if message_severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::WARNING) { + } else if message_severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::WARNING) + && !var("VK_GRAPH_DEBUG_IGNORE_WARNING") + .map(var_value_is_set) + .unwrap_or_default() + { warn!("{message}"); - } else if message_severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::INFO) { + } else if message_severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::INFO) + && !var("VK_GRAPH_DEBUG_IGNORE_INFO") + .map(var_value_is_set) + .unwrap_or_default() + { info!("{message}"); - } else if message_severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE) { + } else if message_severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE) + && !var("VK_GRAPH_DEBUG_IGNORE_VERBOSE") + .map(var_value_is_set) + .unwrap_or_default() + { debug!("{message}"); } @@ -102,8 +114,8 @@ unsafe extern "system" fn debug_callback( } if var(SKIP_VALIDATION_PARK_ENV) - .map(|value| !matches!(value.as_str(), "" | "0" | "false" | "False" | "FALSE")) - .unwrap_or(false) + .map(var_value_is_set) + .unwrap_or_default() { warn!("validation callback park skipped; execution will continue"); @@ -189,6 +201,10 @@ fn has_vk_khr_surface(entry: &ash::Entry, instance: vk::Instance) -> bool { }) } +fn var_value_is_set(val: String) -> bool { + !matches!(val.as_str(), "" | "0" | "false" | "False" | "FALSE") +} + /// Vulkan API version. /// /// See [`VkApplicationInfo::apiVersion`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkApplicationInfo.html). From 22757ddfc850a8b7f61c2edc0d6b7338b479b6ea Mon Sep 17 00:00:00 2001 From: John Wells Date: Wed, 24 Jun 2026 10:07:53 -0400 Subject: [PATCH 4/7] Rename status/wait functions to match Vulkan --- crates/vk-graph-window/src/graphchain.rs | 6 +++--- examples/cpu_readback.rs | 8 ++++---- examples/imgui.rs | 2 +- examples/min_max.rs | 2 +- examples/mip_compute.rs | 2 +- examples/multithread.rs | 2 +- examples/subgroup_ops.rs | 2 +- examples/transitions.rs | 2 +- examples/vr/src/main.rs | 2 +- src/driver/fence.rs | 18 +++++++++++++++--- src/driver/swapchain.rs | 2 +- src/submission.rs | 6 +++--- 12 files changed, 33 insertions(+), 21 deletions(-) diff --git a/crates/vk-graph-window/src/graphchain.rs b/crates/vk-graph-window/src/graphchain.rs index a69fab55..be5bf606 100644 --- a/crates/vk-graph-window/src/graphchain.rs +++ b/crates/vk-graph-window/src/graphchain.rs @@ -183,7 +183,7 @@ impl Graphchain { if self.recreate_pending { for frame in &mut self.frames { if frame.fence.is_queued() { - frame.fence.wait_signaled()?.reset()?; + frame.fence.wait()?.reset()?; } } @@ -206,7 +206,7 @@ impl Graphchain { let frame = &mut self.frames[self.frame_idx]; if frame.fence.is_queued() { - frame.fence.wait_signaled()?.reset()?; + frame.fence.wait()?.reset()?; } self.strategy.prepare_frame( @@ -560,7 +560,7 @@ impl Drop for Graphchain { let device = frame.cmd_buf.device.clone(); for frame in &mut self.frames { - if frame.fence.is_queued() && frame.fence.wait_signaled().is_err() { + if frame.fence.is_queued() && frame.fence.wait().is_err() { return; } } diff --git a/examples/cpu_readback.rs b/examples/cpu_readback.rs index e796eccd..676b4b85 100644 --- a/examples/cpu_readback.rs +++ b/examples/cpu_readback.rs @@ -49,20 +49,20 @@ fn main() -> Result<(), DriverError> { let dst_buf = graph.resource(dst_buf).clone(); /* - Resolve and wait. You can check Fence::is_signaled without blocking, or use + Resolve and wait. You can check Fence::status without blocking, or use device.queue_wait_idle(0) or device.device_wait_idle(), but those block on larger scopes. */ let mut fence = graph .finalize() .queue_submit(&mut HashPool::new(&device), 0, 0)?; - println!("Has executed? {}", fence.is_signaled()?); + println!("Has executed? {}", fence.status()?); let started = Instant::now(); - fence.wait_signaled()?; + fence.wait()?; assert!( - fence.is_signaled()?, + fence.status()?, "We checked above - so this will always be true" ); println!("Waited {}μs", (Instant::now() - started).as_micros()); diff --git a/examples/imgui.rs b/examples/imgui.rs index a6abd47a..1e549d76 100644 --- a/examples/imgui.rs +++ b/examples/imgui.rs @@ -62,7 +62,7 @@ fn main() -> Result<(), WindowError> { frame.window, &mut pool, frame.graph, - |ui, _, _| { + |_, ui, _, _| { ui.window("Hello world") .position([10.0, 10.0], Condition::FirstUseEver) .size([340.0, 250.0], Condition::FirstUseEver) diff --git a/examples/min_max.rs b/examples/min_max.rs index 33edb709..808b810a 100644 --- a/examples/min_max.rs +++ b/examples/min_max.rs @@ -69,7 +69,7 @@ fn main() -> Result<(), DriverError> { let mut fence = graph .finalize() .queue_submit(&mut HashPool::new(&device), 0, 0)?; - fence.wait_signaled()?; + fence.wait()?; // For each image we have reduced each 2x2 pixel group into the min/max values of each group let min_result_data: &[f32] = cast_slice(Buffer::mapped_slice(&min_result_buf)); diff --git a/examples/mip_compute.rs b/examples/mip_compute.rs index 5d253f85..7b5f72c2 100644 --- a/examples/mip_compute.rs +++ b/examples/mip_compute.rs @@ -163,7 +163,7 @@ fn main() -> Result<(), DriverError> { let mut fence = graph .finalize() .queue_submit(&mut HashPool::new(&device), 0, 0)?; - fence.wait_signaled()?; + fence.wait()?; let depth_pixel = f32::from_ne_bytes(Buffer::mapped_slice(&depth_pixel).try_into().unwrap()); diff --git a/examples/multithread.rs b/examples/multithread.rs index 487223fb..01a230ed 100644 --- a/examples/multithread.rs +++ b/examples/multithread.rs @@ -154,7 +154,7 @@ fn main() -> anyhow::Result<()> { } }; - if let Err(err) = fence.wait_signaled() { + if let Err(err) = fence.wait() { warn!("unable to wait for worker fence: {err}"); break; diff --git a/examples/subgroup_ops.rs b/examples/subgroup_ops.rs index 44ca39f3..a643831a 100644 --- a/examples/subgroup_ops.rs +++ b/examples/subgroup_ops.rs @@ -134,7 +134,7 @@ fn exclusive_sum( .queue_submit(&mut HashPool::new(device), 0, 0)?; let started = Instant::now(); - fence.wait_signaled()?; + fence.wait()?; println!( "Waited {}μs (len={})", diff --git a/examples/transitions.rs b/examples/transitions.rs index c41f9fd0..1141f61a 100644 --- a/examples/transitions.rs +++ b/examples/transitions.rs @@ -97,7 +97,7 @@ fn main() -> anyhow::Result<()> { frame.window, &mut pool, frame.graph, - |ui, _, _| { + |_, ui, _, _| { ui.window("Transitions example") .position([10.0, 10.0], Condition::FirstUseEver) .size([340.0, 250.0], Condition::FirstUseEver) diff --git a/examples/vr/src/main.rs b/examples/vr/src/main.rs index 6f1c9e47..9c72d455 100644 --- a/examples/vr/src/main.rs +++ b/examples/vr/src/main.rs @@ -797,7 +797,7 @@ fn load_texture( graph .finalize() .queue_submit(&mut LazyPool::new(device), queue_family_index as _, 0)?; - fence.wait_signaled()?; + fence.wait()?; Ok(texture) } diff --git a/src/driver/fence.rs b/src/driver/fence.rs index 15c7a9fd..516a032d 100644 --- a/src/driver/fence.rs +++ b/src/driver/fence.rs @@ -56,11 +56,17 @@ impl Fence { self.droppables.clear(); } + #[deprecated = "use status function"] + #[doc(hidden)] + pub fn is_signaled(&self) -> Result { + self.status() + } + /// Returns `true` if this fence is signaled. /// /// See [`vkGetFenceStatus`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkGetFenceStatus.html). #[profiling::function] - pub fn is_signaled(&self) -> Result { + pub fn status(&self) -> Result { let res = unsafe { self.device.get_fence_status(self.handle) }; match res { @@ -103,11 +109,17 @@ impl Fence { Ok(self) } + #[deprecated = "use status function"] + #[doc(hidden)] + pub fn wait_signaled(&mut self) -> Result<&mut Self, DriverError> { + self.wait() + } + /// Waits for this fence to signal, then drops any deferred payloads. /// /// See [`vkWaitForFences`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkWaitForFences.html). #[profiling::function] - pub fn wait_signaled(&mut self) -> Result<&mut Self, DriverError> { + pub fn wait(&mut self) -> Result<&mut Self, DriverError> { #[cfg(feature = "checked")] if !self.queued { return Ok(self); @@ -127,7 +139,7 @@ impl Drop for Fence { return; } - if self.queued && self.wait_signaled().is_err() { + if self.queued && self.wait().is_err() { return; } diff --git a/src/driver/swapchain.rs b/src/driver/swapchain.rs index dfd3bf9c..4cfdd47c 100644 --- a/src/driver/swapchain.rs +++ b/src/driver/swapchain.rs @@ -610,7 +610,7 @@ impl QueueSignals { if queue_signal.fence.is_queued() { queue_signal .fence - .wait_signaled() + .wait() .map_err(|_| SwapchainError::DeviceLost)? .reset() .map_err(|_| SwapchainError::DeviceLost)?; diff --git a/src/submission.rs b/src/submission.rs index 312f9003..920d059c 100644 --- a/src/submission.rs +++ b/src/submission.rs @@ -677,7 +677,7 @@ where #[cfg(feature = "checked")] { - release_fence.wait_signaled()?; + release_fence.wait()?; release_fence.reset()?; } @@ -1444,7 +1444,7 @@ where ) -> Result<(), DriverError> { #[cfg(feature = "checked")] if fence.queued { - fence.wait_signaled()?; + fence.wait()?; fence.reset()?; } @@ -7604,7 +7604,7 @@ mod tests { let mut recorded = recorded.finish()?; recorded.queue_submit(&mut fence, 0, QueueSubmitInfo::QUEUE_SUBMIT)?; - fence.wait_signaled()?; + fence.wait()?; Ok(()) } From 641677be3e267d54f2d46bc3aa9796175b7156d5 Mon Sep 17 00:00:00 2001 From: John Wells Date: Wed, 24 Jun 2026 10:25:44 -0400 Subject: [PATCH 5/7] Rename fence APIs to match Vulkan --- CHANGELOG.md | 10 +- Cargo.toml | 4 +- README.md | 2 +- guide/src/usage.md | 4 +- guide/src/usage_thread.md | 4 +- src/cmd/mod.rs | 13 ++- src/driver/cmd_buf.rs | 2 +- src/driver/fence.rs | 77 +++++++++---- src/lib.rs | 225 +++++++++++++++++++++++++++++++++++++- src/submission.rs | 65 +++++++++-- 10 files changed, 362 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e05e9e19..9b1eb72e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.14.3] - TBD + +### Deprecated + +- `Fence::is_signaled`; use `Fence::status` instead. +- `Fence::wait_signaled`; use `Fence::wait` instead. + ## [0.14.2] - 2026-06-18 ### Added @@ -712,7 +719,8 @@ _See [#25](https://github.com/attackgoat/screen-13/pull/25) for migration detail platforms and require no bare-metal graphics API knowledge - "Hello, world!" example using a bitmapped font -[Unreleased]: https://github.com/attackgoat/vk-graph/compare/v0.14.2...HEAD +[Unreleased]: https://github.com/attackgoat/vk-graph/compare/v0.14.3...HEAD +[0.14.3]: https://crates.io/crates/vk-graph/0.14.3 [0.14.2]: https://crates.io/crates/vk-graph/0.14.2 [0.1.0]: https://crates.io/crates/screen-13/0.1.0 [0.2.0]: https://crates.io/crates/screen-13/0.2.0 diff --git a/Cargo.toml b/Cargo.toml index 49f916db..e9428858 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ readme = "README.md" log = "0.4" profiling = "1.0" read-only = "0.1" -vk-graph = { path = "", version = "0.14.2" } +vk-graph = { path = "", version = "0.14.3" } vk-graph-egui = { path = "crates/vk-graph-egui", version = "0.1.1" } vk-graph-fx = { path = "crates/vk-graph-fx", version = "0.1.1" } vk-graph-hot = { path = "crates/vk-graph-hot", version = "0.1.1" } @@ -34,7 +34,7 @@ winit = "0.30" [package] name = "vk-graph" -version = "0.14.2" +version = "0.14.3" authors = ["John Wells "] edition.workspace = true license.workspace = true diff --git a/README.md b/README.md index 18eb7781..1d7786ac 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ real-world use, and supports modern Vulkan commands[^modern]. ```toml [dependencies] -vk-graph = "0.14.2" +vk-graph = "0.14" ``` [*Changelog*](https://github.com/attackgoat/vk-graph/blob/main/CHANGELOG.md) diff --git a/guide/src/usage.md b/guide/src/usage.md index 53fe2f92..d5fbd51e 100644 --- a/guide/src/usage.md +++ b/guide/src/usage.md @@ -273,11 +273,11 @@ graph, but they may do so manually: # graph: Graph, # device: &Device, # ) -> Result<(), DriverError> { -// NOTE: This will stall! Use Fence::is_signaled to check periodically instead. +// NOTE: This will stall! Use Fence::status to check periodically instead. let mut fence = graph .finalize() .queue_submit(&mut LazyPool::new(device), 0, 0)?; -fence.wait_signaled()?; +fence.wait()?; # Ok(()) } ``` diff --git a/guide/src/usage_thread.md b/guide/src/usage_thread.md index 5e7301d3..019c67f8 100644 --- a/guide/src/usage_thread.md +++ b/guide/src/usage_thread.md @@ -61,8 +61,8 @@ These patterns are safe: Host-mappable buffers require extra understanding to use properly. The contents of a buffer are undefined from the time of submission until the returned `Fence` is -signaled. Use `Fence::is_signaled` or `Fence::wait_signaled` before reading or writing host-mapped -memory touched by that submission. +signaled. Use `Fence::status` or `Fence::wait` before reading or writing host-mapped memory touched +by that submission. See: [_`examples/cpu_readback.rs`_](https://github.com/attackgoat/vk-graph/blob/main/examples/cpu_readback.rs) diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 8213cc6c..27211d42 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -41,8 +41,8 @@ use { super::{ AccelerationStructureLeaseNode, AccelerationStructureNode, AnyAccelerationStructureNode, AnyBufferNode, AnyImageNode, AnyResource, BufferLeaseNode, BufferNode, CommandData, - CommandFunction, Execution, Graph, ImageLeaseNode, ImageNode, Node, Resource, - SwapchainImageNode, + CommandExecution, CommandFunction, Execution, Graph, ImageLeaseNode, ImageNode, Node, + Resource, SwapchainImageNode, }, crate::{ NodeIndex, @@ -94,6 +94,7 @@ impl<'a> Command<'a> { #[cfg(debug_assertions)] name: None, stream_scope_id: None, + tracking: Default::default(), }); Self { @@ -103,6 +104,14 @@ impl<'a> Command<'a> { } } + /// Returns a handle that tracks whether this graph command has completed device execution. + /// + /// This may be called multiple times. Each returned handle independently observes the same + /// command execution. + pub fn track_execution(&mut self) -> CommandExecution { + self.cmd_mut().tracking.track() + } + fn cmd(&self) -> &CommandData { &self.graph.cmds[self.cmd_idx] } diff --git a/src/driver/cmd_buf.rs b/src/driver/cmd_buf.rs index 62df66c2..125fc867 100644 --- a/src/driver/cmd_buf.rs +++ b/src/driver/cmd_buf.rs @@ -147,7 +147,7 @@ impl CommandBuffer { /// 2. Record commands. /// 3. End recording with [`Self::end`]. /// 4. Submit this command buffer with `queue_submit`. - /// 5. Later, wait for completion with [`Fence::is_signaled`] or [`Fence::wait_signaled`]. + /// 5. Later, check or wait for completion with [`Fence::status`] or [`Fence::wait`]. /// 6. Before re-submitting this same command buffer, reset the fence with [`Fence::reset`], /// then begin recording again. /// diff --git a/src/driver/fence.rs b/src/driver/fence.rs index 516a032d..0428e51e 100644 --- a/src/driver/fence.rs +++ b/src/driver/fence.rs @@ -4,9 +4,18 @@ use { super::{DriverError, device::Device}, ash::vk, log::{error, trace}, - std::{fmt::Debug, thread::panicking}, + std::{cell::Cell, cell::RefCell, fmt::Debug, thread::panicking}, }; +pub(crate) trait FenceDroppable: Debug + Send { + fn fence_signaled(&mut self) {} +} + +#[derive(Debug)] +struct DeferredDrop(T); + +impl FenceDroppable for DeferredDrop where T: Debug + Send {} + /// Represents a Vulkan fence used to track queue submission completion. /// /// See [`VkFence`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkFence.html). @@ -25,8 +34,8 @@ pub struct Fence { #[readonly] pub handle: vk::Fence, - pub(crate) queued: bool, - droppables: Vec>, + pub(crate) queued: Cell, + droppables: RefCell>>, } impl Fence { @@ -37,26 +46,36 @@ impl Fence { Ok(Self { device: device.clone(), handle: Device::create_fence(device, signaled)?, - queued: signaled, - droppables: Vec::new(), + queued: Cell::new(signaled), + droppables: RefCell::new(Vec::new()), }) } /// Drops an item after this fence signals. - pub(crate) fn drop_when_signaled(&mut self, x: impl Debug + Send + 'static) { - self.droppables.push(Box::new(x)); + pub(crate) fn drop_when_signaled(&self, x: impl Debug + Send + 'static) { + self.droppables.borrow_mut().push(Box::new(DeferredDrop(x))); + } + + pub(crate) fn drop_fence_droppable(&self, x: impl FenceDroppable + 'static) { + self.droppables.borrow_mut().push(Box::new(x)); } #[profiling::function] - fn drop_signaled(&mut self) { - if !self.droppables.is_empty() { - trace!("dropping {} shared references", self.droppables.len()); + fn drop_signaled(&self) { + let mut droppables = self.droppables.borrow_mut(); + + if !droppables.is_empty() { + trace!("dropping {} shared references", droppables.len()); } - self.droppables.clear(); + for droppable in droppables.iter_mut() { + droppable.fence_signaled(); + } + + droppables.clear(); } - #[deprecated = "use status function"] + #[deprecated = "use status"] #[doc(hidden)] pub fn is_signaled(&self) -> Result { self.status() @@ -64,13 +83,21 @@ impl Fence { /// Returns `true` if this fence is signaled. /// + /// Signaled deferred payloads are released before this returns `Ok(true)`. + /// /// See [`vkGetFenceStatus`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkGetFenceStatus.html). #[profiling::function] pub fn status(&self) -> Result { let res = unsafe { self.device.get_fence_status(self.handle) }; match res { - Ok(status) => Ok(status), + Ok(status) => { + if status { + self.drop_signaled(); + } + + Ok(status) + } Err(err) if err == vk::Result::ERROR_DEVICE_LOST => { error!("invalid device state: lost"); @@ -86,42 +113,48 @@ impl Fence { /// Returns `true` if work has been queued against this fence. pub fn is_queued(&self) -> bool { - self.queued + self.queued.get() } /// Marks this fence as having work queued against it. pub(crate) fn mark_queued(&mut self) { - self.queued = true; + self.queued.set(true); } /// Resets this fence to the unsignaled state. /// + /// If queued work has already signaled, deferred payloads are released before the fence is + /// reset. + /// /// See [`vkResetFences`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkResetFences.html). pub fn reset(&mut self) -> Result<&mut Self, DriverError> { #[cfg(feature = "checked")] - if !self.queued { + if !self.queued.get() { return Ok(self); } - Device::reset_fences(&self.device, std::slice::from_ref(&self.handle))?; - self.queued = false; + if self.status()? { + Device::reset_fences(&self.device, std::slice::from_ref(&self.handle))?; + } + + self.queued.set(false); Ok(self) } - #[deprecated = "use status function"] + #[deprecated = "use wait"] #[doc(hidden)] pub fn wait_signaled(&mut self) -> Result<&mut Self, DriverError> { self.wait() } - /// Waits for this fence to signal, then drops any deferred payloads. + /// Waits for this fence to signal, then releases deferred payloads. /// /// See [`vkWaitForFences`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkWaitForFences.html). #[profiling::function] pub fn wait(&mut self) -> Result<&mut Self, DriverError> { #[cfg(feature = "checked")] - if !self.queued { + if !self.queued.get() { return Ok(self); } @@ -139,7 +172,7 @@ impl Drop for Fence { return; } - if self.queued && self.wait().is_err() { + if self.queued.get() && self.wait().is_err() { return; } diff --git a/src/lib.rs b/src/lib.rs index 8f21d821..3effd9d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,18 +66,160 @@ use { ops::Range, ops::{Deref, DerefMut}, slice::Iter, - sync::Arc, + sync::{ + Arc, + atomic::{AtomicU8, Ordering}, + }, }, vk_sync::AccessType, }; #[cfg(feature = "checked")] -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::atomic::AtomicU64; type CommandFn = Arc Fn(CommandRef<'a>) + Send + Sync>; type CommandFnOnce = Box; type NodeIndex = usize; +#[derive(Debug)] +struct AtomicCommandExecution(AtomicU8); + +impl AtomicCommandExecution { + const PENDING: u8 = 0xf0; + const EXECUTED: u8 = 0xf1; + const ABANDONED: u8 = 0xf2; + + fn new_pending() -> Arc { + Arc::new(Self(AtomicU8::new(Self::PENDING))) + } + + fn compare_pending_exchange_abandoned(&self) { + let _ = self.0.compare_exchange( + Self::PENDING, + Self::ABANDONED, + Ordering::AcqRel, + Ordering::Acquire, + ); + } + + fn compare_pending_exchange_executed(&self) { + let _ = self.0.compare_exchange( + Self::PENDING, + Self::EXECUTED, + Ordering::AcqRel, + Ordering::Acquire, + ); + } + + fn load(&self) -> u8 { + self.0.load(Ordering::Acquire) + } +} + +impl Drop for AtomicCommandExecution { + fn drop(&mut self) { + self.compare_pending_exchange_abandoned(); + } +} + +/// Tracks whether a graph command has completed device execution. +#[derive(Clone, Debug)] +pub struct CommandExecution(Arc); + +impl CommandExecution { + /// Returns `true` when the tracked command has completed device execution. + /// + /// Returns [`CommandExecutionAbandoned`] if the graph command can no longer execute, such as + /// when the graph, submission, or queued work was dropped before successful completion. + pub fn has_executed(&self) -> Result { + match self.0.load() { + AtomicCommandExecution::PENDING => Ok(false), + AtomicCommandExecution::EXECUTED => Ok(true), + _ => Err(CommandExecutionAbandoned), + } + } +} + +/// Error returned when a tracked command execution can no longer complete. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct CommandExecutionAbandoned; + +impl From for crate::driver::DriverError { + fn from(_: CommandExecutionAbandoned) -> Self { + Self::InvalidData + } +} + +#[derive(Debug, Default)] +enum CommandExecutions { + #[default] + None, + One(Arc), + Many(Arc<[Arc]>), +} + +impl Clone for CommandExecutions { + fn clone(&self) -> Self { + Self::None + } +} + +impl CommandExecutions { + fn signal_abandoned(&self) { + self.for_each(AtomicCommandExecution::compare_pending_exchange_abandoned); + } + + fn signal_executed(&self) { + self.for_each(AtomicCommandExecution::compare_pending_exchange_executed); + } + + fn extend(&mut self, other: Self) { + match (mem::take(self), other) { + (Self::None, rhs) => *self = rhs, + (lhs, Self::None) => *self = lhs, + (Self::One(lhs), Self::One(rhs)) => *self = Self::Many(Arc::from([lhs, rhs])), + (Self::One(lhs), Self::Many(rhs)) => { + let mut states = Vec::with_capacity(rhs.len() + 1); + states.push(lhs); + states.extend(rhs.iter().cloned()); + *self = Self::Many(Arc::from(states)); + } + (Self::Many(lhs), Self::One(rhs)) => { + let mut states = Vec::with_capacity(lhs.len() + 1); + states.extend(lhs.iter().cloned()); + states.push(rhs); + *self = Self::Many(Arc::from(states)); + } + (Self::Many(lhs), Self::Many(rhs)) => { + let mut states = Vec::with_capacity(lhs.len() + rhs.len()); + states.extend(lhs.iter().cloned()); + states.extend(rhs.iter().cloned()); + *self = Self::Many(Arc::from(states)); + } + } + } + + fn for_each(&self, mut f: impl FnMut(&AtomicCommandExecution)) { + match self { + Self::None => {} + Self::One(state) => f(state), + Self::Many(states) => { + for state in states.iter() { + f(state); + } + } + } + } + + fn track(&mut self) -> CommandExecution { + let tracker = AtomicCommandExecution::new_pending(); + let cmd_exec = CommandExecution(tracker.clone()); + self.extend(Self::One(tracker)); + + cmd_exec + } +} + #[cfg(feature = "checked")] #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub(crate) struct GraphId(u64); @@ -410,6 +552,7 @@ struct CommandData { name: Option, stream_scope_id: Option, + tracking: CommandExecutions, } impl CommandData { @@ -474,6 +617,12 @@ impl CommandData { } } +impl Drop for CommandData { + fn drop(&mut self) { + self.tracking.signal_abandoned(); + } +} + enum CommandFunction { Once(CommandFnOnce), Reusable(CommandFn), @@ -1835,7 +1984,9 @@ mod test { use ash::vk; - use super::{AnyResource, Graph, Node, ResourceMap}; + use super::{ + AnyResource, CommandExecutionAbandoned, CommandExecutions, Graph, Node, ResourceMap, + }; use crate::driver::{ DriverError, accel_struct::{AccelerationStructure, AccelerationStructureInfo}, @@ -1846,6 +1997,74 @@ mod test { }; use crate::pool::{Pool, hash::HashPool}; + #[test] + fn command_execution_starts_pending() { + let mut graph = Graph::new(); + let mut cmd = graph.begin_cmd(); + let execution = cmd.track_execution(); + + assert_eq!(execution.has_executed(), Ok(false)); + } + + #[test] + fn command_execution_is_abandoned_when_graph_drops() { + let execution = { + let mut graph = Graph::new(); + let mut cmd = graph.begin_cmd(); + + cmd.track_execution() + }; + + assert_eq!(execution.has_executed(), Err(CommandExecutionAbandoned)); + } + + #[test] + fn command_executions_track_multiple_handles() { + let mut executions = CommandExecutions::default(); + let first = executions.track(); + let second = executions.track(); + + executions.signal_executed(); + + assert_eq!(first.has_executed(), Ok(true)); + assert_eq!(second.has_executed(), Ok(true)); + } + + #[test] + fn command_executions_extend_preserves_both_sides() { + let mut lhs = CommandExecutions::default(); + let first = lhs.track(); + let mut rhs = CommandExecutions::default(); + let second = rhs.track(); + + lhs.extend(rhs); + lhs.signal_executed(); + + assert_eq!(first.has_executed(), Ok(true)); + assert_eq!(second.has_executed(), Ok(true)); + } + + #[test] + fn command_execution_stays_executed_after_tracker_drops() { + let execution = { + let mut executions = CommandExecutions::default(); + let execution = executions.track(); + + executions.signal_executed(); + + execution + }; + + assert_eq!(execution.has_executed(), Ok(true)); + } + + #[test] + fn command_execution_abandoned_converts_to_driver_error() { + let error = DriverError::from(CommandExecutionAbandoned); + + assert!(matches!(error, DriverError::InvalidData)); + } + mod integration { use super::*; diff --git a/src/submission.rs b/src/submission.rs index 920d059c..083bfce8 100644 --- a/src/submission.rs +++ b/src/submission.rs @@ -34,7 +34,7 @@ use { cmd_buf::{CommandBuffer, CommandBufferInfo}, descriptor_set::{DescriptorPool, DescriptorPoolInfo}, device::Device, - fence::Fence, + fence::{Fence, FenceDroppable}, format_aspect_mask, graphics::{DepthStencilInfo, GraphicsPipeline}, image::{ @@ -58,7 +58,6 @@ use { std::{ cell::RefCell, collections::{BTreeMap, HashMap, HashSet, VecDeque}, - fmt::Debug as FmtDebug, iter::repeat_n, mem::take, ops::Range, @@ -974,6 +973,18 @@ impl Drop for CommandRecordingResources { } } +#[derive(Debug)] +struct SubmittedCommand { + cmd: CommandData, + _resources: CommandRecordingResources, +} + +impl SubmittedCommand { + fn signal_executed(&self) { + self.cmd.tracking.signal_executed(); + } +} + #[derive(Clone, Copy, Debug)] struct ImageQueueOwnershipTransfer { dst_queue_family_index: u32, @@ -1432,7 +1443,7 @@ where drop(state); - fence.drop_when_signaled(self.state.clone()); + fence.drop_fence_droppable(RecordedSubmissionDrop(self.state.clone())); } /// Submits this recorded submission using either `vkQueueSubmit` or `vkQueueSubmit2`. @@ -1443,7 +1454,7 @@ where submit_info: impl Into>, ) -> Result<(), DriverError> { #[cfg(feature = "checked")] - if fence.queued { + if fence.queued.get() { fence.wait()?; fence.reset()?; } @@ -1581,9 +1592,33 @@ where #[derive(Debug)] struct RecordedSubmissionState { _releases: Vec, + executed: bool, submission: Submission, } +impl RecordedSubmissionState { + fn signal_executed(&mut self) { + if self.executed { + return; + } + + self.executed = true; + self.submission.signal_executed(); + } +} + +#[derive(Debug)] +struct RecordedSubmissionDrop(Arc>); + +impl FenceDroppable for RecordedSubmissionDrop { + fn fence_signaled(&mut self) { + self.0 + .lock() + .expect("poisoned recorded submission state") + .signal_executed(); + } +} + /// A [`Submission`] bound to a specific command buffer for explicit recording and submission. #[derive(Debug)] #[read_only::cast] @@ -1850,7 +1885,7 @@ pub struct Submission { Option>, queue_ownership_release_groups: Vec, recorded_commands: Vec, - submit_retained: Vec>, + submit_retained: Vec, } impl Submission { @@ -1887,6 +1922,12 @@ impl Submission { &self.graph } + fn signal_executed(&self) { + for command in &self.submit_retained { + command.signal_executed(); + } + } + pub(crate) fn assert_reusable_commands(&self) { for cmd in &self.graph.cmds { for exec in &cmd.execs { @@ -1982,6 +2023,7 @@ impl Submission { queue_ownership_release_waits: waits, state: Arc::new(Mutex::new(RecordedSubmissionState { _releases: releases, + executed: false, submission: self, })), } @@ -4785,12 +4827,13 @@ impl Submission { if cmd_idx == schedule_idx { // This was a scheduled cmd - store it! - self.submit_retained.push(Box::new(( + self.submit_retained.push(SubmittedCommand { cmd, - self.recorded_commands + _resources: self + .recorded_commands .pop() .expect("missing recorded command"), - ))); + }); break; } else { debug_assert!(cmd_idx > schedule_idx); @@ -6361,6 +6404,7 @@ mod tests { name: None, stream_scope_id: None, + tracking: Default::default(), }; Submission::build_subpass_dependencies(&pass, &ExternalRenderPassAccessHistory::new(1)) @@ -6407,6 +6451,7 @@ mod tests { name: None, stream_scope_id: None, + tracking: Default::default(), }; Submission::build_subpass_dependencies(&pass, &ExternalRenderPassAccessHistory::new(1)) @@ -6483,6 +6528,7 @@ mod tests { name: None, stream_scope_id: None, + tracking: Default::default(), } } @@ -6948,6 +6994,7 @@ mod tests { state: Arc::new(Mutex::new(RecordedSubmissionState { submission, _releases: Vec::new(), + executed: false, })), }; @@ -7003,6 +7050,7 @@ mod tests { state: Arc::new(Mutex::new(RecordedSubmissionState { submission, _releases: Vec::new(), + executed: false, })), }; @@ -8222,6 +8270,7 @@ mod tests { name: None, stream_scope_id: None, + tracking: Default::default(), }; let dependencies = Submission::build_subpass_dependencies(&pass, &ExternalRenderPassAccessHistory::new(1)); From bee3ea077a0e89c07968c9c3399d6c9103d93691 Mon Sep 17 00:00:00 2001 From: John Wells Date: Thu, 25 Jun 2026 08:37:38 -0400 Subject: [PATCH 6/7] Move graph one-shot commands to Command --- crates/vk-graph-egui/src/lib.rs | 54 +-- examples/mip_compute.rs | 46 ++- examples/multithread.rs | 55 +-- guide/src/README.md | 3 + guide/src/cmd.md | 51 ++- guide/src/pipeline.md | 4 +- guide/src/usage_debugging.md | 3 +- src/cmd/mod.rs | 519 ++++++++++++++++++++++++- src/lib.rs | 664 +++++++++++++++----------------- src/stream.rs | 68 +++- 10 files changed, 1008 insertions(+), 459 deletions(-) diff --git a/crates/vk-graph-egui/src/lib.rs b/crates/vk-graph-egui/src/lib.rs index c8cfde13..df489ca6 100644 --- a/crates/vk-graph-egui/src/lib.rs +++ b/crates/vk-graph-egui/src/lib.rs @@ -127,31 +127,35 @@ impl Egui { let image = graph.bind_resource(self.textures.remove(id).expect("missing texture")); - graph.copy_buffer_to_image_region( - tmp_buf, - image, - [vk::BufferImageCopy { - buffer_offset: 0, - buffer_row_length: delta.image.width() as u32, - buffer_image_height: delta.image.height() as u32, - image_offset: vk::Offset3D { - x: pos[0] as i32, - y: pos[1] as i32, - z: 0, - }, - image_extent: vk::Extent3D { - width: delta.image.width() as u32, - height: delta.image.height() as u32, - depth: 1, - }, - image_subresource: vk::ImageSubresourceLayers { - aspect_mask: vk::ImageAspectFlags::COLOR, - mip_level: 0, - base_array_layer: 0, - layer_count: 1, - }, - }], - ); + graph + .begin_cmd() + .debug_name("copy buffer to image") + .copy_buffer_to_image( + tmp_buf, + image, + [vk::BufferImageCopy { + buffer_offset: 0, + buffer_row_length: delta.image.width() as u32, + buffer_image_height: delta.image.height() as u32, + image_offset: vk::Offset3D { + x: pos[0] as i32, + y: pos[1] as i32, + z: 0, + }, + image_extent: vk::Extent3D { + width: delta.image.width() as u32, + height: delta.image.height() as u32, + depth: 1, + }, + image_subresource: vk::ImageSubresourceLayers { + aspect_mask: vk::ImageAspectFlags::COLOR, + mip_level: 0, + base_array_layer: 0, + layer_count: 1, + }, + }], + ) + .end_cmd(); (*id, AnyImageNode::from(image)) } else { let image = graph.bind_resource( diff --git a/examples/mip_compute.rs b/examples/mip_compute.rs index 7b5f72c2..61ce5362 100644 --- a/examples/mip_compute.rs +++ b/examples/mip_compute.rs @@ -136,27 +136,31 @@ fn main() -> Result<(), DriverError> { &device, BufferInfo::host_mem(size_of::() as _, vk::BufferUsageFlags::TRANSFER_DST), )?); - graph.copy_image_to_buffer_region( - depth_pyramid, - depth_pixel, - [vk::BufferImageCopy { - buffer_offset: 0, - buffer_row_length: 1, - buffer_image_height: 1, - image_subresource: vk::ImageSubresourceLayers { - aspect_mask: vk::ImageAspectFlags::COLOR, - mip_level: depth_info.mip_level_count - 1, - base_array_layer: 0, - layer_count: 1, - }, - image_offset: vk::Offset3D { x: 0, y: 0, z: 0 }, - image_extent: vk::Extent3D { - width: 1, - height: 1, - depth: 1, - }, - }], - ); + graph + .begin_cmd() + .debug_name("copy image to buffer") + .copy_image_to_buffer( + depth_pyramid, + depth_pixel, + [vk::BufferImageCopy { + buffer_offset: 0, + buffer_row_length: 1, + buffer_image_height: 1, + image_subresource: vk::ImageSubresourceLayers { + aspect_mask: vk::ImageAspectFlags::COLOR, + mip_level: depth_info.mip_level_count - 1, + base_array_layer: 0, + layer_count: 1, + }, + image_offset: vk::Offset3D { x: 0, y: 0, z: 0 }, + image_extent: vk::Extent3D { + width: 1, + height: 1, + depth: 1, + }, + }], + ) + .end_cmd(); let depth_pixel = graph.resource(depth_pixel).clone(); diff --git a/examples/multithread.rs b/examples/multithread.rs index 01a230ed..7932f50c 100644 --- a/examples/multithread.rs +++ b/examples/multithread.rs @@ -199,31 +199,36 @@ fn main() -> anyhow::Result<()> { let j = frame.width as f32 / 10.0; let k = frame.height as f32 / 10.0; - frame.graph.blit_image_region( - image, - frame.swapchain_image, - vk::Filter::NEAREST, - [vk::ImageBlit { - src_subresource: COLOR_SUBRESOURCE_LAYER, - src_offsets: [ - vk::Offset3D { x: 0, y: 0, z: 0 }, - vk::Offset3D { x: 10, y: 10, z: 1 }, - ], - dst_subresource: COLOR_SUBRESOURCE_LAYER, - dst_offsets: [ - vk::Offset3D { - x: ((x * j) + j) as i32, - y: ((y * k) + k) as i32, - z: 0, - }, - vk::Offset3D { - x: ((x * j) + (2.0 * j)) as i32, - y: ((y * k) + (2.0 * k)) as i32, - z: 1, - }, - ], - }], - ); + frame + .graph + .begin_cmd() + .debug_name("blit image") + .blit_image( + image, + frame.swapchain_image, + vk::Filter::NEAREST, + [vk::ImageBlit { + src_subresource: COLOR_SUBRESOURCE_LAYER, + src_offsets: [ + vk::Offset3D { x: 0, y: 0, z: 0 }, + vk::Offset3D { x: 10, y: 10, z: 1 }, + ], + dst_subresource: COLOR_SUBRESOURCE_LAYER, + dst_offsets: [ + vk::Offset3D { + x: ((x * j) + j) as i32, + y: ((y * k) + k) as i32, + z: 0, + }, + vk::Offset3D { + x: ((x * j) + (2.0 * j)) as i32, + y: ((y * k) + (2.0 * k)) as i32, + z: 1, + }, + ], + }], + ) + .end_cmd(); } let fps = (1.0 / elapsed.as_secs_f32()).round(); diff --git a/guide/src/README.md b/guide/src/README.md index bc09e2a1..fd8d45af 100644 --- a/guide/src/README.md +++ b/guide/src/README.md @@ -25,6 +25,9 @@ Driver Graph : _Builder-pattern for Vulkan commands_ +Command + : _Explicit command recording and region-level transfer helpers_ + Submission : _Automated graph execution_ diff --git a/guide/src/cmd.md b/guide/src/cmd.md index 55c855f0..c85c3bb8 100644 --- a/guide/src/cmd.md +++ b/guide/src/cmd.md @@ -3,6 +3,7 @@ `vk-graph` exposes two styles of commands: API docs: [`Graph::begin_cmd`](https://docs.rs/vk-graph/latest/vk_graph/struct.Graph.html#method.begin_cmd), +[`Graph::builder`](https://docs.rs/vk-graph/latest/vk_graph/struct.Graph.html#method.builder), [`Command::record_cmd`](https://docs.rs/vk-graph/latest/vk_graph/cmd/struct.Command.html#method.record_cmd), [`Graph::finalize`](https://docs.rs/vk-graph/latest/vk_graph/struct.Graph.html#method.finalize). @@ -101,13 +102,14 @@ graph - Use `copy_buffer_to_image` and `copy_image_to_buffer` for upload and readback paths. - Use `copy_image` when source and destination texel footprints already match. - Use `blit_image` when you need scaling or filtering. -- Use the `*_region` variants when you need precise offsets, layers, mip levels, or partial copies. +- Use `begin_cmd()` command methods when you need precise offsets, layers, mip levels, or partial + copies. -## Region Variants +## Explicit Regions -Each built-in helper also has a more explicit form such as `copy_buffer_region` or -`copy_buffer_to_image_region`. Use those variants when the whole-resource convenience behavior is -too broad. +Whole-resource helpers live on `Graph`. Explicit-region transfer methods live on `Command`. Use the +`Command` versions to compose with `debug_name`, resource access declarations, and other command +recording. ```no_run # use vk_graph::Graph; @@ -128,14 +130,35 @@ let dst = graph.bind_resource(Buffer::create( BufferInfo::device_mem(4096, vk::BufferUsageFlags::TRANSFER_DST), )?); -graph.copy_buffer_region( - src, - dst, - [vk::BufferCopy { - src_offset: 512, - dst_offset: 1024, - size: 256, - }], -); +graph + .begin_cmd() + .debug_name("copy buffer region") + .copy_buffer( + src, + dst, + [vk::BufferCopy { + src_offset: 512, + dst_offset: 1024, + size: 256, + }], + ) + .end_cmd(); # Ok(()) } ``` + +## Graph Builder + +`Graph::builder()` offers the same whole-resource helpers in a chainable style and finishes with +`build()`: + +```no_run +# use vk_graph::Graph; +# use vk_graph::driver::ash::vk; +# use vk_graph::node::{BufferNode, ImageNode}; +# fn test(buffer: BufferNode, image: ImageNode) { +let graph = Graph::builder() + .update_buffer(buffer, 0, [1, 2, 3, 4]) + .copy_buffer_to_image(buffer, image) + .build(); +# } +``` diff --git a/guide/src/pipeline.md b/guide/src/pipeline.md index 194ea116..7256d4f6 100644 --- a/guide/src/pipeline.md +++ b/guide/src/pipeline.md @@ -78,7 +78,9 @@ graph ``` A call to `Graph::end_cmd` is not required. The _end-command_ method exists to support builder-style -function-chaining. In the above example two commands are built and added to the graph. +function-chaining. In the above example two commands are built and added to the graph. If you want a +standalone command builder, `Command::builder(&mut graph)` and `begin_cmd().into_builder()` expose +the same command-level transfer helpers and finish with `push_cmd()` or `build()`. ## Shaders diff --git a/guide/src/usage_debugging.md b/guide/src/usage_debugging.md index 1b62d008..9cc6fd7f 100644 --- a/guide/src/usage_debugging.md +++ b/guide/src/usage_debugging.md @@ -98,7 +98,8 @@ echo 100 | sudo tee /sys/devices/system/cpu/intel_pstate/min_perf_pct misuse patterns that the VVL cannot catch: - Missing `resource_access` / `shader_resource_access` declarations before using a resource -- [`update_buffer`] and [`copy_buffer_region`](crate::Graph::copy_buffer_region) buffer bounds +- [`update_buffer`](crate::Graph::update_buffer) and command-level + [`copy_buffer`](crate::cmd::Command::copy_buffer) buffer bounds - Valid image aspect masks and subresource ranges - Cross-graph node ownership checks diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 27211d42..cab14cc0 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -46,7 +46,10 @@ use { }, crate::{ NodeIndex, - driver::{buffer::BufferSubresourceRange, image::ImageViewInfo}, + driver::{ + buffer::BufferSubresourceRange, format_texel_block_extent, format_texel_block_size, + image::ImageViewInfo, image_subresource_range_from_layers, + }, stream::{AccelerationStructureArg, BufferArg, ImageArg}, }, ash::vk, @@ -85,6 +88,137 @@ pub struct Command<'a> { pub(super) graph: &'a mut Graph, } +/// Builder for incrementally constructing a [`Command`]. +pub struct CommandBuilder<'a> { + cmd: Command<'a>, +} + +impl<'a> CommandBuilder<'a> { + /// Begins a new command in `graph`. + pub fn new(graph: &'a mut Graph) -> Self { + Self { + cmd: graph.begin_cmd(), + } + } + + /// Builds the command without pushing it to the graph. + pub fn build(self) -> Command<'a> { + self.cmd + } + + /// Pushes the command onto its graph and returns the graph. + pub fn push_cmd(self) -> &'a mut Graph { + self.cmd.end_cmd() + } + + /// Blits image regions. + #[allow(deprecated)] + pub fn blit_image( + mut self, + src: impl Into, + dst: impl Into, + filter: vk::Filter, + regions: impl AsRef<[vk::ImageBlit]> + 'static + Send, + ) -> Self { + self.cmd = self.cmd.blit_image(src, dst, filter, regions); + self + } + + /// Clears a color image. + #[allow(deprecated)] + pub fn clear_color_image( + mut self, + image: impl Into, + color: impl Into, + ) -> Self { + self.cmd = self.cmd.clear_color_image(image, color); + self + } + + /// Clears a depth/stencil image. + #[allow(deprecated)] + pub fn clear_depth_stencil_image( + mut self, + image: impl Into, + depth: f32, + stencil: u32, + ) -> Self { + self.cmd = self.cmd.clear_depth_stencil_image(image, depth, stencil); + self + } + + /// Copies data between buffer regions. + #[allow(deprecated)] + pub fn copy_buffer( + mut self, + src: impl Into, + dst: impl Into, + regions: impl AsRef<[vk::BufferCopy]> + 'static + Send, + ) -> Self { + self.cmd = self.cmd.copy_buffer(src, dst, regions); + self + } + + /// Copies data from a buffer into image regions. + #[allow(deprecated)] + pub fn copy_buffer_to_image( + mut self, + src: impl Into, + dst: impl Into, + regions: impl AsRef<[vk::BufferImageCopy]> + 'static + Send, + ) -> Self { + self.cmd = self.cmd.copy_buffer_to_image(src, dst, regions); + self + } + + /// Copies data between image regions. + #[allow(deprecated)] + pub fn copy_image( + mut self, + src: impl Into, + dst: impl Into, + regions: impl AsRef<[vk::ImageCopy]> + 'static + Send, + ) -> Self { + self.cmd = self.cmd.copy_image(src, dst, regions); + self + } + + /// Copies image region data into a buffer. + #[allow(deprecated)] + pub fn copy_image_to_buffer( + mut self, + src: impl Into, + dst: impl Into, + regions: impl AsRef<[vk::BufferImageCopy]> + 'static + Send, + ) -> Self { + self.cmd = self.cmd.copy_image_to_buffer(src, dst, regions); + self + } + + /// Fills a region of a buffer with a fixed value. + #[allow(deprecated)] + pub fn fill_buffer( + mut self, + buffer: impl Into, + region: Range, + data: u32, + ) -> Self { + self.cmd = self.cmd.fill_buffer(buffer, region, data); + self + } + + /// Records a [`vkCmdUpdateBuffer`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdUpdateBuffer.html) command. + pub fn update_buffer( + mut self, + buffer: impl Into, + offset: vk::DeviceSize, + data: impl AsRef<[u8]> + 'static + Send, + ) -> Self { + self.cmd = self.cmd.update_buffer(buffer, offset, data); + self + } +} + #[allow(private_bounds)] impl<'a> Command<'a> { pub(super) fn new(graph: &'a mut Graph) -> Self { @@ -104,6 +238,16 @@ impl<'a> Command<'a> { } } + /// Begins a command builder in `graph`. + pub fn builder(graph: &'a mut Graph) -> CommandBuilder<'a> { + CommandBuilder::new(graph) + } + + /// Converts this command into a builder. + pub fn into_builder(self) -> CommandBuilder<'a> { + CommandBuilder { cmd: self } + } + /// Returns a handle that tracks whether this graph command has completed device execution. /// /// This may be called multiple times. Each returned handle independently observes the same @@ -247,6 +391,328 @@ impl<'a> Command<'a> { }); } + /// Blits image regions. + pub fn blit_image( + mut self, + src: impl Into, + dst: impl Into, + filter: vk::Filter, + regions: impl AsRef<[vk::ImageBlit]> + 'static + Send, + ) -> Self { + let src = src.into(); + let dst = dst.into(); + let regions = Arc::<[vk::ImageBlit]>::from(regions.as_ref()); + + for region in regions.as_ref() { + self.set_subresource_access( + src, + image_subresource_range_from_layers(region.src_subresource), + AccessType::TransferRead, + ); + self.set_subresource_access( + dst, + image_subresource_range_from_layers(region.dst_subresource), + AccessType::TransferWrite, + ); + } + + self.record_stream_mut(move |cmd| { + let src_image = cmd.resource(src).handle; + let dst_image = cmd.resource(dst).handle; + + unsafe { + cmd.device.cmd_blit_image( + cmd.handle, + src_image, + vk::ImageLayout::TRANSFER_SRC_OPTIMAL, + dst_image, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + regions.as_ref(), + filter, + ); + } + }); + self + } + + /// Clears a color image. + pub fn clear_color_image( + mut self, + image: impl Into, + color: impl Into, + ) -> Self { + let color = color.into().into(); + let image = image.into(); + let image_view = self.graph.resources[image.index()] + .expect_image_info() + .into(); + + self.set_subresource_access(image, image_view, AccessType::TransferWrite); + self.record_stream_mut(move |cmd| { + let image = cmd.resource(image); + + unsafe { + cmd.device.cmd_clear_color_image( + cmd.handle, + image.handle, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + &color, + &[image_view], + ); + } + }); + self + } + + /// Clears a depth/stencil image. + pub fn clear_depth_stencil_image( + mut self, + image: impl Into, + depth: f32, + stencil: u32, + ) -> Self { + let image = image.into(); + let image_view = self.graph.resources[image.index()] + .expect_image_info() + .into(); + + self.set_subresource_access(image, image_view, AccessType::TransferWrite); + self.record_stream_mut(move |cmd| { + let image = cmd.resource(image); + + unsafe { + cmd.device.cmd_clear_depth_stencil_image( + cmd.handle, + image.handle, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + &vk::ClearDepthStencilValue { depth, stencil }, + &[image_view], + ); + } + }); + self + } + + /// Copies data between buffer regions. + pub fn copy_buffer( + mut self, + src: impl Into, + dst: impl Into, + regions: impl AsRef<[vk::BufferCopy]> + 'static + Send, + ) -> Self { + let src = src.into(); + let dst = dst.into(); + let regions = Arc::<[vk::BufferCopy]>::from(regions.as_ref()); + + #[cfg(feature = "checked")] + let src_size = self.graph.resources[src.index()].expect_buffer_info().size; + + #[cfg(feature = "checked")] + let dst_size = self.graph.resources[dst.index()].expect_buffer_info().size; + + for region in regions.iter() { + #[cfg(feature = "checked")] + { + assert!( + region.src_offset + region.size <= src_size, + "source range end ({}) exceeds source size ({src_size})", + region.src_offset + region.size + ); + assert!( + region.dst_offset + region.size <= dst_size, + "destination range end ({}) exceeds destination size ({dst_size})", + region.dst_offset + region.size + ); + }; + + self.set_subresource_access( + src, + region.src_offset..region.src_offset + region.size, + AccessType::TransferRead, + ); + self.set_subresource_access( + dst, + region.dst_offset..region.dst_offset + region.size, + AccessType::TransferWrite, + ); + } + + self.record_stream_mut(move |cmd| { + let src = cmd.resource(src); + let dst = cmd.resource(dst); + + unsafe { + cmd.device + .cmd_copy_buffer(cmd.handle, src.handle, dst.handle, ®ions); + } + }); + self + } + + /// Copies data from a buffer into image regions. + pub fn copy_buffer_to_image( + mut self, + src: impl Into, + dst: impl Into, + regions: impl AsRef<[vk::BufferImageCopy]> + 'static + Send, + ) -> Self { + let src = src.into(); + let dst = dst.into(); + let dst_info = self.graph.resources[dst.index()].expect_image_info(); + let regions = Arc::<[vk::BufferImageCopy]>::from(regions.as_ref()); + + for region in regions.iter() { + let block_bytes_size = format_texel_block_size(dst_info.format); + let (block_height, block_width) = format_texel_block_extent(dst_info.format); + let data_size = block_bytes_size + * (region.buffer_row_length / block_width) + * (region.buffer_image_height / block_height); + + self.set_subresource_access( + src, + region.buffer_offset..region.buffer_offset + data_size as vk::DeviceSize, + AccessType::TransferRead, + ); + self.set_subresource_access( + dst, + image_subresource_range_from_layers(region.image_subresource), + AccessType::TransferWrite, + ); + } + + self.record_stream_mut(move |cmd| { + let src = cmd.resource(src); + let dst = cmd.resource(dst); + + unsafe { + cmd.device.cmd_copy_buffer_to_image( + cmd.handle, + src.handle, + dst.handle, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + ®ions, + ); + } + }); + self + } + + /// Copies data between image regions. + pub fn copy_image( + mut self, + src: impl Into, + dst: impl Into, + regions: impl AsRef<[vk::ImageCopy]> + 'static + Send, + ) -> Self { + let src = src.into(); + let dst = dst.into(); + let regions = Arc::<[vk::ImageCopy]>::from(regions.as_ref()); + + for region in regions.iter() { + self.set_subresource_access( + src, + image_subresource_range_from_layers(region.src_subresource), + AccessType::TransferRead, + ); + self.set_subresource_access( + dst, + image_subresource_range_from_layers(region.dst_subresource), + AccessType::TransferWrite, + ); + } + + self.record_stream_mut(move |cmd| { + let src = cmd.resource(src); + let dst = cmd.resource(dst); + + unsafe { + cmd.device.cmd_copy_image( + cmd.handle, + src.handle, + vk::ImageLayout::TRANSFER_SRC_OPTIMAL, + dst.handle, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + ®ions, + ); + } + }); + self + } + + /// Copies image region data into a buffer. + pub fn copy_image_to_buffer( + mut self, + src: impl Into, + dst: impl Into, + regions: impl AsRef<[vk::BufferImageCopy]> + 'static + Send, + ) -> Self { + let src = src.into(); + let src_info = self.graph.resources[src.index()].expect_image_info(); + let dst = dst.into(); + let regions = Arc::<[vk::BufferImageCopy]>::from(regions.as_ref()); + + for region in regions.iter() { + let block_bytes_size = format_texel_block_size(src_info.format); + let (block_height, block_width) = format_texel_block_extent(src_info.format); + let data_size = block_bytes_size + * (region.buffer_row_length / block_width) + * (region.buffer_image_height / block_height); + + self.set_subresource_access( + src, + image_subresource_range_from_layers(region.image_subresource), + AccessType::TransferRead, + ); + self.set_subresource_access( + dst, + region.buffer_offset..region.buffer_offset + data_size as vk::DeviceSize, + AccessType::TransferWrite, + ); + } + + self.record_stream_mut(move |cmd| { + let src = cmd.resource(src); + let dst = cmd.resource(dst); + + unsafe { + cmd.device.cmd_copy_image_to_buffer( + cmd.handle, + src.handle, + vk::ImageLayout::TRANSFER_SRC_OPTIMAL, + dst.handle, + ®ions, + ); + } + }); + self + } + + /// Fills a region of a buffer with a fixed value. + pub fn fill_buffer( + mut self, + buffer: impl Into, + region: Range, + data: u32, + ) -> Self { + let buffer = buffer.into(); + + self.set_subresource_access(buffer, region.clone(), AccessType::TransferWrite); + self.record_stream_mut(move |cmd| { + let buffer = cmd.resource(buffer); + + unsafe { + cmd.device.cmd_fill_buffer( + cmd.handle, + buffer.handle, + region.start, + region.end - region.start, + data, + ); + } + }); + self + } + pub(crate) fn record_stream( mut self, func: impl for<'r> Fn(CommandRef<'r>) + Send + Sync + 'static, @@ -350,6 +816,57 @@ impl<'a> Command<'a> { self.set_subresource_access(resource_node, subresource, access); self } + + /// Records a [`vkCmdUpdateBuffer`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdUpdateBuffer.html) + /// command. + /// + /// Vulkan requires `data` to be at most `65536` bytes. + /// + /// These constraints are validated by the Vulkan Validation Layer (VVL) when it is active. + /// When the `checked` feature is enabled, `vk-graph` also validates the data size and bounds + /// before recording the command. + #[profiling::function] + pub fn update_buffer( + mut self, + buffer: impl Into, + offset: vk::DeviceSize, + data: impl AsRef<[u8]> + 'static + Send, + ) -> Self { + debug_assert!(data.as_ref().len() <= 64 * 1024); + + let buffer = buffer.into(); + let data_end = offset + data.as_ref().len() as vk::DeviceSize; + + #[cfg(feature = "checked")] + { + assert!( + data.as_ref().len() <= 64 * 1024, + "data length ({}) exceeds vkCmdUpdateBuffer limit (65536)", + data.as_ref().len() + ); + + let buffer_info = self.graph.resources[buffer.index()].expect_buffer_info(); + + assert!( + data_end <= buffer_info.size, + "data range end ({data_end}) exceeds buffer size ({})", + buffer_info.size + ); + } + + let data = Arc::<[u8]>::from(data.as_ref()); + + self.set_subresource_access(buffer, offset..data_end, AccessType::TransferWrite); + self.record_stream_mut(move |cmd| { + let buffer = cmd.resource(buffer); + + unsafe { + cmd.device + .cmd_update_buffer(cmd.handle, buffer.handle, offset, &data); + } + }); + self + } } /// Describes the SPIR-V binding index, and optionally a specific descriptor set diff --git a/src/lib.rs b/src/lib.rs index 3effd9d2..d87fd480 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,10 +41,9 @@ use { accel_struct::AccelerationStructureInfo, buffer::BufferInfo, compute::ComputePipeline, - format_aspect_mask, format_texel_block_extent, format_texel_block_size, + format_aspect_mask, graphics::{DepthStencilInfo, GraphicsPipeline}, image::{ImageInfo, ImageViewInfo, SampleCount}, - image_subresource_range_from_layers, ray_tracing::RayTracingPipeline, render_pass::ResolveMode, shader::PipelineDescriptorInfo, @@ -968,6 +967,133 @@ pub struct Graph { graph_id: GraphId, } +/// Builder for incrementally constructing a [`Graph`]. +pub struct GraphBuilder { + graph: Graph, +} + +impl GraphBuilder { + /// Creates an empty graph builder. + pub fn new() -> Self { + Self { + graph: Graph::new(), + } + } + + /// Builds the graph. + pub fn build(self) -> Graph { + self.graph + } + + /// Binds a Vulkan buffer, image, or acceleration structure resource to this graph. + pub fn bind_resource(&mut self, resource: R) -> R::Node + where + R: Resource, + { + self.graph.bind_resource(resource) + } + + /// Copies an image, potentially performing format conversion. + pub fn blit_image( + mut self, + src: impl Into, + dst: impl Into, + filter: vk::Filter, + ) -> Self { + self.graph.blit_image(src, dst, filter); + self + } + + /// Clears a color image. + pub fn clear_color_image( + mut self, + image: impl Into, + color: impl Into, + ) -> Self { + self.graph.clear_color_image(image, color); + self + } + + /// Clears a depth/stencil image. + pub fn clear_depth_stencil_image( + mut self, + image: impl Into, + depth: f32, + stencil: u32, + ) -> Self { + self.graph.clear_depth_stencil_image(image, depth, stencil); + self + } + + /// Copies data between buffers. + pub fn copy_buffer( + mut self, + src: impl Into, + dst: impl Into, + ) -> Self { + self.graph.copy_buffer(src, dst); + self + } + + /// Copies data from a buffer into an image. + pub fn copy_buffer_to_image( + mut self, + src: impl Into, + dst: impl Into, + ) -> Self { + self.graph.copy_buffer_to_image(src, dst); + self + } + + /// Copies all layers of a source image to a destination image. + pub fn copy_image( + mut self, + src: impl Into, + dst: impl Into, + ) -> Self { + self.graph.copy_image(src, dst); + self + } + + /// Copies image data into a buffer. + pub fn copy_image_to_buffer( + mut self, + src: impl Into, + dst: impl Into, + ) -> Self { + self.graph.copy_image_to_buffer(src, dst); + self + } + + /// Fills a region of a buffer with a fixed value. + pub fn fill_buffer( + mut self, + buffer: impl Into, + region: Range, + data: u32, + ) -> Self { + self.graph.fill_buffer(buffer, region, data); + self + } + + /// Records a [`vkCmdUpdateBuffer`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdUpdateBuffer.html) command. + pub fn update_buffer( + mut self, + buffer: impl Into, + offset: vk::DeviceSize, + data: impl AsRef<[u8]> + 'static + Send, + ) -> Self { + self.graph.update_buffer(buffer, offset, data); + self + } +} + +impl Default for GraphBuilder { + fn default() -> Self { + Self::new() + } +} + impl Default for Graph { fn default() -> Self { Self { @@ -986,6 +1112,16 @@ impl Graph { Self::default() } + /// Creates an empty graph builder. + pub fn builder() -> GraphBuilder { + GraphBuilder::new() + } + + /// Converts this graph into a builder. + pub fn into_builder(self) -> GraphBuilder { + GraphBuilder { graph: self } + } + pub(crate) fn assert_node_owner(&self, _resource_node: &N) where N: Node, @@ -1037,41 +1173,44 @@ impl Graph { let dst = dst.into(); let dst_info = self.resources[dst.index()].expect_image_info(); - self.blit_image_region( - src, - dst, - filter, - [vk::ImageBlit { - src_subresource: vk::ImageSubresourceLayers { - aspect_mask: format_aspect_mask(src_info.format), - mip_level: 0, - base_array_layer: 0, - layer_count: 1, - }, - src_offsets: [ - vk::Offset3D { x: 0, y: 0, z: 0 }, - vk::Offset3D { - x: src_info.width as _, - y: src_info.height as _, - z: src_info.depth as _, + self.begin_cmd() + .debug_name("blit image") + .blit_image( + src, + dst, + filter, + [vk::ImageBlit { + src_subresource: vk::ImageSubresourceLayers { + aspect_mask: format_aspect_mask(src_info.format), + mip_level: 0, + base_array_layer: 0, + layer_count: 1, }, - ], - dst_subresource: vk::ImageSubresourceLayers { - aspect_mask: format_aspect_mask(dst_info.format), - mip_level: 0, - base_array_layer: 0, - layer_count: 1, - }, - dst_offsets: [ - vk::Offset3D { x: 0, y: 0, z: 0 }, - vk::Offset3D { - x: dst_info.width as _, - y: dst_info.height as _, - z: dst_info.depth as _, + src_offsets: [ + vk::Offset3D { x: 0, y: 0, z: 0 }, + vk::Offset3D { + x: src_info.width as _, + y: src_info.height as _, + z: src_info.depth as _, + }, + ], + dst_subresource: vk::ImageSubresourceLayers { + aspect_mask: format_aspect_mask(dst_info.format), + mip_level: 0, + base_array_layer: 0, + layer_count: 1, }, - ], - }], - ) + dst_offsets: [ + vk::Offset3D { x: 0, y: 0, z: 0 }, + vk::Offset3D { + x: dst_info.width as _, + y: dst_info.height as _, + z: dst_info.depth as _, + }, + ], + }], + ) + .end_cmd() } /// Copies regions of an image, potentially performing format conversion. @@ -1081,6 +1220,8 @@ impl Graph { /// /// [`vkCmdBlitImage`]: https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdBlitImage.html #[profiling::function] + #[doc(hidden)] + #[deprecated(note = "use Graph::begin_cmd().blit_image(...).end_cmd() for explicit regions")] pub fn blit_image_region( &mut self, src: impl Into, @@ -1088,37 +1229,10 @@ impl Graph { filter: vk::Filter, regions: impl AsRef<[vk::ImageBlit]> + 'static + Send, ) -> &mut Self { - let src = src.into(); - let dst = dst.into(); - let regions = Arc::<[vk::ImageBlit]>::from(regions.as_ref()); - - let mut cmd = self.begin_cmd().debug_name("blit image"); - - for region in regions.as_ref() { - let src_region = image_subresource_range_from_layers(region.src_subresource); - cmd.set_subresource_access(src, src_region, AccessType::TransferRead); - - let dst_region = image_subresource_range_from_layers(region.dst_subresource); - cmd.set_subresource_access(dst, dst_region, AccessType::TransferWrite); - } - - cmd.record_stream(move |cmd| { - let src_image = cmd.resource(src).handle; - let dst_image = cmd.resource(dst).handle; - - unsafe { - cmd.device.cmd_blit_image( - cmd.handle, - src_image, - vk::ImageLayout::TRANSFER_SRC_OPTIMAL, - dst_image, - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - regions.as_ref(), - filter, - ); - } - }) - .end_cmd() + self.begin_cmd() + .debug_name("blit image") + .blit_image(src, dst, filter, regions) + .end_cmd() } /// Clears a color image. @@ -1133,26 +1247,9 @@ impl Graph { image: impl Into, color: impl Into, ) -> &mut Self { - let color = color.into().into(); - let image = image.into(); - let image_view = self.resources[image.index()].expect_image_info().into(); - self.begin_cmd() .debug_name("clear color") - .subresource_access(image, image_view, AccessType::TransferWrite) - .record_stream(move |cmd| { - let image = cmd.resource(image); - - unsafe { - cmd.device.cmd_clear_color_image( - cmd.handle, - image.handle, - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - &color, - &[image_view], - ); - } - }) + .clear_color_image(image, color) .end_cmd() } @@ -1169,25 +1266,9 @@ impl Graph { depth: f32, stencil: u32, ) -> &mut Self { - let image = image.into(); - let image_view = self.resources[image.index()].expect_image_info().into(); - self.begin_cmd() .debug_name("clear depth/stencil") - .subresource_access(image, image_view, AccessType::TransferWrite) - .record_stream(move |cmd| { - let image = cmd.resource(image); - - unsafe { - cmd.device.cmd_clear_depth_stencil_image( - cmd.handle, - image.handle, - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - &vk::ClearDepthStencilValue { depth, stencil }, - &[image_view], - ); - } - }) + .clear_depth_stencil_image(image, depth, stencil) .end_cmd() } @@ -1207,15 +1288,18 @@ impl Graph { let src_info = self.resources[src.index()].expect_buffer_info(); let dst_info = self.resources[dst.index()].expect_buffer_info(); - self.copy_buffer_region( - src, - dst, - [vk::BufferCopy { - src_offset: 0, - dst_offset: 0, - size: src_info.size.min(dst_info.size), - }], - ) + self.begin_cmd() + .debug_name("copy buffer") + .copy_buffer( + src, + dst, + [vk::BufferCopy { + src_offset: 0, + dst_offset: 0, + size: src_info.size.min(dst_info.size), + }], + ) + .end_cmd() } /// Copies data between buffer regions. @@ -1225,61 +1309,18 @@ impl Graph { /// /// [`vkCmdCopyBuffer`]: https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdCopyBuffer.html #[profiling::function] + #[doc(hidden)] + #[deprecated(note = "use Graph::begin_cmd().copy_buffer(...).end_cmd() for explicit regions")] pub fn copy_buffer_region( &mut self, src: impl Into, dst: impl Into, regions: impl AsRef<[vk::BufferCopy]> + 'static + Send, ) -> &mut Self { - let src = src.into(); - let dst = dst.into(); - let regions = Arc::<[vk::BufferCopy]>::from(regions.as_ref()); - - #[cfg(feature = "checked")] - let src_size = self.resources[src.index()].expect_buffer_info().size; - - #[cfg(feature = "checked")] - let dst_size = self.resources[dst.index()].expect_buffer_info().size; - - let mut cmd = self.begin_cmd().debug_name("copy buffer"); - - for region in regions.iter() { - #[cfg(feature = "checked")] - { - assert!( - region.src_offset + region.size <= src_size, - "source range end ({}) exceeds source size ({src_size})", - region.src_offset + region.size - ); - assert!( - region.dst_offset + region.size <= dst_size, - "destination range end ({}) exceeds destination size ({dst_size})", - region.dst_offset + region.size - ); - }; - - cmd.set_subresource_access( - src, - region.src_offset..region.src_offset + region.size, - AccessType::TransferRead, - ); - cmd.set_subresource_access( - dst, - region.dst_offset..region.dst_offset + region.size, - AccessType::TransferWrite, - ); - } - - cmd.record_stream(move |cmd| { - let src = cmd.resource(src); - let dst = cmd.resource(dst); - - unsafe { - cmd.device - .cmd_copy_buffer(cmd.handle, src.handle, dst.handle, ®ions); - } - }) - .end_cmd() + self.begin_cmd() + .debug_name("copy buffer") + .copy_buffer(src, dst, regions) + .end_cmd() } /// Copies data from a buffer into an image. @@ -1295,27 +1336,30 @@ impl Graph { let dst = dst.into(); let dst_info = self.resources[dst.index()].expect_image_info(); - self.copy_buffer_to_image_region( - src, - dst, - [vk::BufferImageCopy { - buffer_offset: 0, - buffer_row_length: dst_info.width, - buffer_image_height: dst_info.height, - image_subresource: vk::ImageSubresourceLayers { - aspect_mask: format_aspect_mask(dst_info.format), - mip_level: 0, - base_array_layer: 0, - layer_count: 1, - }, - image_offset: Default::default(), - image_extent: vk::Extent3D { - depth: dst_info.depth, - height: dst_info.height, - width: dst_info.width, - }, - }], - ) + self.begin_cmd() + .debug_name("copy buffer to image") + .copy_buffer_to_image( + src, + dst, + [vk::BufferImageCopy { + buffer_offset: 0, + buffer_row_length: dst_info.width, + buffer_image_height: dst_info.height, + image_subresource: vk::ImageSubresourceLayers { + aspect_mask: format_aspect_mask(dst_info.format), + mip_level: 0, + base_array_layer: 0, + layer_count: 1, + }, + image_offset: Default::default(), + image_extent: vk::Extent3D { + depth: dst_info.depth, + height: dst_info.height, + width: dst_info.width, + }, + }], + ) + .end_cmd() } /// Copies data from a buffer into image regions. @@ -1325,53 +1369,20 @@ impl Graph { /// /// [`vkCmdCopyBufferToImage`]: https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdCopyBufferToImage.html #[profiling::function] + #[doc(hidden)] + #[deprecated( + note = "use Graph::begin_cmd().copy_buffer_to_image(...).end_cmd() for explicit regions" + )] pub fn copy_buffer_to_image_region( &mut self, src: impl Into, dst: impl Into, regions: impl AsRef<[vk::BufferImageCopy]> + 'static + Send, ) -> &mut Self { - let src = src.into(); - let dst = dst.into(); - let dst_info = self.resources[dst.index()].expect_image_info(); - let regions = Arc::<[vk::BufferImageCopy]>::from(regions.as_ref()); - - let mut cmd = self.begin_cmd().debug_name("copy buffer to image"); - - for region in regions.iter() { - let block_bytes_size = format_texel_block_size(dst_info.format); - let (block_height, block_width) = format_texel_block_extent(dst_info.format); - let data_size = block_bytes_size - * (region.buffer_row_length / block_width) - * (region.buffer_image_height / block_height); - - cmd.set_subresource_access( - src, - region.buffer_offset..region.buffer_offset + data_size as vk::DeviceSize, - AccessType::TransferRead, - ); - cmd.set_subresource_access( - dst, - image_subresource_range_from_layers(region.image_subresource), - AccessType::TransferWrite, - ); - } - - cmd.record_stream(move |cmd| { - let src = cmd.resource(src); - let dst = cmd.resource(dst); - - unsafe { - cmd.device.cmd_copy_buffer_to_image( - cmd.handle, - src.handle, - dst.handle, - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - ®ions, - ); - } - }) - .end_cmd() + self.begin_cmd() + .debug_name("copy buffer to image") + .copy_buffer_to_image(src, dst, regions) + .end_cmd() } /// Copies all layers of a source image to a destination image. @@ -1391,31 +1402,34 @@ impl Graph { let dst = dst.into(); let dst_info = self.resources[dst.index()].expect_image_info(); - self.copy_image_region( - src, - dst, - [vk::ImageCopy { - src_subresource: vk::ImageSubresourceLayers { - aspect_mask: format_aspect_mask(src_info.format), - mip_level: 0, - base_array_layer: 0, - layer_count: src_info.array_layer_count, - }, - src_offset: vk::Offset3D { x: 0, y: 0, z: 0 }, - dst_subresource: vk::ImageSubresourceLayers { - aspect_mask: format_aspect_mask(dst_info.format), - mip_level: 0, - base_array_layer: 0, - layer_count: src_info.array_layer_count, - }, - dst_offset: vk::Offset3D { x: 0, y: 0, z: 0 }, - extent: vk::Extent3D { - depth: src_info.depth.clamp(1, dst_info.depth), - height: src_info.height.clamp(1, dst_info.height), - width: src_info.width.min(dst_info.width), - }, - }], - ) + self.begin_cmd() + .debug_name("copy image") + .copy_image( + src, + dst, + [vk::ImageCopy { + src_subresource: vk::ImageSubresourceLayers { + aspect_mask: format_aspect_mask(src_info.format), + mip_level: 0, + base_array_layer: 0, + layer_count: src_info.array_layer_count, + }, + src_offset: vk::Offset3D { x: 0, y: 0, z: 0 }, + dst_subresource: vk::ImageSubresourceLayers { + aspect_mask: format_aspect_mask(dst_info.format), + mip_level: 0, + base_array_layer: 0, + layer_count: src_info.array_layer_count, + }, + dst_offset: vk::Offset3D { x: 0, y: 0, z: 0 }, + extent: vk::Extent3D { + depth: src_info.depth.clamp(1, dst_info.depth), + height: src_info.height.clamp(1, dst_info.height), + width: src_info.width.min(dst_info.width), + }, + }], + ) + .end_cmd() } /// Copies data between image regions. @@ -1425,47 +1439,18 @@ impl Graph { /// /// [`vkCmdCopyImage`]: https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdCopyImage.html #[profiling::function] + #[doc(hidden)] + #[deprecated(note = "use Graph::begin_cmd().copy_image(...).end_cmd() for explicit regions")] pub fn copy_image_region( &mut self, src: impl Into, dst: impl Into, regions: impl AsRef<[vk::ImageCopy]> + 'static + Send, ) -> &mut Self { - let src = src.into(); - let dst = dst.into(); - let regions = Arc::<[vk::ImageCopy]>::from(regions.as_ref()); - - let mut cmd = self.begin_cmd().debug_name("copy image"); - - for region in regions.iter() { - cmd.set_subresource_access( - src, - image_subresource_range_from_layers(region.src_subresource), - AccessType::TransferRead, - ); - cmd.set_subresource_access( - dst, - image_subresource_range_from_layers(region.dst_subresource), - AccessType::TransferWrite, - ); - } - - cmd.record_stream(move |cmd| { - let src = cmd.resource(src); - let dst = cmd.resource(dst); - - unsafe { - cmd.device.cmd_copy_image( - cmd.handle, - src.handle, - vk::ImageLayout::TRANSFER_SRC_OPTIMAL, - dst.handle, - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - ®ions, - ); - } - }) - .end_cmd() + self.begin_cmd() + .debug_name("copy image") + .copy_image(src, dst, regions) + .end_cmd() } /// Copies image data into a buffer. @@ -1483,27 +1468,30 @@ impl Graph { let src_info = self.resources[src.index()].expect_image_info(); - self.copy_image_to_buffer_region( - src, - dst, - [vk::BufferImageCopy { - buffer_offset: 0, - buffer_row_length: src_info.width, - buffer_image_height: src_info.height, - image_subresource: vk::ImageSubresourceLayers { - aspect_mask: format_aspect_mask(src_info.format), - mip_level: 0, - base_array_layer: 0, - layer_count: 1, - }, - image_offset: Default::default(), - image_extent: vk::Extent3D { - depth: src_info.depth, - height: src_info.height, - width: src_info.width, - }, - }], - ) + self.begin_cmd() + .debug_name("copy image to buffer") + .copy_image_to_buffer( + src, + dst, + [vk::BufferImageCopy { + buffer_offset: 0, + buffer_row_length: src_info.width, + buffer_image_height: src_info.height, + image_subresource: vk::ImageSubresourceLayers { + aspect_mask: format_aspect_mask(src_info.format), + mip_level: 0, + base_array_layer: 0, + layer_count: 1, + }, + image_offset: Default::default(), + image_extent: vk::Extent3D { + depth: src_info.depth, + height: src_info.height, + width: src_info.width, + }, + }], + ) + .end_cmd() } /// Copies image region data into a buffer. @@ -1513,53 +1501,20 @@ impl Graph { /// /// [`vkCmdCopyImageToBuffer`]: https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdCopyImageToBuffer.html #[profiling::function] + #[doc(hidden)] + #[deprecated( + note = "use Graph::begin_cmd().copy_image_to_buffer(...).end_cmd() for explicit regions" + )] pub fn copy_image_to_buffer_region( &mut self, src: impl Into, dst: impl Into, regions: impl AsRef<[vk::BufferImageCopy]> + 'static + Send, ) -> &mut Self { - let src = src.into(); - let src_info = self.resources[src.index()].expect_image_info(); - let dst = dst.into(); - let regions = Arc::<[vk::BufferImageCopy]>::from(regions.as_ref()); - - let mut cmd = self.begin_cmd().debug_name("copy image to buffer"); - - for region in regions.iter() { - let block_bytes_size = format_texel_block_size(src_info.format); - let (block_height, block_width) = format_texel_block_extent(src_info.format); - let data_size = block_bytes_size - * (region.buffer_row_length / block_width) - * (region.buffer_image_height / block_height); - - cmd.set_subresource_access( - src, - image_subresource_range_from_layers(region.image_subresource), - AccessType::TransferRead, - ); - cmd.set_subresource_access( - dst, - region.buffer_offset..region.buffer_offset + data_size as vk::DeviceSize, - AccessType::TransferWrite, - ); - } - - cmd.record_stream(move |cmd| { - let src = cmd.resource(src); - let dst = cmd.resource(dst); - - unsafe { - cmd.device.cmd_copy_image_to_buffer( - cmd.handle, - src.handle, - vk::ImageLayout::TRANSFER_SRC_OPTIMAL, - dst.handle, - ®ions, - ); - } - }) - .end_cmd() + self.begin_cmd() + .debug_name("copy image to buffer") + .copy_image_to_buffer(src, dst, regions) + .end_cmd() } /// Fills a region of a buffer with a fixed value. @@ -1573,24 +1528,9 @@ impl Graph { region: Range, data: u32, ) -> &mut Self { - let buffer = buffer.into(); - self.begin_cmd() .debug_name("fill buffer") - .subresource_access(buffer, region.clone(), AccessType::TransferWrite) - .record_stream(move |cmd| { - let buffer = cmd.resource(buffer); - - unsafe { - cmd.device.cmd_fill_buffer( - cmd.handle, - buffer.handle, - region.start, - region.end - region.start, - data, - ); - } - }) + .fill_buffer(buffer, region, data) .end_cmd() } @@ -1617,7 +1557,7 @@ impl Graph { #[profiling::function] pub fn finalize(mut self) -> Submission { // The final execution of each command has no function. - for cmd in &mut self.cmds { + self.cmds.retain_mut(|cmd| { debug_assert!(cmd.expect_last_exec().func.is_none()); cmd.execs.pop(); @@ -1625,7 +1565,9 @@ impl Graph { for exec in &mut cmd.execs { exec.accesses.freeze(); } - } + + !cmd.execs.is_empty() + }); Submission::new(self) } diff --git a/src/stream.rs b/src/stream.rs index 143de7b8..f640b86e 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -761,7 +761,9 @@ impl CommandStreamMut { self } - /// Stream equivalent of [`Graph::blit_image_region`]. + /// Deprecated stream equivalent of explicit-region blitting. + #[doc(hidden)] + #[deprecated(note = "use Command::blit_image for explicit regions")] pub fn blit_image_region( &mut self, src: impl Into, @@ -769,7 +771,11 @@ impl CommandStreamMut { filter: vk::Filter, regions: impl AsRef<[vk::ImageBlit]> + 'static + Send, ) -> &mut Self { - self.graph.blit_image_region(src, dst, filter, regions); + self.graph + .begin_cmd() + .debug_name("blit image") + .blit_image(src, dst, filter, regions) + .end_cmd(); self } @@ -804,14 +810,20 @@ impl CommandStreamMut { self } - /// Stream equivalent of [`Graph::copy_buffer_region`]. + /// Deprecated stream equivalent of explicit-region buffer copies. + #[doc(hidden)] + #[deprecated(note = "use Command::copy_buffer for explicit regions")] pub fn copy_buffer_region( &mut self, src: impl Into, dst: impl Into, regions: impl AsRef<[vk::BufferCopy]> + 'static + Send, ) -> &mut Self { - self.graph.copy_buffer_region(src, dst, regions); + self.graph + .begin_cmd() + .debug_name("copy buffer") + .copy_buffer(src, dst, regions) + .end_cmd(); self } @@ -825,14 +837,20 @@ impl CommandStreamMut { self } - /// Stream equivalent of [`Graph::copy_buffer_to_image_region`]. + /// Deprecated stream equivalent of explicit-region buffer-to-image copies. + #[doc(hidden)] + #[deprecated(note = "use Command::copy_buffer_to_image for explicit regions")] pub fn copy_buffer_to_image_region( &mut self, src: impl Into, dst: impl Into, regions: impl AsRef<[vk::BufferImageCopy]> + 'static + Send, ) -> &mut Self { - self.graph.copy_buffer_to_image_region(src, dst, regions); + self.graph + .begin_cmd() + .debug_name("copy buffer to image") + .copy_buffer_to_image(src, dst, regions) + .end_cmd(); self } @@ -846,14 +864,20 @@ impl CommandStreamMut { self } - /// Stream equivalent of [`Graph::copy_image_region`]. + /// Deprecated stream equivalent of explicit-region image copies. + #[doc(hidden)] + #[deprecated(note = "use Command::copy_image for explicit regions")] pub fn copy_image_region( &mut self, src: impl Into, dst: impl Into, regions: impl AsRef<[vk::ImageCopy]> + 'static + Send, ) -> &mut Self { - self.graph.copy_image_region(src, dst, regions); + self.graph + .begin_cmd() + .debug_name("copy image") + .copy_image(src, dst, regions) + .end_cmd(); self } @@ -867,14 +891,20 @@ impl CommandStreamMut { self } - /// Stream equivalent of [`Graph::copy_image_to_buffer_region`]. + /// Deprecated stream equivalent of explicit-region image-to-buffer copies. + #[doc(hidden)] + #[deprecated(note = "use Command::copy_image_to_buffer for explicit regions")] pub fn copy_image_to_buffer_region( &mut self, src: impl Into, dst: impl Into, regions: impl AsRef<[vk::BufferImageCopy]> + 'static + Send, ) -> &mut Self { - self.graph.copy_image_to_buffer_region(src, dst, regions); + self.graph + .begin_cmd() + .debug_name("copy image to buffer") + .copy_image_to_buffer(src, dst, regions) + .end_cmd(); self } @@ -1374,6 +1404,7 @@ mod tests { use super::*; use crate::{ driver::{ + buffer::BufferInfo, descriptor_set::{DescriptorPool, DescriptorPoolInfo}, render_pass::{RenderPass, RenderPassInfo}, }, @@ -1420,6 +1451,23 @@ mod tests { assert_eq!(graph.cmds.len(), 1); } + #[test] + fn graph_copy_wrapper_can_prepare_stream() { + let _stream = CommandStream::finalize(|stream| { + let src = stream.arg(BufferInfo::device_mem( + 4, + vk::BufferUsageFlags::TRANSFER_SRC, + )); + let dst = stream.arg(BufferInfo::device_mem( + 4, + vk::BufferUsageFlags::TRANSFER_DST, + )); + + stream.graph.copy_buffer(src, dst); + }) + .into_stream(); + } + #[test] fn reusable_callback_can_prepare_optimized_stream() { let mut pool = NoopPool; From e3dfac1676abb2fd6a432e53488ba70f2898920c Mon Sep 17 00:00:00 2001 From: John Wells Date: Thu, 25 Jun 2026 08:46:36 -0400 Subject: [PATCH 7/7] v0.14.3 --- CHANGELOG.md | 12 +++++++++++- Cargo.toml | 6 +++--- crates/vk-graph-egui/CHANGELOG.md | 5 +++++ crates/vk-graph-egui/Cargo.toml | 2 +- crates/vk-graph-imgui/CHANGELOG.md | 5 +++++ crates/vk-graph-imgui/Cargo.toml | 2 +- crates/vk-graph-window/CHANGELOG.md | 5 +++++ crates/vk-graph-window/Cargo.toml | 2 +- 8 files changed, 32 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b1eb72e..f1bd97e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,17 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -## [0.14.3] - TBD +## [0.14.3] - 2026-06-25 + +### Added + +- Command builder one-shot commands for recording graph work. +- `vk-graph-imgui` support for frame-scoped user images in ImGui widgets. + +### Changed + +- Workspace integration crates affected by this release now target `vk-graph` `0.14.3` and are + versioned as `0.1.2`. ### Deprecated diff --git a/Cargo.toml b/Cargo.toml index e9428858..20a2da79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,11 +25,11 @@ log = "0.4" profiling = "1.0" read-only = "0.1" vk-graph = { path = "", version = "0.14.3" } -vk-graph-egui = { path = "crates/vk-graph-egui", version = "0.1.1" } +vk-graph-egui = { path = "crates/vk-graph-egui", version = "0.1.2" } vk-graph-fx = { path = "crates/vk-graph-fx", version = "0.1.1" } vk-graph-hot = { path = "crates/vk-graph-hot", version = "0.1.1" } -vk-graph-imgui = { path = "crates/vk-graph-imgui", version = "0.1.1" } -vk-graph-window = { path = "crates/vk-graph-window", version = "0.1.1" } +vk-graph-imgui = { path = "crates/vk-graph-imgui", version = "0.1.2" } +vk-graph-window = { path = "crates/vk-graph-window", version = "0.1.2" } winit = "0.30" [package] diff --git a/crates/vk-graph-egui/CHANGELOG.md b/crates/vk-graph-egui/CHANGELOG.md index 5ecea9ba..7e248bd7 100644 --- a/crates/vk-graph-egui/CHANGELOG.md +++ b/crates/vk-graph-egui/CHANGELOG.md @@ -6,6 +6,11 @@ This crate is versioned independently from `vk-graph`. ## [Unreleased] +## [0.1.2] - 2026-06-25 + +- Supports `vk-graph` `0.14.3`. +- Uses the command builder API for texture upload copies. + ## [0.1.1] - 2026-06-18 - Supports `vk-graph` `0.14.2`. diff --git a/crates/vk-graph-egui/Cargo.toml b/crates/vk-graph-egui/Cargo.toml index 14cbaf07..08ecc6a8 100644 --- a/crates/vk-graph-egui/Cargo.toml +++ b/crates/vk-graph-egui/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vk-graph-egui" -version = "0.1.1" +version = "0.1.2" authors = ["Christian Döring "] description = "egui renderer integration for vk-graph" documentation = "https://docs.rs/vk-graph-egui" diff --git a/crates/vk-graph-imgui/CHANGELOG.md b/crates/vk-graph-imgui/CHANGELOG.md index 5ecea9ba..085d9aa7 100644 --- a/crates/vk-graph-imgui/CHANGELOG.md +++ b/crates/vk-graph-imgui/CHANGELOG.md @@ -6,6 +6,11 @@ This crate is versioned independently from `vk-graph`. ## [Unreleased] +## [0.1.2] - 2026-06-25 + +- Supports `vk-graph` `0.14.3`. +- Adds frame-scoped image registration for rendering user textures in ImGui widgets. + ## [0.1.1] - 2026-06-18 - Supports `vk-graph` `0.14.2`. diff --git a/crates/vk-graph-imgui/Cargo.toml b/crates/vk-graph-imgui/Cargo.toml index 934a93f2..549f097a 100644 --- a/crates/vk-graph-imgui/Cargo.toml +++ b/crates/vk-graph-imgui/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vk-graph-imgui" -version = "0.1.1" +version = "0.1.2" authors = ["John Wells "] description = "Dear ImGui renderer integration for vk-graph" documentation = "https://docs.rs/vk-graph-imgui" diff --git a/crates/vk-graph-window/CHANGELOG.md b/crates/vk-graph-window/CHANGELOG.md index 5ecea9ba..a48a3980 100644 --- a/crates/vk-graph-window/CHANGELOG.md +++ b/crates/vk-graph-window/CHANGELOG.md @@ -6,6 +6,11 @@ This crate is versioned independently from `vk-graph`. ## [Unreleased] +## [0.1.2] - 2026-06-25 + +- Supports `vk-graph` `0.14.3`. +- Uses the renamed `Fence::wait` API. + ## [0.1.1] - 2026-06-18 - Supports `vk-graph` `0.14.2`. diff --git a/crates/vk-graph-window/Cargo.toml b/crates/vk-graph-window/Cargo.toml index d4cc8f1c..08cc37fe 100644 --- a/crates/vk-graph-window/Cargo.toml +++ b/crates/vk-graph-window/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vk-graph-window" -version = "0.1.1" +version = "0.1.2" authors = ["John Wells "] description = "winit window and swapchain integration for vk-graph" documentation = "https://docs.rs/vk-graph-window"