Compare commits

...

40 commits

Author SHA1 Message Date
janis ad22ff8c76
datastructures crate: intervalmap 2026-10-30 01:00:31 +01:00
janis 33b42533ef
compile bench mark for aliasing 2026-10-29 12:38:07 +01:00
janis afb322fa93
rendergraph: dynamic-rendering render passes, single-pass compiler, perf
- Add BeginRendering/EndRendering commands with imported/created attachment
  views, view plumbing, and aspect-gated depth/stencil slots
- Derive and validate render-pass spans at compile time; contract spans into
  single DAG nodes before hazard analysis; hoist cross-boundary barriers to
  pass begins with in-pass policy and created-view resolution
- Replace the four-pass ordering pipeline with a single command walk
  (build_command_orderings): span derivation, validation, lifetime-only
  injection, dependency edges, and Kahn batching in one pass; batches are
  ranges into execution_order and RenderPassSpan keeps only begin/end
- Perf: node-collapsed frontier analysis, dense command/resource indexing
  (no SipHash on hot paths), inline barrier storage, exact preallocation,
  lazy Kahn ready heap, scoped barrier fast paths
- Scope buffer and image alias barriers to the incoming resource per the
  Vulkan resource-memory-aliasing rules (availability unscoped, visibility
  scoped to the new owner)
- Add criterion compile benchmarks (two_pass/{no_pass,pass}/per_{20,50,100})
2026-10-29 00:51:52 +01:00
janis 4bf2d874b0
todo 2026-10-28 00:06:09 +01:00
janis 5e4bc0ccf0
rendergraph: correct transient-resource aliasing with pooled placement
Lifetimes derive from the real dependency ordering (retained DAG +
compile-time happens-before closure), not command-index intervals.
Aliasing is restricted to the single-queue contract; alias reuse is
synchronized with memory/image barriers anchored at first-use commands.

- renderer_device: aliasable shared allocations (CAN_ALIAS), memory-
  requirements queries incl. dedicated hints, offset-aware
  bind_buffer/image_memory2, grouped bind under one allocator lock
- renderer_resources: unallocated resource creation, requirements
  cached in Allocation::Unmanaged (OnceLock), MemoryGroup pooling with
  intersecting memoryTypeBits, bind-time validation
- renderer_rendergraph: retained dependency DAG, happens_before
  closure, batch lifetimes, Placement type, plan_placements with
  pooled aliasing (first-fit over offset-sorted ranges, pool classes
  split by bufferImageGranularity), alias-barrier construction,
  placement/bind error variants
- perf: quadratic compile/resolve paths eliminated (per-resource
  indexing, frontier caching, backward-scan prune, cached recording
  index, recorder scratch reuse); single requirements query per
  resource through plan/validate/bind
- design plan + GPU-gated validation tests
2026-10-27 01:57:27 +01:00
janis e116851b90
feat: migrate GPU memory allocator from gpu-allocator to VMA (vk-mem)
- Replace gpu-allocator with vk-mem 0.5.0 (VMA 3.3.0) in renderer_device
- Add public GpuAllocation wrapper, MemoryLocation, AllocationDesc, and
  allocate_for_buffer/image + bind_buffer/image_memory helpers
- Restrict CPU-mapped allocations to HOST_VISIBLE|HOST_COHERENT memory types
- Handle guards: raw buffer/image handles owned before allocation so failure
  paths never leak
- Use renderer_device::device::MemoryLocation across renderer, renderer_resources,
  and renderer_rendergraph; remove all gpu-allocator dependencies
- Error on dedicated allocation below Vulkan 1.1 instead of silent downgrade
- Add Send/Sync asserts, coherent-mapping and dedicated-allocation validation
  tests; update AGENTS.md docs
2026-10-26 13:44:17 +01:00
janis 7b2adb2c03
gitignore pipeline caches 2026-10-26 10:26:54 +01:00
janis 0426e4039b
testing harness 2026-10-26 02:50:28 +01:00
janis f7c67f9872
managed descriptorsets 2026-10-26 02:11:29 +01:00
janis 03240e3de2
clippy lints 2026-10-25 23:05:14 +01:00
janis d901fe250b
feat: new draw command with arguments and resource tracking 2026-10-25 23:04:27 +01:00
janis d896a207de
refactor: simplify barrier building logic and remove unused structures 2026-10-25 22:27:01 +01:00
janis ac1b6c9cc8
idk? barrier stuff 2026-10-25 21:44:21 +01:00
janis 8d8595a0e3
queue transfer in graph barrier 2026-10-25 17:33:12 +01:00
janis f84b2a1ea4
response to llm review 2026-10-25 02:28:35 +01:00
janis c0f60b890c
dont consider imported resources in the lifetimes building 2026-10-25 02:50:03 +02:00
janis 88512b37ac
change interferrence graph impl 2026-10-25 01:56:04 +02:00
janis 51a2e69f74
fewer btrees 2026-10-25 01:36:34 +02:00
janis 6a41cccf8b
extract kahn algo and build interference graph 2026-10-25 00:42:27 +02:00
janis 195cf3e08a
extract graph compilation logic 2026-10-25 00:18:52 +02:00
janis 5f7bbba49d
custom resource descriptors for graph 2026-10-25 00:13:26 +02:00
janis d367b4d05b
use renderer_resources 2026-10-24 23:05:51 +02:00
janis e7a6536df5
initial renderer_rendergraph 2026-10-24 22:23:02 +02:00
janis f9c18bcd3c
newtype queue family index 2026-10-24 21:37:07 +02:00
janis 885f3ca492
agent files 2026-10-24 21:17:06 +02:00
janis a173cf18ff
extract resources into renderer_resources 2026-10-24 21:16:54 +02:00
janis 69039ba3c1
remove old define_device_owned_handle macro_rules 2026-10-24 18:44:11 +02:00
janis dd02ea23cf
simplify device ownership logic 2026-10-24 18:11:14 +02:00
janis ce46ca1801
extract device into renderer_device
add deviceref macro
2026-10-24 17:36:43 +02:00
janis c20cee38de
asdf 2026-10-24 16:44:29 +02:00
janis 65bae2196a
add renderer proc macros crate 2026-10-24 13:15:04 +02:00
janis 08fbc0323c
warnings 2026-10-24 13:14:57 +02:00
janis e2f1ceffd8
disable unused deps 2026-10-24 02:41:42 +02:00
janis f6af46e7f4
update to latest bevy (local checkout) 2026-10-24 02:16:51 +02:00
janis 00c67d5bdf
...........kkkkkkk yeah.. 2026-10-24 01:55:27 +02:00
janis c20a4fa1a3
iiiiiii dooooooooont know....... 2026-04-17 20:35:04 +02:00
janis 30fc9f9f44
copy commands 2026-04-17 11:27:40 +02:00
janis 29dbdf9f94
idk rewrite this again 2026-04-17 07:04:52 +02:00
janis d1a0fbd592
im lost.. 2026-04-15 23:33:09 +02:00
janis d1b5e427a7
access: this is quite complicated actually.. 2026-04-15 16:30:49 +02:00
61 changed files with 23155 additions and 4655 deletions

9
.cargo/config.toml.old Normal file
View file

@ -0,0 +1,9 @@
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = [
"-Clink-arg=-fuse-ld=mold",
# Nightly
"-Zshare-generics=y",
"-Zthreads=0",
]

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target/ /target/
/Cargo.lock /Cargo.lock
**/pipeline_cache.bin

41
AGENTS.md Normal file
View file

@ -0,0 +1,41 @@
# Workspace guide
## Workspace
- Rust workspace uses the nightly toolchain (`rust-toolchain`), edition 2024, resolver 2.
- Members: `crates/renderer`, `crates/renderer/renderer_macros`, `crates/renderer_device`, `crates/renderer_resources`, `crates/window`, `crates/game`, `crates/text`, and `crates/bevy_vulkan_render`.
- Dependency direction for Vulkan rendering: `bevy_vulkan_render` → `renderer` → `renderer_resources` → `renderer_device`. `renderer_macros` supplies `DeviceRef` derives to `renderer` and is a dev-dependency for `renderer_device` tests.
- `renderer` re-exports device, instance, queue, and sync modules and many root device types. Prefer its paths for downstream rendering code; its resource-module aliases remain crate-private.
- `crates/renderer/renderer_macros` is the proc-macro crate used by `renderer` for `DeviceRef` derives.
- `crates/renderer_device` owns Vulkan instance/device setup, queues, sync primitives, and handle lifetimes.
- `crates/renderer_resources` owns public buffer, image, view, descriptor, sampler, shader-module, and pipeline resource APIs on `renderer_device`.
- `crates/renderer` owns rendering orchestration, commands, swapchains, and render graph APIs; resource module aliases are internal, not `renderer` public modules.
- `crates/bevy_vulkan_render` integrates the renderer with Bevy. `crates/window`, `crates/game`, and `crates/text` are separate workspace crates.
- The workspace root package is `vidya`; it also contains examples and depends on `bevy_vulkan_render`.
- `renderer_resources` depends on `renderer_device`, not `renderer`; do not introduce a dependency cycle.
- `renderer` uses `renderer_device` for device/instance ownership and accesses resource modules through crate-private aliases.
- `renderer_macros` is nested under the renderer package directory but is a distinct workspace package.
## Layout
- Workspace and shared dependency versions: root `Cargo.toml`.
- Per-crate manifests live beside crate source under `crates/`.
- Rendering shaders and assets are under `crates/renderer/shaders` and `assets/`.
- Root-level demos and examples are under `examples/`; add shared dependencies under `[workspace.dependencies]` where appropriate.
- The workspace uses Rust 2024 syntax; `renderer` and `renderer_device` use nightly features. `renderer_resources` has no crate-level nightly feature attributes.
## Conventions
- Keep changes local to the owning crate; preserve the device/rendering layer boundary.
- Follow existing Rust style and use the workspace dependency declarations in the root manifest when applicable.
- Keep the device/resource/rendering dependency boundaries described above; use `renderer`'s re-exported API in Bevy integration where possible.
- Do not commit changes; the user owns commits.
- Read the crate-level instructions before changing rendering/device/resource boundaries:
- [`crates/renderer_device/AGENTS.md`](crates/renderer_device/AGENTS.md)
- [`crates/renderer_resources/AGENTS.md`](crates/renderer_resources/AGENTS.md)
- [`crates/renderer/AGENTS.md`](crates/renderer/AGENTS.md)
## Verification
- `cargo check --workspace` checks all workspace crates.
- `cargo test -p renderer_device` runs the device-layer tests.
- Rendering code uses nightly features; use the pinned workspace toolchain for all Cargo commands.
- Run Cargo commands from the workspace root to use the pinned toolchain and shared workspace configuration.
- Use crate-scoped `cargo check -p <crate>` or `cargo test -p <crate>` for focused checks, then workspace check for cross-crate API changes.
- Review `git status` and `git diff` before reporting changes; preserve unrelated user modifications.

View file

@ -8,14 +8,16 @@ resolver = "2"
members = [ members = [
"crates/renderer", "crates/renderer",
"crates/renderer/renderer_macros",
"crates/window", "crates/window",
"crates/game", "crates/game",
"crates/text", "crates/text",
"crates/bevy_vulkan_render", "crates/bevy_vulkan_render", "crates/renderer_device", "crates/renderer_resources", "crates/renderer_rendergraph", "crates/datastructures",
] ]
[dependencies] [dependencies]
bevy_vulkan_render = { path = "crates/bevy_vulkan_render" } bevy_vulkan_render = { path = "crates/bevy_vulkan_render" }
bitflags.workspace = true
[dev-dependencies] [dev-dependencies]
bevy = { workspace = true } bevy = { workspace = true }
@ -27,13 +29,14 @@ debug = true
[workspace.dependencies] [workspace.dependencies]
anyhow = "1.0.89" anyhow = "1.0.89"
criterion = "0.7"
thiserror = "2.0" thiserror = "2.0"
derive_more = { version = "1.0.0", features = ["deref", "debug", "deref_mut"] } derive_more = { version = "1.0.0", features = ["deref", "debug", "deref_mut"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = {version ="0.3", features = ["env-filter"]} tracing-subscriber = {version ="0.3", features = ["env-filter"]}
glam = {version = "0.29.0", features = ["bytemuck"]} glam = {version = "0.33", features = ["bytemuck"]}
rand = "0.9" rand = "0.9"
bitflags = "2.6" bitflags = "2.6"
thread_local = "1.1.8" thread_local = "1.1.8"
@ -41,26 +44,27 @@ thread_local = "1.1.8"
ash = "0.38.0" ash = "0.38.0"
ash-window = "0.13.0" ash-window = "0.13.0"
vk-mem = "0.5.0" vk-mem = "0.5.0"
gpu-allocator = { git = "https://github.com/janis-bhm/gpu-allocator", branch = "main" }
rectangle-pack = "0.4.2" rectangle-pack = "0.4.2"
vk-sync = "0.1.6" # vk-sync = "0.1.6"
arrayvec = "0.7.6" arrayvec = "0.7.6"
tinyvec = "1.8" tinyvec = { version = "1.8", features = ["alloc"] }
indexmap = "2" indexmap = "2"
petgraph = "0.7" petgraph = "0.7"
itertools = "0.14.0" fixedbitset = "0.5"
# itertools = "0.14.0"
ahash = "0.8" ahash = "0.8"
# for non-cryptographic hashing of resources like pipelines, e.g. for caching # for non-cryptographic hashing of resources like pipelines, e.g. for caching
md-5 = "0.11.0" md-5 = "0.11.0"
parking_lot = "0.12.3" parking_lot = "0.12.3"
bytemuck = { version = "1.21.0", features = ["derive"] }
tokio = "1.42" # tokio = "1.42"
futures = "0.3" # futures = "0.3"
smol = "2.0" smol = "2.0"
rayon = "1.10" # rayon = "1.10"
winit = {version = "0.30.5", features = ["rwh_06"]} winit = {version = "0.30.5", features = ["rwh_06"]}
raw-window-handle = "0.6" raw-window-handle = "0.6"
@ -68,22 +72,21 @@ raw-window-handle = "0.6"
egui = "0.32" egui = "0.32"
egui_winit_platform = "0.27" egui_winit_platform = "0.27"
bevy = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" } bevy = { path = "/var/code/bevy-engine/bevy" }
bevy_app = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_app = { path = "/var/code/bevy-engine/bevy/crates/bevy_app" }
bevy_asset = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_asset = { path = "/var/code/bevy-engine/bevy/crates/bevy_asset" }
bevy_derive = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_derive = { path = "/var/code/bevy-engine/bevy/crates/bevy_derive" }
bevy_ecs = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy", features = ["multi_threaded", "trace"]} bevy_ecs = { path = "/var/code/bevy-engine/bevy/crates/bevy_ecs" }
bevy_hierarchy = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_log = { path = "/var/code/bevy-engine/bevy/crates/bevy_log" }
bevy_log = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" } # bevy_math = { path = "/var/code/bevy-engine/bevy/crates/bevy_math" }
bevy_math = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_platform = { path = "/var/code/bevy-engine/bevy/crates/bevy_platform" }
bevy_platform = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_reflect = { path = "/var/code/bevy-engine/bevy/crates/bevy_reflect" }
bevy_reflect = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_tasks = { path = "/var/code/bevy-engine/bevy/crates/bevy_tasks" }
bevy_tasks = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_time = { path = "/var/code/bevy-engine/bevy/crates/bevy_time" }
bevy_time = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" } # bevy_transform = { path = "/var/code/bevy-engine/bevy/crates/bevy_transform" }
bevy_transform = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_utils = { path = "/var/code/bevy-engine/bevy/crates/bevy_utils" }
bevy_utils = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" } bevy_window = { path = "/var/code/bevy-engine/bevy/crates/bevy_window" }
bevy_window = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" } # bevy_winit = { path = "/var/code/bevy-engine/bevy/crates/bevy_winit" }
bevy_winit = { version = "0.19.0-dev", git = "https://github.com/bevyengine/bevy" }
[[example]] [[example]]
name = "bevy_integration" name = "bevy_integration"

View file

@ -2,10 +2,9 @@ use crate::extract_plugin::MainWorld;
use bevy_ecs::{ use bevy_ecs::{
change_detection::Tick, change_detection::Tick,
prelude::*, prelude::*,
query::FilteredAccessSet,
system::{ system::{
ReadOnlySystemParam, SystemMeta, SystemParam, SystemParamItem, SystemParamValidationError, ReadOnlySystemParam, SystemAccess, SystemMeta, SystemParam, SystemParamItem,
SystemState, SystemParamValidationError, SystemState,
}, },
world::unsafe_world_cell::UnsafeWorldCell, world::unsafe_world_cell::UnsafeWorldCell,
}; };
@ -83,15 +82,9 @@ where
fn init_access( fn init_access(
state: &Self::State, state: &Self::State,
system_meta: &mut SystemMeta, system_meta: &mut SystemMeta,
component_access_set: &mut FilteredAccessSet, system_access: &mut SystemAccess,
world: &mut World, ) -> Result<(), bevy_ecs::system::SystemParamAccessConflict> {
) { Res::<MainWorld>::init_access(&state.main_world_state, system_meta, system_access)
Res::<MainWorld>::init_access(
&state.main_world_state,
system_meta,
component_access_set,
world,
);
} }
#[inline] #[inline]

View file

@ -89,6 +89,7 @@ fn mock_render_system() {
impl Plugin for RenderPlugin { impl Plugin for RenderPlugin {
fn build(&self, app: &mut bevy_app::App) { fn build(&self, app: &mut bevy_app::App) {
app.add_plugins(ExtractPlugin::default()); app.add_plugins(ExtractPlugin::default());
app.insert_non_send(window::Surfaces::default());
let asset_server = app.world().resource::<AssetServer>().clone(); let asset_server = app.world().resource::<AssetServer>().clone();
if let Some(render_app) = app.get_sub_app_mut(RenderApp) { if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
@ -100,18 +101,6 @@ impl Plugin for RenderPlugin {
} }
} }
fn init_vulkan(app: &mut App) -> Option<()> {
let device = renderer::device::Device::new_from_default_desc(None, &[]).ok()?;
let device = RenderDevice { device };
app.insert_resource(device.clone());
let render_app = app.sub_app_mut(RenderApp);
render_app.insert_resource(device.clone());
Some(())
}
/// A "scratch" world used to avoid allocating new worlds every frame when /// A "scratch" world used to avoid allocating new worlds every frame when
/// swapping out the [`MainWorld`] for [`ExtractSchedule`]. /// swapping out the [`MainWorld`] for [`ExtractSchedule`].
#[derive(Resource, Default)] #[derive(Resource, Default)]

View file

@ -93,12 +93,12 @@ impl Plugin for SyncWorldPlugin {
fn build(&self, app: &mut bevy_app::App) { fn build(&self, app: &mut bevy_app::App) {
app.init_resource::<PendingSyncEntity>(); app.init_resource::<PendingSyncEntity>();
app.add_observer( app.add_observer(
|add: On<Add, SyncToRenderWorld>, mut pending: ResMut<PendingSyncEntity>| { |add: On<Add<SyncToRenderWorld>>, mut pending: ResMut<PendingSyncEntity>| {
pending.push(EntityRecord::Added(add.entity)); pending.push(EntityRecord::Added(add.entity));
}, },
); );
app.add_observer( app.add_observer(
|remove: On<Remove, SyncToRenderWorld>, |remove: On<Remove<SyncToRenderWorld>>,
mut pending: ResMut<PendingSyncEntity>, mut pending: ResMut<PendingSyncEntity>,
query: Query<&RenderEntity>| { query: Query<&RenderEntity>| {
if let Ok(e) = query.get(remove.entity) { if let Ok(e) = query.get(remove.entity) {
@ -344,6 +344,13 @@ mod render_entities_world_query_impls {
<&RenderEntity as WorldQuery>::update_component_access(&component_id, access); <&RenderEntity as WorldQuery>::update_component_access(&component_id, access);
} }
fn init_nested_access(
state: &Self::State,
component_access_set: &mut bevy_ecs::query::FilteredAccessSet,
) -> Result<(), bevy_ecs::query::FilteredAccessSet> {
<&RenderEntity as WorldQuery>::init_nested_access(state, component_access_set)
}
fn init_state(world: &mut World) -> ComponentId { fn init_state(world: &mut World) -> ComponentId {
<&RenderEntity as WorldQuery>::init_state(world) <&RenderEntity as WorldQuery>::init_state(world)
} }
@ -358,6 +365,10 @@ mod render_entities_world_query_impls {
) -> bool { ) -> bool {
<&RenderEntity as WorldQuery>::matches_component_set(&state, set_contains_id) <&RenderEntity as WorldQuery>::matches_component_set(&state, set_contains_id)
} }
fn update_archetypes(state: &mut Self::State, world: UnsafeWorldCell) {
<&RenderEntity as WorldQuery>::update_archetypes(state, world);
}
} }
// SAFETY: Component access of Self::ReadOnly is a subset of Self. // SAFETY: Component access of Self::ReadOnly is a subset of Self.
@ -464,6 +475,13 @@ mod render_entities_world_query_impls {
<&MainEntity as WorldQuery>::update_component_access(&component_id, access); <&MainEntity as WorldQuery>::update_component_access(&component_id, access);
} }
fn init_nested_access(
state: &Self::State,
component_access_set: &mut bevy_ecs::query::FilteredAccessSet,
) -> Result<(), bevy_ecs::query::FilteredAccessSet> {
<&MainEntity as WorldQuery>::init_nested_access(state, component_access_set)
}
fn init_state(world: &mut World) -> ComponentId { fn init_state(world: &mut World) -> ComponentId {
<&MainEntity as WorldQuery>::init_state(world) <&MainEntity as WorldQuery>::init_state(world)
} }
@ -478,6 +496,10 @@ mod render_entities_world_query_impls {
) -> bool { ) -> bool {
<&MainEntity as WorldQuery>::matches_component_set(&state, set_contains_id) <&MainEntity as WorldQuery>::matches_component_set(&state, set_contains_id)
} }
fn update_archetypes(state: &mut Self::State, world: UnsafeWorldCell) {
<&MainEntity as WorldQuery>::update_archetypes(state, world);
}
} }
// SAFETY: Component access of Self::ReadOnly is a subset of Self. // SAFETY: Component access of Self::ReadOnly is a subset of Self.
@ -558,12 +580,12 @@ mod tests {
main_world.init_resource::<PendingSyncEntity>(); main_world.init_resource::<PendingSyncEntity>();
main_world.add_observer( main_world.add_observer(
|add: On<Add, SyncToRenderWorld>, mut pending: ResMut<PendingSyncEntity>| { |add: On<Add<SyncToRenderWorld>>, mut pending: ResMut<PendingSyncEntity>| {
pending.push(EntityRecord::Added(add.entity)); pending.push(EntityRecord::Added(add.entity));
}, },
); );
main_world.add_observer( main_world.add_observer(
|remove: On<Remove, SyncToRenderWorld>, |remove: On<Remove<SyncToRenderWorld>>,
mut pending: ResMut<PendingSyncEntity>, mut pending: ResMut<PendingSyncEntity>,
query: Query<&RenderEntity>| { query: Query<&RenderEntity>| {
if let Ok(e) = query.get(remove.entity) { if let Ok(e) = query.get(remove.entity) {

View file

@ -5,9 +5,9 @@ use std::{
use bevy_ecs::{entity::EntityHashMap, prelude::*}; use bevy_ecs::{entity::EntityHashMap, prelude::*};
use bevy_window::{PrimaryWindow, RawHandleWrapper, Window, WindowClosing}; use bevy_window::{PrimaryWindow, RawHandleWrapper, Window, WindowClosing};
use renderer::{ash::vk, swapchain::WindowSurface}; use renderer::{ash::vk, swapchain::Surface};
use crate::{RenderDevice, extract_param::Extract}; use crate::extract_param::Extract;
pub struct ExtractedWindow { pub struct ExtractedWindow {
pub entity: Entity, pub entity: Entity,
@ -39,7 +39,7 @@ impl DerefMut for ExtractedWindows {
} }
} }
#[derive(Default, Resource)] #[derive(Default)]
pub struct Surfaces { pub struct Surfaces {
surfaces: EntityHashMap<SurfaceData>, surfaces: EntityHashMap<SurfaceData>,
configured: HashSet<Entity>, configured: HashSet<Entity>,
@ -57,11 +57,12 @@ impl Surfaces {
} }
pub struct SurfaceData { pub struct SurfaceData {
pub surface: WindowSurface, renderer: renderer::Renderer2,
pub surface: Surface,
} }
impl Deref for SurfaceData { impl Deref for SurfaceData {
type Target = WindowSurface; type Target = Surface;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.surface &self.surface
@ -74,7 +75,7 @@ pub(crate) fn extract_windows(
mut closing: Extract<MessageReader<WindowClosing>>, mut closing: Extract<MessageReader<WindowClosing>>,
windows: Extract<Query<(Entity, &RawHandleWrapper, &Window, Option<&PrimaryWindow>)>>, windows: Extract<Query<(Entity, &RawHandleWrapper, &Window, Option<&PrimaryWindow>)>>,
mut removed: Extract<RemovedComponents<RawHandleWrapper>>, mut removed: Extract<RemovedComponents<RawHandleWrapper>>,
mut surfaces: ResMut<Surfaces>, mut surfaces: NonSendMut<Surfaces>,
) { ) {
for (entity, handle, window, primary) in windows.iter() { for (entity, handle, window, primary) in windows.iter() {
if primary.is_some() { if primary.is_some() {
@ -116,7 +117,7 @@ pub(crate) fn extract_windows(
pub(crate) fn need_surface_configuration( pub(crate) fn need_surface_configuration(
windows: Res<ExtractedWindows>, windows: Res<ExtractedWindows>,
surfaces: Res<Surfaces>, surfaces: NonSend<Surfaces>,
) -> bool { ) -> bool {
windows windows
.windows .windows
@ -126,37 +127,43 @@ pub(crate) fn need_surface_configuration(
pub(crate) fn configure_surfaces( pub(crate) fn configure_surfaces(
windows: Res<ExtractedWindows>, windows: Res<ExtractedWindows>,
mut surfaces: ResMut<Surfaces>, mut surfaces: NonSendMut<Surfaces>,
device: Res<RenderDevice>,
) { ) {
for window in windows.windows.values() { for window in windows.windows.values() {
let surface = surfaces.surfaces.entry(window.entity).or_insert_with(|| { let surface_data = surfaces.surfaces.entry(window.entity).or_insert_with(|| {
let (display_handle, window_handle) = ( let (display_handle, window_handle) = (
window.handle.get_display_handle(), window.handle.get_display_handle(),
window.handle.get_window_handle(), window.handle.get_window_handle(),
); );
let surface = WindowSurface::new( let renderer =
(**device).clone(), renderer::Renderer2::new(display_handle).expect("Failed to create Vulkan renderer");
let surface = renderer
.create_surface(
window_handle,
vk::Extent2D { vk::Extent2D {
width: window.physical_width, width: window.physical_width,
height: window.physical_height, height: window.physical_height,
}, },
window_handle,
display_handle,
) )
.expect("Failed to create window surface"); .expect("Failed to create window surface");
SurfaceData { surface } SurfaceData { renderer, surface }
}); });
if window.size_changed { if window.size_changed {
surface surface_data
.surface .surface
.recreate_with(Some(vk::Extent2D { .configure(
surface_data.renderer.device(),
renderer::swapchain::SwapchainConfiguration {
extent: vk::Extent2D {
width: window.physical_width, width: window.physical_width,
height: window.physical_height, height: window.physical_height,
})) },
..Default::default()
},
)
.expect("Failed to recreate surface"); .expect("Failed to recreate surface");
} }
surfaces.configured.insert(window.entity); surfaces.configured.insert(window.entity);

View file

@ -0,0 +1,6 @@
[package]
name = "datastructures"
version = "0.1.0"
edition = "2024"
[dependencies]

View file

@ -0,0 +1,471 @@
//! A map that stores values over disjoint, half-open intervals.
use std::collections::BTreeMap;
/// Operations applied to the covered and uncovered parts of a range.
pub trait IntervalUpdateOps<K, V> {
/// Updates the value of a subrange that was already present in the map.
fn update(&mut self, range: core::range::Range<K>, value: &mut V);
/// Creates a value for a subrange that was not previously present.
fn infill(&mut self, range: core::range::Range<K>) -> V;
}
impl<K, V, T> IntervalUpdateOps<K, V> for &mut T
where
T: IntervalUpdateOps<K, V> + ?Sized,
{
fn update(&mut self, range: core::range::Range<K>, value: &mut V) {
(**self).update(range, value);
}
fn infill(&mut self, range: core::range::Range<K>) -> V {
(**self).infill(range)
}
}
/// An interval and its associated value.
#[derive(Debug, Clone)]
struct Entry<K, V> {
/// The covered interval; its start is always the BTreeMap key.
range: core::range::Range<K>,
value: V,
}
/// A map from disjoint, half-open ranges of ordered keys to values.
///
/// Stored ranges are pairwise disjoint and sorted by their start key.
#[derive(Debug, Clone)]
pub struct DisjointIntervalMap<K, V> {
map: BTreeMap<K, Entry<K, V>>,
}
impl<K, V> DisjointIntervalMap<K, V> {
/// Creates an empty interval map.
pub const fn new() -> Self {
Self {
map: BTreeMap::new(),
}
}
/// Applies operations to every existing and missing subrange within `range`.
///
/// On return, every point in the non-empty range is covered by exactly one
/// map entry. Existing values are updated, and missing intervals are
/// created through [`IntervalUpdateOps::infill`].
pub fn split_apply(
&mut self,
range: core::range::Range<K>,
mut ops: impl IntervalUpdateOps<K, V>,
) where
K: Ord + Copy,
V: Clone,
{
if range.is_empty() {
return;
}
self.split(range.start);
self.split(range.end);
let mut cursor = range.start;
let mut gaps = Vec::new();
for entry in self
.map
.range_mut(range.start..range.end)
.map(|(_, entry)| entry)
{
if cursor < entry.range.start {
gaps.push(core::range::Range {
start: cursor,
end: entry.range.start,
});
}
let entry_range = entry.range;
ops.update(entry_range, &mut entry.value);
cursor = entry_range.end;
}
if cursor < range.end {
gaps.push(core::range::Range {
start: cursor,
end: range.end,
});
}
for gap in gaps {
let value = ops.infill(gap);
self.map.insert(gap.start, Entry { range: gap, value });
}
}
/// Inserts `range` unless it is empty or overlaps an existing interval.
#[allow(clippy::result_unit_err)]
pub fn try_insert(&mut self, range: core::range::Range<K>, value: V) -> Result<(), ()>
where
K: Ord + Copy,
{
if range.is_empty() {
return Err(());
}
if self
.map
.range(..range.end)
.next_back()
.is_some_and(|(_, entry)| entry.range.end > range.start)
{
return Err(());
}
self.map.insert(range.start, Entry { range, value });
Ok(())
}
/// Removes all mapped portions covered by `range`.
pub fn remove(&mut self, range: core::range::Range<K>)
where
K: Ord + Copy,
V: Clone,
{
if range.is_empty() {
return;
}
self.split(range.start);
self.split(range.end);
let keys: Vec<K> = self
.map
.range(range.start..range.end)
.map(|(key, _)| *key)
.collect();
for key in keys {
self.map.remove(&key);
}
}
/// Splits the interval containing `pos` into two intervals at `pos`.
///
/// Does nothing if no interval contains `pos` or an interval already
/// starts there.
pub fn split(&mut self, pos: K)
where
K: Ord + Copy,
V: Clone,
{
let Some((key, interval)) = self
.map
.range_mut(..=pos)
.next_back()
.map(|(key, entry)| (*key, entry.range))
else {
return;
};
if key == pos || pos >= interval.end {
return;
}
let entry = self.map.remove(&key).expect("candidate entry must exist");
let right_value = entry.value.clone();
let left_range = core::range::Range {
start: key,
end: pos,
};
let right_range = core::range::Range {
start: pos,
end: entry.range.end,
};
self.map.insert(
key,
Entry {
range: left_range,
value: entry.value,
},
);
self.map.insert(
pos,
Entry {
range: right_range,
value: right_value,
},
);
}
/// Returns the value of the interval containing `pos`, if any.
pub fn get(&self, pos: K) -> Option<&V>
where
K: Ord,
{
let (_, entry) = self
.map
.range((std::ops::Bound::Unbounded, std::ops::Bound::Included(&pos)))
.next_back()?;
(pos < entry.range.end).then_some(&entry.value)
}
/// Returns a mutable reference to the value of the interval containing `pos`.
pub fn get_mut(&mut self, pos: K) -> Option<&mut V>
where
K: Ord,
{
let (_, entry) = self
.map
.range_mut((std::ops::Bound::Unbounded, std::ops::Bound::Included(&pos)))
.next_back()?;
(pos < entry.range.end).then_some(&mut entry.value)
}
/// Iterates over intervals and their values in start-key order.
pub fn iter(&self) -> impl Iterator<Item = (&core::range::Range<K>, &V)> {
self.map.values().map(|entry| (&entry.range, &entry.value))
}
/// Iterates over values in start-key order.
pub fn values(&self) -> impl Iterator<Item = &V> {
self.map.values().map(|entry| &entry.value)
}
/// Iterates over intervals in start-key order.
pub fn keys(&self) -> impl Iterator<Item = &core::range::Range<K>> {
self.map.values().map(|entry| &entry.range)
}
/// Returns the number of stored intervals.
pub fn len(&self) -> usize {
self.map.len()
}
/// Returns whether the map contains no intervals.
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
/// Removes all intervals from the map.
pub fn clear(&mut self) {
self.map.clear();
}
}
impl<K, V> Default for DisjointIntervalMap<K, V> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::{DisjointIntervalMap, IntervalUpdateOps};
fn r(start: i32, end: i32) -> core::range::Range<i32> {
core::range::Range::from(start..end)
}
#[derive(Default)]
struct CountingOps {
updates: Vec<(i32, i32)>,
infills: Vec<(i32, i32)>,
}
impl IntervalUpdateOps<i32, i32> for CountingOps {
fn update(&mut self, range: core::range::Range<i32>, value: &mut i32) {
*value += 1;
self.updates.push((range.start, range.end));
}
fn infill(&mut self, range: core::range::Range<i32>) -> i32 {
self.infills.push((range.start, range.end));
100 + range.start
}
}
fn intervals(map: &DisjointIntervalMap<i32, i32>) -> Vec<(i32, i32, i32)> {
map.iter()
.map(|(range, value)| (range.start, range.end, *value))
.collect()
}
#[test]
fn try_insert_overlap_adjacency_and_lookup() {
let mut map = DisjointIntervalMap::new();
assert!(map.try_insert(r(5, 10), 1).is_ok());
assert!(map.try_insert(r(0, 5), 2).is_ok());
assert!(map.try_insert(r(10, 15), 3).is_ok());
for overlap in [r(4, 6), r(9, 12), r(6, 8), r(4, 11)] {
assert_eq!(map.try_insert(overlap, 9), Err(()));
}
assert_eq!(map.try_insert(r(2, 2), 9), Err(()));
assert_eq!(map.get(0), Some(&2));
assert_eq!(map.get(5), Some(&1));
assert_eq!(map.get(14), Some(&3));
assert_eq!(map.get(15), None);
assert_eq!(map.get(-1), None);
*map.get_mut(5).unwrap() = 7;
assert_eq!(map.get(5), Some(&7));
assert!(map.get_mut(15).is_none());
}
#[test]
fn split_boundaries_and_interior() {
let mut map = DisjointIntervalMap::new();
assert!(map.try_insert(r(0, 10), 4).is_ok());
map.split(5);
assert_eq!(intervals(&map), vec![(0, 5, 4), (5, 10, 4)]);
for pos in [0, 10, -1, 11] {
let before = intervals(&map);
map.split(pos);
assert_eq!(intervals(&map), before);
}
let mut empty = DisjointIntervalMap::<i32, i32>::new();
empty.split(1);
assert!(empty.is_empty());
}
#[test]
fn remove_exact_partial_spanning_and_noop_ranges() {
let mut exact = DisjointIntervalMap::new();
exact.try_insert(r(0, 5), 1).unwrap();
exact.remove(r(0, 5));
assert!(exact.is_empty());
let mut partial = DisjointIntervalMap::new();
partial.try_insert(r(0, 5), 1).unwrap();
partial.try_insert(r(5, 10), 2).unwrap();
partial.remove(r(3, 7));
assert_eq!(intervals(&partial), vec![(0, 3, 1), (7, 10, 2)]);
partial.remove(r(8, 12));
assert_eq!(intervals(&partial), vec![(0, 3, 1), (7, 8, 2)]);
let mut spanning = DisjointIntervalMap::new();
for start in [0, 5, 10] {
spanning.try_insert(r(start, start + 5), start).unwrap();
}
spanning.remove(r(2, 13));
assert_eq!(intervals(&spanning), vec![(0, 2, 0), (13, 15, 10)]);
let before = intervals(&spanning);
spanning.remove(r(20, 25));
spanning.remove(r(4, 4));
assert_eq!(intervals(&spanning), before);
}
#[test]
fn split_apply_on_empty_and_fully_covered_maps() {
let mut empty = DisjointIntervalMap::new();
let mut ops = CountingOps::default();
empty.split_apply(r(2, 6), &mut ops);
assert_eq!(intervals(&empty), vec![(2, 6, 102)]);
assert!(ops.updates.is_empty());
assert_eq!(ops.infills, vec![(2, 6)]);
let mut covered = DisjointIntervalMap::new();
covered.try_insert(r(0, 10), 5).unwrap();
let mut ops = CountingOps::default();
covered.split_apply(r(0, 10), &mut ops);
assert_eq!(intervals(&covered), vec![(0, 10, 6)]);
assert_eq!(ops.updates, vec![(0, 10)]);
assert!(ops.infills.is_empty());
}
#[test]
fn split_apply_mixed_coverage_splits_and_fills_gaps() {
let mut map = DisjointIntervalMap::new();
map.try_insert(r(0, 4), 1).unwrap();
map.try_insert(r(6, 10), 2).unwrap();
let mut ops = CountingOps::default();
map.split_apply(r(2, 8), &mut ops);
assert_eq!(
intervals(&map),
vec![(0, 2, 1), (2, 4, 2), (4, 6, 104), (6, 8, 3), (8, 10, 2)]
);
assert_eq!(ops.updates, vec![(2, 4), (6, 8)]);
assert_eq!(ops.infills, vec![(4, 6)]);
}
#[test]
fn split_apply_inside_one_entry_and_empty_range() {
let mut map = DisjointIntervalMap::new();
map.try_insert(r(0, 10), 7).unwrap();
let mut ops = CountingOps::default();
map.split_apply(r(3, 5), &mut ops);
assert_eq!(intervals(&map), vec![(0, 3, 7), (3, 5, 8), (5, 10, 7)]);
assert_eq!(ops.updates, vec![(3, 5)]);
assert!(ops.infills.is_empty());
let before = intervals(&map);
let prior_ops = ops.updates.clone();
let prior_infills = ops.infills.clone();
map.split_apply(r(4, 4), &mut ops);
assert_eq!(intervals(&map), before);
assert_eq!(ops.updates, prior_ops);
assert_eq!(ops.infills, prior_infills);
}
#[test]
fn split_apply_mutable_ops_accumulate_state() {
let mut map = DisjointIntervalMap::new();
let mut ops = CountingOps::default();
map.split_apply(r(0, 2), &mut ops);
map.split_apply(r(4, 6), &mut ops);
assert_eq!(ops.infills, vec![(0, 2), (4, 6)]);
assert_eq!(map.len(), 2);
}
#[test]
fn iteration_and_collection_basics() {
let mut map = DisjointIntervalMap::new();
assert!(map.is_empty());
map.try_insert(r(5, 8), 2).unwrap();
map.try_insert(r(0, 3), 1).unwrap();
assert_eq!(map.len(), 2);
assert!(!map.is_empty());
assert_eq!(
map.iter()
.map(|(range, value)| ((range.start, range.end), *value))
.collect::<Vec<_>>(),
vec![((0, 3), 1), ((5, 8), 2)]
);
assert_eq!(
map.keys()
.map(|range| (range.start, range.end))
.collect::<Vec<_>>(),
vec![(0, 3), (5, 8)]
);
assert_eq!(map.values().copied().collect::<Vec<_>>(), vec![1, 2]);
map.clear();
assert!(map.is_empty());
assert_eq!(map.len(), 0);
}
#[test]
fn operations_preserve_disjoint_nonempty_sorted_invariant() {
let mut map = DisjointIntervalMap::new();
map.try_insert(r(0, 4), 1).unwrap();
map.try_insert(r(6, 10), 2).unwrap();
map.split(2);
map.remove(r(3, 7));
let mut ops = CountingOps::default();
map.split_apply(r(-2, 12), &mut ops);
map.try_insert(r(20, 24), 3).unwrap();
map.split(22);
map.remove(r(1, 2));
let ranges = map.keys().collect::<Vec<_>>();
for range in &ranges {
assert!(!range.is_empty());
}
for pair in ranges.windows(2) {
assert!(pair[0].start < pair[1].start);
assert!(pair[0].end <= pair[1].start);
}
}
}

130
crates/renderer/AGENTS.md Normal file
View file

@ -0,0 +1,130 @@
# renderer
## Purpose
`renderer` builds rendering orchestration, command submission, swapchains, and render-graph APIs on `renderer_resources` and the lower-level `renderer_device` layer. It re-exports selected `renderer_device` APIs and uses crate-private aliases to the resource crate. Follow [`../renderer_device/AGENTS.md`](../renderer_device/AGENTS.md) for device ownership internals and [`../renderer_resources/AGENTS.md`](../renderer_resources/AGENTS.md) for resource construction and ownership.
## Module map
- `lib.rs`: public root API, `Renderer2`, sampler cache, egui state, texture manager, and `renderer_device` re-exports.
- `memory.rs` (not declared by `lib.rs`): empty; not part of the compiled crate module tree.
- `renderer_resources::{buffers, images, pipeline}`: public resource-crate modules aliased through `pub(crate) use` in this crate root. These aliases preserve internal `crate::buffers::X`, `crate::images::X`, and `crate::pipeline::X` paths without exposing public renderer modules.
- `commands.rs` (public): command pools/buffers, command recording helpers, queue submission, and command futures.
- `swapchain.rs` (public): `Surface`, swapchain configuration, acquired images/frames, and presentation.
- `render_graph/mod.rs` (public): re-exports the legacy graph API from `legacy.rs`; sibling `commands.rs`, `graph_builder.rs`, `recorder.rs`, and `resources.rs` are private modules.
- `render_graph/legacy.rs`: public resource/pass descriptions, graph construction/resolution, resource barriers, and `RenderContext`.
- `render_graph/commands.rs`: private typed render-graph commands and resource-access declarations; not wired into the legacy graph.
- `render_graph/graph_builder.rs`: private newer graph-builder scaffolding and API example.
- `render_graph/recorder.rs`: private typed command-list and side-effect tracking scaffolding.
- `render_graph/resources.rs`: graph resource IDs, buffer/texture ranges and access scopes; private.
- `rendering/mod.rs` (public): `Wireframe` helper and re-exports `egui_pass` / `egui_pre_pass`.
- `egui.rs` is included as private `egui_pass`: egui texture upload/pre-pass and drawing integration.
- `egui_pass` is a private module, but `rendering` publicly re-exports its `egui_pass` and `egui_pre_pass` functions.
- `text.rs` (private): font storage, glyph rasterization, and CPU-side glyph-atlas data; largely internal/dead-code-allowed.
- `util.rs` (public): renderer helpers and stable re-exports of `eq_f32`, `ord_f32`, `hash_f32`, and `F32` from `renderer_resources::util`; `weak_vec` moved there and is not re-exported from renderer.
- `texture` (private inline module in `lib.rs`): `TextureManager` and `TextureId` around shared images.
## Key types and API surface
- Public modules are `commands`, `render_graph`, `rendering`, `swapchain`, and `util`; `text`, `egui_pass`, and `texture` are private. `buffers`, `images`, and `pipeline` are crate-private aliases to public `renderer_resources` modules, not public paths through `renderer`.
- `lib.rs` re-exports `ash` and the principal `renderer_device` APIs at crate root, including `device`, `instance`, `queue`, `sync`, `Device`, `DeviceOwned`, `DeviceRef`, `Instance`, `Error`, and `Result`.
- Root entry point: `Renderer2` owns a `Device`, `SamplerCache`, `TextureManager`, and display handle. Its API creates surfaces and builds/submits a `RenderGraph` in `draw_graph`.
- Root helper types include `EguiState`, `EguiTextureInfo`, and `SamplerCache`. `TextureManager` and `TextureId` are used by these internals but are not re-exported at crate root.
- `SamplerCache` caches `renderer_resources::pipeline::Sampler` objects by `SamplerDesc`; `EguiState` keeps texture mappings and descriptor/pipeline resources for egui.
- `commands::CommandPools`, `CommandPool`, `CommandBuffer`, and `CommandBufferFuture` provide per-thread command-pool allocation and submission. `SingleUseCommandPool`/`SingleUseCommand` support one-shot operations.
- `commands::CommandBufferExt` adds recording helpers for barriers, copies, rendering, and pipeline state. `CommandList<T: HasQueue>` submits command buffers that share a queue.
- `swapchain::Surface` owns the surface and current configured swapchain; `SwapchainConfiguration` describes image format, extent, present mode, usage, and image count. Acquired image values must be presented.
- Public `render_graph` items come from `legacy.rs`: `RenderGraph`, `GraphResourceId`, `GraphResourceDesc`, `PassDesc`, `RenderContext`, `Access`, `LoadOp`, and `StoreOp` provide pass/resource declaration and resolution.
- `RenderGraph::add_resource` / `import_image` / `import_buffer` / `import_framebuffer` register graph resources; `add_pass` records read/write declarations and a callback; `resolve` creates internal resources and returns a command list with graph transitions.
- `rendering::Wireframe` is an example higher-level renderer; egui pass functions are re-exported from `rendering` rather than the private `egui_pass` module.
- Resource types are defined in `renderer_resources`: `Buffer`/`BufferDesc`; `Image`/`ImageDesc`, `ImageView`/`ImageViewDesc`, `MipRange`, `Extent`, and `Offset`; and pipeline, descriptor, shader, and sampler types.
- Buffer/image descriptors carry Vulkan usage, `renderer_device::device::MemoryLocation`, queue-sharing flags, and allocation strategy; `Buffer::map`/`map_mut` expose mapped memory for VMA-mapped allocations (CPU-mapped locations require host-visible, host-coherent memory).
- The egui integration uploads texture deltas to GPU images and records passes through the legacy `RenderGraph`; shader binaries remain under `crates/renderer/shaders`, and renderer's hardcoded shader paths are unchanged.
- `ImageDesc` includes image shape, format, layout, usage, queue family selection, and allocation options. The resource crate's `Image::new_with_allocation` checks allocation size and memory-type compatibility before binding.
- `ImageViewDesc` selects view type, format, aspects, mip range, and array layers; the resource crate's `Image::create_view` normalizes `REMAINING_MIP_LEVELS` / `REMAINING_ARRAY_LAYERS` and checks image/view compatibility.
- `BufferDesc` and `ImageDesc` use `renderer_device::device::QueueFlags` to select concurrent sharing families. Empty/single-family selections use exclusive sharing.
- `Pipeline` retains its `PipelineLayout` via `Arc`; `DescriptorPool` serializes descriptor allocation/reset using a mutex. Pipeline creation uses the device pipeline cache.
- `Surface::configure` skips recreating an equivalent swapchain for the same device; configuration validation falls back for unsupported present mode/format and rejects zero or oversized extents.
- `SwapchainImage` dereferences to an `ImageView`; `SwapchainFrame` exposes the swapchain, image, format, view, and acquire/release semaphores used by frame submission.
- `render_graph::legacy::RenderGraph` stores resource descriptions until `resolve`; framebuffer imports are marked as present outputs.
- `render_graph::legacy::PassDesc` lists per-pass `reads`, `writes`, and an optional `RecordFn`; the graph resolver builds dependencies and transitions between `Access` states.
- `render_graph::commands::Command` distinguishes side-effect declaration from GPU command application, with commands marking resource ranges/stages/accesses for barriers.
- `render_graph::recorder::CommandList` type-erases recorded commands into bytes and tracks per-resource side effects.
- Internal text support defines `FontId` and a private font atlas store; it rasterizes glyphs with `cosmic-text` into CPU image data but is not a public renderer API.
- `util::DropGuard`, `MutexExt`, `FormatExt`, `Rgba`/`Rgba8`, and `def_monotonic_id!` support lifetime callbacks, lock closures, format conversion, color values, and IDs.
- `render_graph::legacy::RenderContext` exposes the selected device, one-shot command buffer, graph resources, and optional framebuffer to each pass record callback.
- Graph `Access` includes stage, access mask, and image layout. Imported resources require callers to describe their initial access; framebuffer import marks presentation as an output.
- `DeviceObject<vk::Buffer>`, `DeviceObject<vk::Image>`, `DeviceObject<vk::ImageView>`, and `DeviceObject<vk::SwapchainKHR>` wrap device-owned Vulkan handles. Resource wrappers retain allocations/devices as needed; see the device-layer guide for lifecycle rules.
- `renderer::device::Allocation` (from `renderer_device`) distinguishes owned, shared, and unmanaged GPU allocations; the resource image implementation uses it when constructing allocated images.
- `GraphResource` can hold imported resources, owned buffers/images, framebuffers, or resource descriptions awaiting resolution. `RenderGraph::resolve` creates described resources and records `SingleUseCommand`s for its passes.
- Descriptor, pipeline, shader, and sampler abstractions are implemented in `renderer_resources::pipeline`. Root `SamplerCache` uses them through the private alias, not a public renderer pipeline module.
## Invariants and gotchas
- Device ownership is delegated to `renderer_device`: wrappers here strongly retain `Device` through its `DeviceObject` or explicit fields. Do not place a device-owning wrapper back inside `DeviceInner`.
- `Image::from_swapchain_raw(image, device, format, extent, usage)` wraps a swapchain-owned raw image without taking image ownership. `Swapchain::acquire_image` passes the swapchain device, configured format/usage, and configured extent with depth 1. Its descriptor remains TYPE_2D, one mip/layer, TYPE_1 samples, OPTIMAL tiling, PRESENT queue flags, UNDEFINED layout, and GpuOnly memory. Do not destroy that image as an independently allocated image.
- `ImageView` retains an `Arc<Image>` so its source image outlives the Vulkan view. `Image::create_view` in `renderer_resources` returns `ManuallyDrop<ImageView>`; renderer call sites transfer it deliberately (the swapchain path uses `ManuallyDrop::into_inner`).
- For allocated images, `Allocation::Owned` frees VMA allocation memory with its `DeviceObject`; `Allocation::Shared` permits shared ownership and only exposes mutable access when uniquely held; `Unmanaged` carries no allocation.
- `Swapchain` owns raw swapchain images and manually created acquire/release semaphores. Its drop path waits idle and destroys those semaphores; the `DeviceObject` destroys the swapchain handle.
- `SwapchainImage` / `SwapchainFrame` are `#[must_use]` acquired-frame values. Presentation consumes the frame; preserve its `Arc<Swapchain>` so image ownership stays valid until presentation.
- `Surface` uses a `DropGuard` for surface destruction and keeps its `Instance`; field order drops the swapchain lock first, then the guard, then the instance.
- `SingleUseCommand` and `CommandBuffer` use `DeviceObject<vk::CommandBuffer>`, whose device-layer destroy implementation is intentionally a no-op. Their own drops free command buffers through their pools. `SingleUseCommandPool` owns a raw mutex-protected pool and destroys it itself.
- `CommandList::submit` requires all commands to come from the same queue; `CommandBuffer::submit` checks queue-family compatibility.
- `CommandPool` is explicitly `!Sync`; `CommandBuffer` is explicitly `!Send` and `!Sync`. `SingleUseCommand` is explicitly `!Sync`. Preserve these thread-affinity constraints when changing allocation/submission wrappers.
- `CommandPool::alloc` currently calls `free_command_buffers()` before allocating, so its free-list is not an enduring reusable buffer cache. `CommandBufferFuture` frees the submitted buffer when the future is dropped.
- `SingleUseCommand` begins recording with `ONE_TIME_SUBMIT`; callers must end/submit according to its state and must not access it concurrently (it is `!Sync`).
- `CommandPools` is a thread-local registry sharded by thread and queue family; its unsafe `Send` implementation documents an assumption that shards are unused during drop/movement.
- `Surface::new_from_raw_window_handle` is unsafe: the caller keeps display/window handles valid while surface use continues.
- `Swapchain::release_resources` is an unsafe once-only pre-destruction operation; `Drop` invokes it and currently ignores the result of its internal idle wait.
- `Swapchain` uses separate synchronization semaphore lifetimes: acquire semaphores are sized for frames in flight, release semaphores are per swapchain image. Preserve their indexing, explicit destruction, and uniqueness; they are manually managed, not entries in `DevicePools`.
- Resource implementation details are owned by `renderer_resources`; `crate::buffers`, `crate::images`, and `crate::pipeline` resolve through crate-private aliases. Do not expose these aliases as renderer public modules.
- `Allocation::Shared` only permits `allocation_mut()` through `Arc::get_mut`; callers must not assume shared allocation contents can always be mutated.
- `ImageDesc` and `BufferDesc` omit `alloc_scheme` from both `Hash` and `Eq`; `Hash` omits `name`, and `Eq` also omits `name`. Check descriptor-cache assumptions before relying on either as a complete semantic identity.
- `Image::new_with_allocation` in `renderer_resources` validates memory size/type but still creates the raw image before validation; failure paths should account for cleaning up handles if this function is revised.
- `Surface::configure` replaces the current `Arc<Swapchain>` after creating the new one; in-flight tokens retain old swapchains, so preserve that overlap behavior on reconfiguration.
- `Swapchain::acquire_image` moves the potentially blocking acquire call to `smol::unblock`, waits on a pooled fence, and constructs the acquired view before returning the frame.
- `Renderer2::draw_graph` acquires a frame, imports its image, resolves/submits the graph with acquire/release semaphores, awaits completion, and then presents.
- The active public graph API is `render_graph/legacy.rs`. Other graph siblings are private experimental/incomplete implementation, not a replacement API; avoid relying on TODO scaffolding.
- `render_graph/legacy.rs` has partial/unfinished behavior in some paths; inspect the exact pass/resource path being changed.
- `GraphResource::simple_hash` is used as an import deduplication key, not a collision-proof identity or equality check. `resolve` appends an output-read pass.
- `RenderGraph::resolve` creates a `SingleUseCommandPool` on the device main queue and records pass callbacks into command buffers. The graph's task scheduling remains single-queue in this path.
- Some legacy graph commands (`clear_pass`, `present_pass`) are convenience helpers; `present_pass` is deprecated in favor of marking outputs with `Access::present()`.
- The newer `render_graph/commands.rs` and `recorder.rs` use additional `ResourceId`/typed resource wrappers, distinct from legacy `GraphResourceId` and `GraphResource`.
- The crate root also exposes `Extent2D` and `utils` alongside renderer modules and device re-exports.
- `SamplerDesc` implements float-aware equality and hashing with utility helpers; keep those implementations consistent if adding sampler fields.
- `Pipeline::new_graphics` supports dynamic rendering state through `RenderingState`; its render-pass and rendering choices are supplied in `GraphicsPipelineDesc`.
- `Buffer` and `Image` creation in `renderer_resources` allocates/binds through `DeviceInner::allocate_for_buffer` / `allocate_for_image` and `bind_buffer_memory` / `bind_image_memory`; device ownership and final VMA allocation release are handled by the device-layer wrappers.
- Resource types live in `renderer_resources`; the `renderer` aliases are crate-private and are not externally reachable as `renderer::buffers::...` etc.
- `Renderer2::new` selects an adapter with `None` for the surface, then creates the logical device using its retained display handle. `choose_adapter_default` does not evaluate raw surface compatibility.
- `render_graph/legacy.rs` implements the active public graph resolver. Its resources are ordered by `GraphResourceId`, and passes declare reads/writes through `PassDesc` so the resolver can derive dependencies and transitions.
## Conventions
- Nightly crate features in `lib.rs`: `negative_impls`, `map_try_insert`, `debug_closure_helpers`.
- Use `renderer`'s re-exported `Device`, `DeviceOwned`, and `DeviceRef` APIs rather than adding a parallel device ownership model.
- Do not commit changes. Verify with `cargo check --workspace`; device-layer tests run with `cargo test -p renderer_device`.
- Run focused crate checks with `cargo check -p renderer`; use `cargo check --workspace` for changes crossing crate boundaries.
- Resource APIs live in the public `renderer_resources` crate. Renderer aliases are deliberately crate-private; keep the renderer-facing API intentional when adding re-exports.
## Change guidance
- Add buffer/image/pipeline resource constructors in `renderer_resources`, using its `renderer_device` re-exports and retaining the correct device/handle owner. Do not add a dependency from `renderer_resources` back to `renderer`.
- For every resource, identify whether it owns, shares, or merely borrows Vulkan handles and memory before implementing `Drop` or changing field order.
- Keep image-view format, aspect, mip, layer, and view-kind validation in `renderer_resources`; Vulkan view compatibility depends on the backing image description and create flags.
- A swapchain image is not an independently destroyable `Image`; retain its swapchain and do not attach a GPU allocation to it.
- Keep allocation freeing and Vulkan handle destruction in `renderer_resources` in a safe drop order: image/buffer before bound allocation memory.
- Use the correct queue family set in buffer/image descriptions when resources are accessed from more than one queue; `QueueFlags` here is the device-layer selection type.
- Command buffer recording and queue submission have separate synchronization requirements. Preserve queue locks and ensure an in-flight command buffer/pool is not freed before completion.
- `CommandBufferFuture` owns the submitted command buffer until the future is dropped; callers that discard it early change its lifetime and must ensure GPU work is complete.
- Surface/window raw-handle validity is the caller's obligation; retain the originating instance until surface teardown.
- `SwapchainImage` and `SwapchainFrame` are must-use ownership tokens; keep presentation consuming the token to prevent duplicate presentation.
- Graph callbacks in `PassDesc` capture resources across graph resolution. Preserve their lifetimes and correctly declare resource reads/writes for dependency/barrier generation.
- When adding a public graph API, check whether it belongs to the legacy `render_graph` re-export or an unexposed experimental sibling; do not accidentally promise incomplete internals.
- The newer typed graph modules have stubs and `todo!()` paths; add tests and complete the actual resolve/record flow before routing runtime rendering through them.
- Egui texture IDs map separately between `egui::TextureId` and internal `TextureId`; updates and frees must maintain both maps and texture manager state.
- Shader paths are currently relative to the workspace working directory; preserve or intentionally redesign that runtime asset lookup contract.
- `renderer_resources::util` owns the float helpers and `F32`; renderer re-exports those names through `renderer::util`. Keep `SamplerDesc` float equality/hash helpers in sync for every field so `SamplerCache` map keys remain valid.
- For pipeline changes, account for both legacy render-pass and dynamic-rendering descriptions and the shared `device.pools.pipeline_cache.raw` handle.
- `util::DropGuard` invokes its closure at drop; do not allow a closure to outlive raw Vulkan dependencies it captures.
- The `egui` pre-pass currently packs texture updates into a staging image/buffer and registers graph work; texture-free deltas and upload ordering deserve explicit coverage when changing the integration.
- The private font-atlas image is CPU memory, not a Vulkan `images::Image`; do not conflate the similarly named `Image` types when editing `text.rs`.
- `weak_vec` moved to `renderer_resources::util` and is not re-exported by `renderer`; it prunes dead weak references on capacity pressure. Image-side view tracking should remain weak to avoid ownership cycles.
- Render graph IDs are distinct: legacy `GraphResourceId` wraps `u32`; the newer internal `resources::ResourceId` also wraps `u32` (constructed via `ResourceId::from_u32`).
- Public `commands::traits::CommandBufferExt` contains raw recording convenience methods; those methods assume valid command-buffer state and do not all enforce it at runtime.
- `SwapchainConfiguration.image_count` is treated as the additional desired in-flight count during validation, while creation asks for minimum surface image count plus that configured value. The fallback-configuration helper assigns a total count.
- The supported surface capability extension query can filter present modes based on the selected mode; its behavior depends on optional `InstanceExtensions` support.
- `rendering::Wireframe` allocates GPU buffers and submits a one-shot upload before returning, then records its rendering pass through the legacy graph.

View file

@ -4,44 +4,41 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
tinyvec = { workspace = true } renderer_device = { path = "../renderer_device" }
renderer_resources = { path = "../renderer_resources" }
renderer_macros = { path = "renderer_macros" }
arrayvec = { workspace = true } arrayvec = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
petgraph = { workspace = true } petgraph = { workspace = true }
itertools = { workspace = true } # itertools = { workspace = true }
ahash = { workspace = true } ahash = { workspace = true }
rand = { workspace = true } # rand = { workspace = true }
parking_lot = { workspace = true } parking_lot = { workspace = true }
glam = { workspace = true } glam = { workspace = true }
bitflags = { workspace = true } bitflags = { workspace = true }
thread_local = {workspace = true} thread_local = {workspace = true}
thiserror = { workspace = true } thiserror = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
ash = { workspace = true } ash = { workspace = true }
ash-window = { workspace = true } ash-window = { workspace = true }
vk-mem = { workspace = true } # vk-mem = { workspace = true }
gpu-allocator = { workspace = true }
rectangle-pack = { workspace = true } rectangle-pack = { workspace = true }
md-5 = { workspace = true }
raw-window-handle = { workspace = true } raw-window-handle = { workspace = true }
egui = { workspace = true , features = ["bytemuck"]} egui = { workspace = true , features = ["bytemuck"]}
egui_winit_platform = { workspace = true } # egui_winit_platform = { workspace = true }
futures = { workspace = true } # futures = { workspace = true }
smol = { workspace = true } smol = { workspace = true }
# tokio = {workspace = true, features = ["rt", "sync"]} # tokio = {workspace = true, features = ["rt", "sync"]}
bevy_tasks = { workspace = true } # bevy_tasks = { workspace = true }
derive_more = { workspace = true , features = ["debug"] } derive_more = { workspace = true , features = ["debug"] }
cfg-if = "1.0.0" cfg-if = "1.0.0"
dyn-clone = "1"
crossbeam = "0.8.4" crossbeam = "0.8.4"
bytemuck = { version = "1.21.0", features = ["derive"] } bytemuck = { version = "1.21.0", features = ["derive"] }
@ -49,6 +46,8 @@ cosmic-text = "0.12.1"
sys-locale = "0.3.2" sys-locale = "0.3.2"
guillotiere = "0.6.2" guillotiere = "0.6.2"
ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" }
[dev-dependencies] [dev-dependencies]
tracing-test = "0.2.5" tracing-test = "0.2.5"
image = "0.25.5" image = "0.25.5"

View file

@ -0,0 +1,12 @@
[package]
name = "renderer_macros"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2", features = ["full"] }

View file

@ -0,0 +1,122 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields};
#[proc_macro_derive(DeviceRef, attributes(device))]
pub fn derive_device_ref(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let field = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => {
let marked_fields = fields
.named.iter()
.filter(|field| {
field
.attrs
.iter()
.any(|attr| attr.path().is_ident("device"))
})
.collect::<Vec<_>>();
let field = match marked_fields.as_slice() {
[] => {
return syn::Error::new_spanned(
&input.ident,
"no field marked with #[device]",
)
.to_compile_error()
.into();
}
[field] => *field,
duplicates => {
let mut errors = duplicates.iter().skip(1).map(|field| {
let name = field.ident.as_ref().expect("named fields have identifiers");
syn::Error::new_spanned(
name,
format!("duplicate #[device] field `{name}`"),
)
});
let mut error = errors.next().expect("at least two marked fields");
for duplicate in errors {
error.combine(duplicate);
}
return error.to_compile_error().into();
}
};
field
}
Fields::Unnamed(fields) => {
let marked_fields = fields
.unnamed.iter()
.filter(|field| {
field
.attrs
.iter()
.any(|attr| attr.path().is_ident("device"))
})
.collect::<Vec<_>>();
let field = match marked_fields.as_slice() {
[] => {
return syn::Error::new_spanned(
&input.ident,
"no field marked with #[device]",
)
.to_compile_error()
.into();
}
[field] => *field,
duplicates => {
let mut errors = duplicates.iter().skip(1).map(|field| {
let name = field.ident.as_ref().expect("named fields have identifiers");
syn::Error::new_spanned(
name,
format!("duplicate #[device] field `{name}`"),
)
});
let mut error = errors.next().expect("at least two marked fields");
for duplicate in errors {
error.combine(duplicate);
}
return error.to_compile_error().into();
}
};
field
}
_ => {
return syn::Error::new_spanned(
&input.ident,
"DeviceRef can only be derived for structs with named fields",
)
.to_compile_error()
.into();
}
},
Data::Enum(_) | Data::Union(_) => {
return syn::Error::new_spanned(
&input.ident,
"DeviceRef can only be derived for structs with named fields",
)
.to_compile_error()
.into();
}
};
let field_ident = field.ident.as_ref().expect("named fields have identifiers");
let ident = &input.ident;
let generics = input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
quote! {
impl #impl_generics ::renderer_device::device::DeviceRef for #ident #ty_generics #where_clause {
fn device(&self) -> &::renderer_device::device::Device {
::renderer_device::device::DeviceRef::device(&self.#field_ident)
}
}
}
.into()
}

View file

@ -1,171 +0,0 @@
use std::borrow::Cow;
use ash::vk;
use gpu_allocator::vulkan::AllocationScheme;
use crate::{
Device,
device::{Allocation, AllocationStrategy, DeviceObject, QueueFlags},
};
#[derive(Clone)]
pub struct BufferDesc {
pub flags: vk::BufferCreateFlags,
pub name: Option<Cow<'static, str>>,
pub size: u64,
pub usage: vk::BufferUsageFlags,
pub queue_families: QueueFlags,
pub mem_location: gpu_allocator::MemoryLocation,
pub alloc_scheme: AllocationStrategy,
}
impl std::hash::Hash for BufferDesc {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.flags.hash(state);
self.size.hash(state);
self.usage.hash(state);
self.queue_families.hash(state);
self.mem_location.hash(state);
}
}
impl std::fmt::Debug for BufferDesc {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BufferDesc")
.field("flags", &self.flags)
.field("name", &self.name)
.field("size", &self.size)
.field("usage", &self.usage)
.field("queue_families", &self.queue_families)
.field("mem_location", &self.mem_location)
.field("alloc_scheme", &self.alloc_scheme)
.finish()
}
}
impl Eq for BufferDesc {}
impl PartialEq for BufferDesc {
fn eq(&self, other: &Self) -> bool {
self.flags == other.flags
// for hashmaps, `Eq` may be more strict than `Hash`
&& self.name == other.name
&& self.size == other.size
&& self.usage == other.usage
&& self.queue_families == other.queue_families
&& self.mem_location == other.mem_location
}
}
impl Default for BufferDesc {
fn default() -> Self {
Self {
flags: Default::default(),
name: Default::default(),
size: Default::default(),
usage: Default::default(),
queue_families: QueueFlags::empty(),
mem_location: gpu_allocator::MemoryLocation::Unknown,
alloc_scheme: AllocationStrategy::default(),
}
}
}
#[derive(Debug)]
pub struct Buffer {
buffer: DeviceObject<vk::Buffer>,
pub desc: BufferDesc,
alloc: Allocation,
}
impl Eq for Buffer {}
impl PartialEq for Buffer {
fn eq(&self, other: &Self) -> bool {
*self.buffer == *other.buffer
}
}
impl Buffer {
pub fn new(device: Device, desc: BufferDesc) -> crate::Result<Self> {
let (buffer, requirements) = Self::new_raw(device.clone(), &desc)?;
let alloc =
device
.alloc2
.lock()
.allocate(&gpu_allocator::vulkan::AllocationCreateDesc {
name: desc.name.as_deref().unwrap_or_default(),
requirements,
location: desc.mem_location,
linear: true,
allocation_scheme: match desc.alloc_scheme {
AllocationStrategy::AllocatorManaged => {
AllocationScheme::GpuAllocatorManaged
}
AllocationStrategy::Dedicated => AllocationScheme::DedicatedBuffer(buffer),
},
})?;
unsafe {
device
.raw
.bind_buffer_memory(buffer, alloc.memory(), alloc.offset())?;
}
Ok(Self {
buffer: DeviceObject::new_debug_named(device.clone(), buffer, desc.name.clone()),
desc,
alloc: Allocation::Owned(DeviceObject::new(device, alloc)),
})
}
fn new_raw(
device: Device,
desc: &BufferDesc,
) -> crate::Result<(vk::Buffer, vk::MemoryRequirements)> {
let queue_families = device.queues.family_indices(desc.queue_families);
let sharing_mode = if queue_families.len() > 1 {
vk::SharingMode::CONCURRENT
} else {
vk::SharingMode::EXCLUSIVE
};
let create_info = vk::BufferCreateInfo::default()
.size(desc.size)
.usage(desc.usage)
.queue_family_indices(&queue_families)
.sharing_mode(sharing_mode);
let buffer = unsafe { device.dev().create_buffer(&create_info, None)? };
let mem_reqs = unsafe { device.dev().get_buffer_memory_requirements(buffer) };
Ok((buffer, mem_reqs))
}
pub fn map(&mut self) -> Option<&[u8]> {
if let Some(alloc) = self.alloc.allocation() {
alloc
.mapped_slice()
.map(|slice| &slice[..self.desc.size as usize])
} else {
None
}
}
pub fn map_mut(&mut self) -> Option<&mut [u8]> {
if let Some(alloc) = self.alloc.allocation_mut() {
alloc
.mapped_slice_mut()
.map(|slice| &mut slice[..self.desc.size as usize])
} else {
None
}
}
pub fn raw(&self) -> vk::Buffer {
*self.buffer
}
pub fn len(&self) -> u64 {
self.desc.size
}
}

View file

@ -1,8 +1,7 @@
use std::sync::{Arc, atomic::AtomicU8}; use std::sync::{Arc, atomic::AtomicU8};
use crate::{ use crate::{
define_device_owned_handle, device::{DeviceObject, DeviceOwned},
device::DeviceOwned,
images::{Image, QueueOwnership}, images::{Image, QueueOwnership},
pipeline::{Pipeline, PipelineLayout}, pipeline::{Pipeline, PipelineLayout},
sync::{self, FenceFuture}, sync::{self, FenceFuture},
@ -33,7 +32,7 @@ impl Drop for SingleUseCommandPool {
impl SingleUseCommandPool { impl SingleUseCommandPool {
pub fn new(device: Device, queue: Queue) -> VkResult<Arc<Self>> { pub fn new(device: Device, queue: Queue) -> VkResult<Arc<Self>> {
let pool_info = vk::CommandPoolCreateInfo::default() let pool_info = vk::CommandPoolCreateInfo::default()
.queue_family_index(queue.family_index()) .queue_family_index(queue.family_index().index())
.flags(vk::CommandPoolCreateFlags::TRANSIENT); .flags(vk::CommandPoolCreateFlags::TRANSIENT);
let pool = unsafe { device.dev().create_command_pool(&pool_info, None)? }; let pool = unsafe { device.dev().create_command_pool(&pool_info, None)? };
@ -174,15 +173,32 @@ impl CommandBufferState2 {
} }
} }
define_device_owned_handle! {
#[derive(Debug)] #[derive(Debug)]
pub SingleUseCommand(vk::CommandBuffer) { pub struct SingleUseCommand {
inner: DeviceObject<vk::CommandBuffer>,
pool: Arc<SingleUseCommandPool>, pool: Arc<SingleUseCommandPool>,
state: CommandBufferState2, state: CommandBufferState2,
} => |this| unsafe { }
this.pool
.pool impl DeviceOwned<vk::CommandBuffer> for SingleUseCommand {
.with_locked(|&pool| this.device().dev().free_command_buffers(pool, &[this.handle()])) fn device(&self) -> &Device {
self.inner.device()
}
fn handle(&self) -> vk::CommandBuffer {
*self.inner
}
}
impl Drop for SingleUseCommand {
fn drop(&mut self) {
unsafe {
self.pool.pool.with_locked(|&pool| {
self.device()
.dev()
.free_command_buffers(pool, &[self.handle()])
})
}
} }
} }
@ -208,7 +224,15 @@ impl SingleUseCommand {
buffer buffer
}; };
Self::construct(device, buffer, None, pool, CommandBufferState2::recording()) Ok(Self {
inner: DeviceObject::new_debug_named(
device,
buffer,
None::<std::borrow::Cow<'static, str>>,
),
pool,
state: CommandBufferState2::recording(),
})
} }
pub fn state(&self) -> CommandBufferState { pub fn state(&self) -> CommandBufferState {
@ -223,7 +247,7 @@ impl SingleUseCommand {
pub fn end(&self) -> VkResult<()> { pub fn end(&self) -> VkResult<()> {
assert_eq!(self.state(), CommandBufferState::Recording); assert_eq!(self.state(), CommandBufferState::Recording);
unsafe { unsafe {
self.inner.dev().dev().end_command_buffer(self.handle())?; self.device().dev().end_command_buffer(self.handle())?;
} }
self.state.set_executable(); self.state.set_executable();
Ok(()) Ok(())
@ -288,7 +312,7 @@ impl SingleUseCommand {
wait: Option<(vk::Semaphore, vk::PipelineStageFlags)>, wait: Option<(vk::Semaphore, vk::PipelineStageFlags)>,
signal: Option<vk::Semaphore>, signal: Option<vk::Semaphore>,
) -> crate::Result<()> { ) -> crate::Result<()> {
let fence = Arc::new(sync::Fence::from_pool(&self.device().pools.fences, None)?); let fence = Arc::new(sync::Fence::from_pool(self.device(), None)?);
let future = self.submit_async(wait, signal, fence)?; let future = self.submit_async(wait, signal, fence)?;
future.block()?; future.block()?;
Ok(()) Ok(())
@ -638,12 +662,12 @@ mod command_pools {
use arrayvec::ArrayVec; use arrayvec::ArrayVec;
use ash::{prelude::VkResult, vk}; use ash::{prelude::VkResult, vk};
use parking_lot::Mutex; use parking_lot::Mutex;
use renderer_device::queue::QueueFamilyIndex;
use smol::future::FutureExt; use smol::future::FutureExt;
use thread_local::ThreadLocal; use thread_local::ThreadLocal;
use crate::{ use crate::{
define_device_owned_handle, device::{Device, DeviceObject, DeviceOwned},
device::{Device, DeviceOwned},
queue::Queue, queue::Queue,
sync, sync,
util::MutexExt, util::MutexExt,
@ -659,13 +683,19 @@ mod command_pools {
InvalidQueueSubmission, InvalidQueueSubmission,
} }
define_device_owned_handle! { pub struct CommandPool {
#[derive()] inner: DeviceObject<vk::CommandPool>,
pub CommandPool(vk::CommandPool) { family: QueueFamilyIndex,
family: u32,
free_list: Mutex<Cell<(Vec<vk::CommandBufferLevel>, Vec<vk::CommandBuffer>)>>, free_list: Mutex<Cell<(Vec<vk::CommandBufferLevel>, Vec<vk::CommandBuffer>)>>,
} => |this| unsafe { }
this.device().dev().destroy_command_pool(this.handle(), None);
impl DeviceOwned<vk::CommandPool> for CommandPool {
fn device(&self) -> &Device {
self.inner.device()
}
fn handle(&self) -> vk::CommandPool {
*self.inner
} }
} }
@ -674,17 +704,21 @@ mod command_pools {
impl CommandPool { impl CommandPool {
pub fn new( pub fn new(
dev: Device, dev: Device,
family: u32, family: QueueFamilyIndex,
flags: vk::CommandPoolCreateFlags, flags: vk::CommandPoolCreateFlags,
name: Option<Cow<'static, str>>, name: Option<Cow<'static, str>>,
) -> Result<CommandPool, vk::Result> { ) -> Result<CommandPool, vk::Result> {
let pool_info = vk::CommandPoolCreateInfo::default() let pool_info = vk::CommandPoolCreateInfo::default()
.queue_family_index(family) .queue_family_index(family.index())
.flags(flags); .flags(flags);
let pool = unsafe { dev.dev().create_command_pool(&pool_info, None)? }; let pool = unsafe { dev.dev().create_command_pool(&pool_info, None)? };
Self::construct(dev, pool, name, family, Mutex::new(Default::default())) Ok(Self {
inner: DeviceObject::new_debug_named(dev, pool, name),
family,
free_list: Mutex::new(Default::default()),
})
} }
#[allow(dead_code)] #[allow(dead_code)]
@ -761,13 +795,11 @@ mod command_pools {
.begin_command_buffer(handle, &begin_info)?; .begin_command_buffer(handle, &begin_info)?;
} }
Ok(CommandBuffer::construct( Ok(CommandBuffer {
self.device().clone(), inner: DeviceObject::new_debug_named(self.device().clone(), handle, name),
handle, pool: self.clone(),
name,
self.clone(),
level, level,
)?) })
} }
pub fn alloc_primary( pub fn alloc_primary(
@ -795,7 +827,7 @@ mod command_pools {
} }
} }
fn find_or_default(&mut self, dev: &Device, family: u32) -> Arc<CommandPool> { fn find_or_default(&mut self, dev: &Device, family: QueueFamilyIndex) -> Arc<CommandPool> {
match self match self
.per_family_pools .per_family_pools
.binary_search_by_key(&family, |entry| entry.0) .binary_search_by_key(&family, |entry| entry.0)
@ -829,7 +861,7 @@ mod command_pools {
#[derive(Debug)] #[derive(Debug)]
struct CommandPoolsShard { struct CommandPoolsShard {
per_family_pools: ArrayVec<(u32, Arc<CommandPool>), 8>, per_family_pools: ArrayVec<(QueueFamilyIndex, Arc<CommandPool>), 8>,
} }
pub struct CommandPools { pub struct CommandPools {
@ -838,7 +870,7 @@ mod command_pools {
} }
impl CommandPools { impl CommandPools {
pub fn get_for_family(&self, family: u32) -> Arc<CommandPool> { pub fn get_for_family(&self, family: QueueFamilyIndex) -> Arc<CommandPool> {
let shard = self let shard = self
.shards .shards
.get_or(|| core::cell::Cell::new(CommandPoolsShard::new()).into()); .get_or(|| core::cell::Cell::new(CommandPoolsShard::new()).into());
@ -852,7 +884,7 @@ mod command_pools {
pub fn allocate_buffer( pub fn allocate_buffer(
&self, &self,
family: u32, family: QueueFamilyIndex,
name: Option<Cow<'static, str>>, name: Option<Cow<'static, str>>,
level: vk::CommandBufferLevel, level: vk::CommandBufferLevel,
flags: vk::CommandBufferUsageFlags, flags: vk::CommandBufferUsageFlags,
@ -861,14 +893,31 @@ mod command_pools {
} }
} }
define_device_owned_handle! {
#[derive(Debug)] #[derive(Debug)]
pub CommandBuffer(vk::CommandBuffer) { pub struct CommandBuffer {
inner: DeviceObject<vk::CommandBuffer>,
pool: Arc<CommandPool>, pool: Arc<CommandPool>,
#[allow(unused)] #[allow(unused)]
level: vk::CommandBufferLevel, level: vk::CommandBufferLevel,
} => |this| unsafe { }
this.device().dev().free_command_buffers(this.pool.handle(), &[this.handle()]);
impl DeviceOwned<vk::CommandBuffer> for CommandBuffer {
fn device(&self) -> &Device {
self.inner.device()
}
fn handle(&self) -> vk::CommandBuffer {
*self.inner
}
}
impl Drop for CommandBuffer {
fn drop(&mut self) {
unsafe {
self.device()
.dev()
.free_command_buffers(self.pool.handle(), &[self.handle()]);
}
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,17 @@
use std::{ use std::{
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap},
mem::ManuallyDrop,
sync::Arc, sync::Arc,
}; };
use ash::vk; use ash::vk;
use gpu_allocator::MemoryLocation;
use indexmap::IndexMap; use indexmap::IndexMap;
use crate::{ use crate::{
EguiState, EguiState,
buffers::{Buffer, BufferDesc}, buffers::{Buffer, BufferDesc},
commands::traits::CommandBufferExt, commands::traits::CommandBufferExt,
device::{self, AllocationStrategy}, device::{self, AllocationStrategy, MemoryLocation},
images::{Image, ImageDesc, ImageViewDesc}, images::{Image, ImageDesc, ImageViewDesc},
render_graph::{ render_graph::{
Access, Barrier, GraphResourceDesc, GraphResourceId, PassDesc, RecordFn, RenderContext, Access, Barrier, GraphResourceDesc, GraphResourceId, PassDesc, RecordFn, RenderContext,
@ -485,6 +485,13 @@ pub fn egui_pass(
textures_indices.push(idx as u32); textures_indices.push(idx as u32);
} }
const TEXTURE_DESCRIPTOR_CAPACITY: usize = 10;
if textures.len() > TEXTURE_DESCRIPTOR_CAPACITY {
return Err(crate::Error::Todo(
"egui frame references more textures than the descriptor array supports",
));
}
let num_draw_calls = draw_calls.len(); let num_draw_calls = draw_calls.len();
let vertices_size = vertices.len() * size_of::<Vertex>(); let vertices_size = vertices.len() * size_of::<Vertex>();
let indices_size = indices.len() * size_of::<u32>(); let indices_size = indices.len() * size_of::<u32>();
@ -558,57 +565,110 @@ pub fn egui_pass(
(staging, vertices, indices, draw_calls, texture_ids) (staging, vertices, indices, draw_calls, texture_ids)
}; };
let descriptor_infos = textures // This map contains only unique textures referenced by these meshes. Its insertion order
// is the exact index stored in `texture_ids`, matching `texture[index]` in egui.slang.
let descriptor_resources = textures
.values() .values()
.map(|entry| { .map(|entry| {
let texture = texture_handler.get_texture(entry.id).unwrap(); let texture = texture_handler.get_texture(entry.id).unwrap();
vk::DescriptorImageInfo { let view = Arc::new(
sampler: samplers.get_sampler(entry.as_sampler_desc()).unwrap(), texture
image_view: texture
.create_view( .create_view(
ImageViewDesc::color_2d() ImageViewDesc::color_2d()
.with_mip_range(0..1) .with_mip_range(0..1)
.with_layer_range(0..1), .with_layer_range(0..1),
) )
.unwrap() .unwrap(),
.raw(), );
image_layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, let sampler = samplers.get_sampler_owned(entry.as_sampler_desc()).unwrap();
} (view, sampler)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let uniform_info = vk::DescriptorBufferInfo { let texture_ids = Arc::new(texture_ids);
buffer: texture_ids.raw(), // Each pass gets its own managed set, so later passes/frames cannot mutate the contents
// captured by this callback.
let descriptor_pool = crate::pipeline::DescriptorPool::new(
dev.clone(),
crate::pipeline::DescriptorPoolDesc {
flags: vk::DescriptorPoolCreateFlags::UPDATE_AFTER_BIND,
name: Some("egui-pass-descriptor-pool".into()),
sizes: &[
vk::DescriptorPoolSize {
ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
descriptor_count: TEXTURE_DESCRIPTOR_CAPACITY as u32,
},
vk::DescriptorPoolSize {
ty: vk::DescriptorType::STORAGE_BUFFER,
descriptor_count: 1,
},
],
max_sets: 1,
},
)?;
let mut descriptor_set = descriptor_pool
.allocate_managed(crate::pipeline::DescriptorSetAllocDesc {
name: None,
layout: &egui_state.descriptor_layout,
})
.map_err(|_| crate::Error::Todo("egui managed descriptor allocation failed"))?;
{
let mut writer = descriptor_set
.writer()
.map_err(|_| crate::Error::Todo("egui descriptor set is stale"))?;
writer
.clear_binding(EguiState::TEXTURE_BINDING)
.map_err(|_| crate::Error::Todo("egui descriptor binding clear failed"))?;
// Managed bindings are partially bound, but Vulkan descriptor contents do not have an
// unset operation. Populate every unused slot with a live descriptor to avoid stale
// shader-array entries while preserving the mesh texture indices above.
if let Some((first_view, first_sampler)) = descriptor_resources.first() {
for array_element in descriptor_resources.len()..TEXTURE_DESCRIPTOR_CAPACITY {
writer
.write_image(
EguiState::TEXTURE_BINDING,
array_element as u32,
&[crate::pipeline::ImageDescriptor {
view: first_view,
sampler: Some(first_sampler),
layout: vk::ImageLayout::GENERAL,
}],
)
.map_err(|_| crate::Error::Todo("egui descriptor image padding failed"))?;
}
}
for (array_element, (view, sampler)) in descriptor_resources.iter().enumerate() {
writer
.write_image(
EguiState::TEXTURE_BINDING,
array_element as u32,
&[crate::pipeline::ImageDescriptor {
view,
sampler: Some(sampler),
layout: vk::ImageLayout::GENERAL,
}],
)
.map_err(|_| crate::Error::Todo("egui descriptor image write failed"))?;
}
writer
.write_buffer(
EguiState::UNIFORM_BINDING,
0,
&[crate::pipeline::BufferDescriptor {
buffer: &texture_ids,
offset: 0, offset: 0,
range: texture_ids.len(), range: texture_ids.len(),
}; }],
)
let descriptor_writes = descriptor_infos .map_err(|_| crate::Error::Todo("egui descriptor buffer write failed"))?;
.iter() writer
.enumerate() .commit()
.map(|(i, info)| { .map_err(|_| crate::Error::Todo("egui descriptor commit failed"))?;
vk::WriteDescriptorSet::default()
.image_info(core::slice::from_ref(info))
.descriptor_count(1)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.dst_set(egui_state.descriptor_set)
.dst_binding(EguiState::TEXTURE_BINDING)
.dst_array_element(i as u32)
})
.chain(core::iter::once({
vk::WriteDescriptorSet::default()
.buffer_info(core::slice::from_ref(&uniform_info))
.descriptor_count(1)
.dst_binding(EguiState::UNIFORM_BINDING)
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
.dst_array_element(0)
.dst_set(egui_state.descriptor_set)
}))
.collect::<Vec<_>>();
unsafe {
dev.dev().update_descriptor_sets(&descriptor_writes, &[]);
} }
let descriptor_set_raw = descriptor_set
.raw()
.map_err(|_| crate::Error::Todo("egui descriptor set is stale"))?;
let descriptor_set_owner = Arc::new(descriptor_set);
let to_remove_tex_ids = output let to_remove_tex_ids = output
.textures_delta .textures_delta
@ -631,10 +691,15 @@ pub fn egui_pass(
let record: Box<RecordFn> = Box::new({ let record: Box<RecordFn> = Box::new({
let pipeline = egui_state.pipeline.clone(); let pipeline = egui_state.pipeline.clone();
let descriptor_set = egui_state.descriptor_set; let descriptor_set_owner = descriptor_set_owner.clone();
let screen_rect = egui.screen_rect(); let screen_rect = egui.screen_rect();
move |ctx: &RenderContext| -> crate::Result<()> { move |ctx: &RenderContext| -> crate::Result<()> {
// Keep this managed wrapper alive through recording, and validate its generation
// immediately before binding the raw handle captured for this frame.
let _descriptor_set = descriptor_set_owner
.raw()
.map_err(|_| crate::Error::Todo("egui descriptor set is stale"))?;
let cmd = &ctx.cmd; let cmd = &ctx.cmd;
let staging = ctx.get_buffer(draw_staging).unwrap(); let staging = ctx.get_buffer(draw_staging).unwrap();
let vertices = ctx.get_buffer(vertices).unwrap(); let vertices = ctx.get_buffer(vertices).unwrap();
@ -705,7 +770,7 @@ pub fn egui_pass(
let color_attachment = &vk::RenderingAttachmentInfo::default() let color_attachment = &vk::RenderingAttachmentInfo::default()
.image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) .image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
.image_view(target.create_view(ImageViewDesc::color_2d())?.raw()) .image_view(ManuallyDrop::new(target.create_view(ImageViewDesc::color_2d())?).raw())
.load_op(vk::AttachmentLoadOp::LOAD) .load_op(vk::AttachmentLoadOp::LOAD)
.store_op(vk::AttachmentStoreOp::STORE); .store_op(vk::AttachmentStoreOp::STORE);
@ -742,7 +807,7 @@ pub fn egui_pass(
cmd.bind_descriptor_sets( cmd.bind_descriptor_sets(
pipeline.layout(), pipeline.layout(),
vk::PipelineBindPoint::GRAPHICS, vk::PipelineBindPoint::GRAPHICS,
&[descriptor_set], &[descriptor_set_raw],
); );
cmd.draw_indexed_indirect( cmd.draw_indexed_indirect(
draw_calls.raw(), draw_calls.raw(),

View file

@ -1,41 +1,32 @@
#![feature( #![feature(negative_impls, map_try_insert, debug_closure_helpers)]
negative_impls,
map_try_insert,
debug_closure_helpers,
core_io_borrowed_buf,
slice_partition_dedup
)]
use std::{collections::HashMap, fmt::Debug, path::PathBuf, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use ash::{prelude::VkResult, vk};
use raw_window_handle::{RawDisplayHandle, RawWindowHandle}; use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
use ash::{
ext,
prelude::VkResult,
vk::{self, SurfaceCapabilitiesKHR},
};
use dyn_clone::DynClone;
pub use ash; pub use ash;
mod buffers;
pub mod commands; pub mod commands;
mod debug;
pub mod device;
#[path = "egui.rs"] #[path = "egui.rs"]
mod egui_pass; mod egui_pass;
mod images;
pub mod instance;
mod pipeline;
pub mod render_graph; pub mod render_graph;
pub mod rendering; pub mod rendering;
pub mod swapchain; pub mod swapchain;
pub mod sync;
mod text; mod text;
pub mod util; pub mod util;
use device::{Device, DeviceOwned}; pub(crate) use renderer_resources::{buffers, images, pipeline};
pub use renderer_device::{
DebugUtils, DebugUtilsCreateInfo, Device, DeviceOwned, DeviceRef, Error,
ExtendsDeviceFeatures2Debug, ExtendsDeviceProperties2Debug, Extension, Instance,
PhysicalDeviceFeatures, PhysicalDeviceInfo, PhysicalDeviceProperties, Result,
SurfaceCapabilities, compatible_extension_properties, extension_intersection,
get_physical_device_features, get_physical_device_properties, make_extension,
};
pub use renderer_device::{device, instance, queue, sync};
pub use renderer_macros::DeviceRef;
mod texture { mod texture {
use std::{collections::BTreeMap, sync::Arc}; use std::{collections::BTreeMap, sync::Arc};
@ -79,585 +70,9 @@ mod texture {
use util::Rgba; use util::Rgba;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("No Physical Device found for this instance.")]
NoAdapter,
#[error("Swapchain suboptimal.")]
SuboptimalSwapchain,
#[error(transparent)]
LoadingError(#[from] ash::LoadingError),
#[error(transparent)]
Result(#[from] ash::vk::Result),
#[error(transparent)]
CStrError(#[from] core::ffi::c_str::FromBytesUntilNulError),
#[error(transparent)]
Utf8Error(#[from] core::str::Utf8Error),
#[error(transparent)]
NulError(#[from] std::ffi::NulError),
#[error(transparent)]
GpuAllocatorError(#[from] gpu_allocator::AllocationError),
#[error("No Physical Device found.")]
NoPhysicalDevice,
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(
"Image dimensions ({width}x{height}) exceed the maximum allowed size of {max_size}x{max_size}."
)]
ImageTooLarge {
width: u32,
height: u32,
max_size: u32,
},
#[error("Image dimensions cannot be zero.")]
ImageZeroSized,
#[error("Incompatible image view kind {view_kind:?} for image kind {image_kind:?}.")]
IncompatibleImageViewKind {
view_kind: vk::ImageViewType,
image_kind: vk::ImageType,
},
#[error("Asset missing at path {0:?}")]
AssetMissing(PathBuf),
#[error("Unspecified Error")]
Unspecified,
#[error("BEEP BOOP create an error variant for {0} BEEP BOOP")]
Todo(&'static str),
}
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct DeviceExtension<'a> {
name: &'a core::ffi::CStr,
version: u32,
}
/// returns true if lhs and rhs have the same name and lhs spec_version is less
/// than or equal to rhs spec_version
#[allow(dead_code)]
fn compatible_extension_properties(
lhs: &vk::ExtensionProperties,
rhs: &vk::ExtensionProperties,
) -> bool {
let Some(lhs_name) = lhs.extension_name_as_c_str().ok() else {
return false;
};
let Some(rhs_name) = rhs.extension_name_as_c_str().ok() else {
return false;
};
if lhs_name == rhs_name {
lhs.spec_version <= rhs.spec_version
} else {
false
}
}
pub mod queue;
pub trait ExtendsDeviceFeatures2Debug:
vk::ExtendsPhysicalDeviceFeatures2 + Debug + Send + Sync
{
}
pub trait ExtendsDeviceProperties2Debug:
vk::ExtendsPhysicalDeviceProperties2 + Debug + DynClone + Send + Sync
{
}
impl<T: vk::ExtendsPhysicalDeviceFeatures2 + Debug + Send + Sync> ExtendsDeviceFeatures2Debug
for T
{
}
impl<T: vk::ExtendsPhysicalDeviceProperties2 + Debug + DynClone + Send + Sync>
ExtendsDeviceProperties2Debug for T
{
}
#[derive(Debug)]
pub struct PhysicalDeviceInfo {
pub pdev: vk::PhysicalDevice,
pub properties: PhysicalDeviceProperties,
pub features: PhysicalDeviceFeatures,
}
#[derive(Debug)]
pub struct SurfaceCapabilities {
pub capabilities: SurfaceCapabilitiesKHR,
/// The minimum number of images that must be present in the swapchain for
/// the surface for any present mode supported by the surface.
pub min_image_count: u32,
pub formats: Vec<vk::SurfaceFormatKHR>,
pub present_modes: Vec<vk::PresentModeKHR>,
}
#[derive(Default, Debug)]
pub struct PhysicalDeviceFeatures {
pub core: vk::PhysicalDeviceFeatures,
pub core11: vk::PhysicalDeviceVulkan11Features<'static>,
pub core12: vk::PhysicalDeviceVulkan12Features<'static>,
pub core13: vk::PhysicalDeviceVulkan13Features<'static>,
pub mesh_shader: Option<vk::PhysicalDeviceMeshShaderFeaturesEXT<'static>>,
}
impl PhysicalDeviceFeatures {
pub fn push_to_device_create_info<'a>(
&'a mut self,
mut create_info: vk::DeviceCreateInfo<'a>,
) -> vk::DeviceCreateInfo<'a> {
create_info = create_info
.enabled_features(&self.core)
.push_next(&mut self.core11)
.push_next(&mut self.core12)
.push_next(&mut self.core13);
if let Some(mesh_shader) = &mut self.mesh_shader {
create_info = create_info.push_next(mesh_shader);
}
create_info
}
pub fn superset_of(&self, other: &PhysicalDeviceFeatures) -> bool {
fn core_superset_of(
a: &vk::PhysicalDeviceFeatures,
b: &vk::PhysicalDeviceFeatures,
) -> bool {
(b.robust_buffer_access == vk::FALSE
|| a.robust_buffer_access != b.robust_buffer_access)
&& (b.full_draw_index_uint32 == vk::FALSE
|| a.full_draw_index_uint32 != b.full_draw_index_uint32)
&& (b.image_cube_array == vk::FALSE || a.image_cube_array != b.image_cube_array)
&& (b.independent_blend == vk::FALSE || a.independent_blend != b.independent_blend)
&& (b.geometry_shader == vk::FALSE || a.geometry_shader != b.geometry_shader)
&& (b.tessellation_shader == vk::FALSE
|| a.tessellation_shader != b.tessellation_shader)
&& (b.sample_rate_shading == vk::FALSE
|| a.sample_rate_shading != b.sample_rate_shading)
&& (b.dual_src_blend == vk::FALSE || a.dual_src_blend != b.dual_src_blend)
&& (b.logic_op == vk::FALSE || a.logic_op != b.logic_op)
&& (b.multi_draw_indirect == vk::FALSE
|| a.multi_draw_indirect != b.multi_draw_indirect)
&& (b.draw_indirect_first_instance == vk::FALSE
|| a.draw_indirect_first_instance != b.draw_indirect_first_instance)
&& (b.depth_clamp == vk::FALSE || a.depth_clamp != b.depth_clamp)
&& (b.depth_bias_clamp == vk::FALSE || a.depth_bias_clamp != b.depth_bias_clamp)
&& (b.fill_mode_non_solid == vk::FALSE
|| a.fill_mode_non_solid != b.fill_mode_non_solid)
&& (b.depth_bounds == vk::FALSE || a.depth_bounds != b.depth_bounds)
&& (b.wide_lines == vk::FALSE || a.wide_lines != b.wide_lines)
&& (b.large_points == vk::FALSE || a.large_points != b.large_points)
&& (b.alpha_to_one == vk::FALSE || a.alpha_to_one != b.alpha_to_one)
&& (b.multi_viewport == vk::FALSE || a.multi_viewport != b.multi_viewport)
&& (b.sampler_anisotropy == vk::FALSE
|| a.sampler_anisotropy != b.sampler_anisotropy)
&& (b.texture_compression_etc2 == vk::FALSE
|| a.texture_compression_etc2 != b.texture_compression_etc2)
&& (b.texture_compression_astc_ldr == vk::FALSE
|| a.texture_compression_astc_ldr != b.texture_compression_astc_ldr)
&& (b.texture_compression_bc == vk::FALSE
|| a.texture_compression_bc != b.texture_compression_bc)
&& (b.occlusion_query_precise == vk::FALSE
|| a.occlusion_query_precise != b.occlusion_query_precise)
&& (b.pipeline_statistics_query == vk::FALSE
|| a.pipeline_statistics_query != b.pipeline_statistics_query)
&& (b.vertex_pipeline_stores_and_atomics == vk::FALSE
|| a.vertex_pipeline_stores_and_atomics != b.vertex_pipeline_stores_and_atomics)
&& (b.fragment_stores_and_atomics == vk::FALSE
|| a.fragment_stores_and_atomics != b.fragment_stores_and_atomics)
&& (b.shader_tessellation_and_geometry_point_size == vk::FALSE
|| a.shader_tessellation_and_geometry_point_size
!= b.shader_tessellation_and_geometry_point_size)
&& (b.shader_image_gather_extended == vk::FALSE
|| a.shader_image_gather_extended != b.shader_image_gather_extended)
&& (b.shader_storage_image_extended_formats == vk::FALSE
|| a.shader_storage_image_extended_formats
!= b.shader_storage_image_extended_formats)
&& (b.shader_storage_image_multisample == vk::FALSE
|| a.shader_storage_image_multisample != b.shader_storage_image_multisample)
&& (b.shader_storage_image_read_without_format == vk::FALSE
|| a.shader_storage_image_read_without_format
!= b.shader_storage_image_read_without_format)
&& (b.shader_storage_image_write_without_format == vk::FALSE
|| a.shader_storage_image_write_without_format
!= b.shader_storage_image_write_without_format)
&& (b.shader_uniform_buffer_array_dynamic_indexing == vk::FALSE
|| a.shader_uniform_buffer_array_dynamic_indexing
!= b.shader_uniform_buffer_array_dynamic_indexing)
&& (b.shader_sampled_image_array_dynamic_indexing == vk::FALSE
|| a.shader_sampled_image_array_dynamic_indexing
!= b.shader_sampled_image_array_dynamic_indexing)
&& (b.shader_storage_buffer_array_dynamic_indexing == vk::FALSE
|| a.shader_storage_buffer_array_dynamic_indexing
!= b.shader_storage_buffer_array_dynamic_indexing)
&& (b.shader_storage_image_array_dynamic_indexing == vk::FALSE
|| a.shader_storage_image_array_dynamic_indexing
!= b.shader_storage_image_array_dynamic_indexing)
&& (b.shader_clip_distance == vk::FALSE
|| a.shader_clip_distance != b.shader_clip_distance)
&& (b.shader_cull_distance == vk::FALSE
|| a.shader_cull_distance != b.shader_cull_distance)
&& (b.shader_float64 == vk::FALSE || a.shader_float64 != b.shader_float64)
&& (b.shader_int64 == vk::FALSE || a.shader_int64 != b.shader_int64)
&& (b.shader_int16 == vk::FALSE || a.shader_int16 != b.shader_int16)
&& (b.shader_resource_residency == vk::FALSE
|| a.shader_resource_residency != b.shader_resource_residency)
&& (b.shader_resource_min_lod == vk::FALSE
|| a.shader_resource_min_lod != b.shader_resource_min_lod)
&& (b.sparse_binding == vk::FALSE || a.sparse_binding != b.sparse_binding)
&& (b.sparse_residency_buffer == vk::FALSE
|| a.sparse_residency_buffer != b.sparse_residency_buffer)
&& (b.sparse_residency_image2_d == vk::FALSE
|| a.sparse_residency_image2_d != b.sparse_residency_image2_d)
&& (b.sparse_residency_image3_d == vk::FALSE
|| a.sparse_residency_image3_d != b.sparse_residency_image3_d)
&& (b.sparse_residency2_samples == vk::FALSE
|| a.sparse_residency2_samples != b.sparse_residency2_samples)
&& (b.sparse_residency4_samples == vk::FALSE
|| a.sparse_residency4_samples != b.sparse_residency4_samples)
&& (b.sparse_residency8_samples == vk::FALSE
|| a.sparse_residency8_samples != b.sparse_residency8_samples)
&& (b.sparse_residency16_samples == vk::FALSE
|| a.sparse_residency16_samples != b.sparse_residency16_samples)
&& (b.sparse_residency_aliased == vk::FALSE
|| a.sparse_residency_aliased != b.sparse_residency_aliased)
&& (b.variable_multisample_rate == vk::FALSE
|| a.variable_multisample_rate != b.variable_multisample_rate)
&& (b.inherited_queries == vk::FALSE || a.inherited_queries != b.inherited_queries)
}
fn core11_superset_of(
a: &vk::PhysicalDeviceVulkan11Features,
b: &vk::PhysicalDeviceVulkan11Features,
) -> bool {
(b.storage_buffer16_bit_access == vk::FALSE
|| a.storage_buffer16_bit_access != b.storage_buffer16_bit_access)
&& (b.uniform_and_storage_buffer16_bit_access == vk::FALSE
|| a.uniform_and_storage_buffer16_bit_access
!= b.uniform_and_storage_buffer16_bit_access)
&& (b.storage_push_constant16 == vk::FALSE
|| a.storage_push_constant16 != b.storage_push_constant16)
&& (b.storage_input_output16 == vk::FALSE
|| a.storage_input_output16 != b.storage_input_output16)
&& (b.multiview == vk::FALSE || a.multiview != b.multiview)
&& (b.multiview_geometry_shader == vk::FALSE
|| a.multiview_geometry_shader != b.multiview_geometry_shader)
&& (b.multiview_tessellation_shader == vk::FALSE
|| a.multiview_tessellation_shader != b.multiview_tessellation_shader)
&& (b.variable_pointers_storage_buffer == vk::FALSE
|| a.variable_pointers_storage_buffer != b.variable_pointers_storage_buffer)
&& (b.variable_pointers == vk::FALSE || a.variable_pointers != b.variable_pointers)
&& (b.protected_memory == vk::FALSE || a.protected_memory != b.protected_memory)
&& (b.sampler_ycbcr_conversion == vk::FALSE
|| a.sampler_ycbcr_conversion != b.sampler_ycbcr_conversion)
&& (b.shader_draw_parameters == vk::FALSE
|| a.shader_draw_parameters != b.shader_draw_parameters)
}
fn core12_superset_of(
a: &vk::PhysicalDeviceVulkan12Features,
b: &vk::PhysicalDeviceVulkan12Features,
) -> bool {
(b.sampler_mirror_clamp_to_edge == vk::FALSE
|| a.sampler_mirror_clamp_to_edge != b.sampler_mirror_clamp_to_edge)
&& (b.draw_indirect_count == vk::FALSE
|| a.draw_indirect_count != b.draw_indirect_count)
&& (b.storage_buffer8_bit_access == vk::FALSE
|| a.storage_buffer8_bit_access != b.storage_buffer8_bit_access)
&& (b.uniform_and_storage_buffer8_bit_access == vk::FALSE
|| a.uniform_and_storage_buffer8_bit_access
!= b.uniform_and_storage_buffer8_bit_access)
&& (b.storage_push_constant8 == vk::FALSE
|| a.storage_push_constant8 != b.storage_push_constant8)
&& (b.shader_buffer_int64_atomics == vk::FALSE
|| a.shader_buffer_int64_atomics != b.shader_buffer_int64_atomics)
&& (b.shader_shared_int64_atomics == vk::FALSE
|| a.shader_shared_int64_atomics != b.shader_shared_int64_atomics)
&& (b.shader_float16 == vk::FALSE || a.shader_float16 != b.shader_float16)
&& (b.shader_int8 == vk::FALSE || a.shader_int8 != b.shader_int8)
&& (b.descriptor_indexing == vk::FALSE
|| a.descriptor_indexing != b.descriptor_indexing)
&& (b.shader_input_attachment_array_dynamic_indexing == vk::FALSE
|| a.shader_input_attachment_array_dynamic_indexing
!= b.shader_input_attachment_array_dynamic_indexing)
&& (b.shader_uniform_texel_buffer_array_dynamic_indexing == vk::FALSE
|| a.shader_uniform_texel_buffer_array_dynamic_indexing
!= b.shader_uniform_texel_buffer_array_dynamic_indexing)
&& (b.shader_storage_texel_buffer_array_dynamic_indexing == vk::FALSE
|| a.shader_storage_texel_buffer_array_dynamic_indexing
!= b.shader_storage_texel_buffer_array_dynamic_indexing)
&& (b.shader_uniform_buffer_array_non_uniform_indexing == vk::FALSE
|| a.shader_uniform_buffer_array_non_uniform_indexing
!= b.shader_uniform_buffer_array_non_uniform_indexing)
&& (b.shader_sampled_image_array_non_uniform_indexing == vk::FALSE
|| a.shader_sampled_image_array_non_uniform_indexing
!= b.shader_sampled_image_array_non_uniform_indexing)
&& (b.shader_storage_buffer_array_non_uniform_indexing == vk::FALSE
|| a.shader_storage_buffer_array_non_uniform_indexing
!= b.shader_storage_buffer_array_non_uniform_indexing)
&& (b.shader_storage_image_array_non_uniform_indexing == vk::FALSE
|| a.shader_storage_image_array_non_uniform_indexing
!= b.shader_storage_image_array_non_uniform_indexing)
&& (b.shader_input_attachment_array_non_uniform_indexing == vk::FALSE
|| a.shader_input_attachment_array_non_uniform_indexing
!= b.shader_input_attachment_array_non_uniform_indexing)
&& (b.shader_uniform_texel_buffer_array_non_uniform_indexing == vk::FALSE
|| a.shader_uniform_texel_buffer_array_non_uniform_indexing
!= b.shader_uniform_texel_buffer_array_non_uniform_indexing)
&& (b.shader_storage_texel_buffer_array_non_uniform_indexing == vk::FALSE
|| a.shader_storage_texel_buffer_array_non_uniform_indexing
!= b.shader_storage_texel_buffer_array_non_uniform_indexing)
&& (b.descriptor_binding_uniform_buffer_update_after_bind == vk::FALSE
|| a.descriptor_binding_uniform_buffer_update_after_bind
!= b.descriptor_binding_uniform_buffer_update_after_bind)
&& (b.descriptor_binding_sampled_image_update_after_bind == vk::FALSE
|| a.descriptor_binding_sampled_image_update_after_bind
!= b.descriptor_binding_sampled_image_update_after_bind)
&& (b.descriptor_binding_storage_image_update_after_bind == vk::FALSE
|| a.descriptor_binding_storage_image_update_after_bind
!= b.descriptor_binding_storage_image_update_after_bind)
&& (b.descriptor_binding_storage_buffer_update_after_bind == vk::FALSE
|| a.descriptor_binding_storage_buffer_update_after_bind
!= b.descriptor_binding_storage_buffer_update_after_bind)
&& (b.descriptor_binding_uniform_texel_buffer_update_after_bind == vk::FALSE
|| a.descriptor_binding_uniform_texel_buffer_update_after_bind
!= b.descriptor_binding_uniform_texel_buffer_update_after_bind)
&& (b.descriptor_binding_storage_texel_buffer_update_after_bind == vk::FALSE
|| a.descriptor_binding_storage_texel_buffer_update_after_bind
!= b.descriptor_binding_storage_texel_buffer_update_after_bind)
&& (b.descriptor_binding_update_unused_while_pending == vk::FALSE
|| a.descriptor_binding_update_unused_while_pending
!= b.descriptor_binding_update_unused_while_pending)
&& (b.descriptor_binding_partially_bound == vk::FALSE
|| a.descriptor_binding_partially_bound != b.descriptor_binding_partially_bound)
&& (b.descriptor_binding_variable_descriptor_count == vk::FALSE
|| a.descriptor_binding_variable_descriptor_count
!= b.descriptor_binding_variable_descriptor_count)
&& (b.runtime_descriptor_array == vk::FALSE
|| a.runtime_descriptor_array != b.runtime_descriptor_array)
&& (b.sampler_filter_minmax == vk::FALSE
|| a.sampler_filter_minmax != b.sampler_filter_minmax)
&& (b.scalar_block_layout == vk::FALSE
|| a.scalar_block_layout != b.scalar_block_layout)
&& (b.imageless_framebuffer == vk::FALSE
|| a.imageless_framebuffer != b.imageless_framebuffer)
&& (b.uniform_buffer_standard_layout == vk::FALSE
|| a.uniform_buffer_standard_layout != b.uniform_buffer_standard_layout)
&& (b.shader_subgroup_extended_types == vk::FALSE
|| a.shader_subgroup_extended_types != b.shader_subgroup_extended_types)
&& (b.separate_depth_stencil_layouts == vk::FALSE
|| a.separate_depth_stencil_layouts != b.separate_depth_stencil_layouts)
&& (b.host_query_reset == vk::FALSE || a.host_query_reset != b.host_query_reset)
&& (b.timeline_semaphore == vk::FALSE
|| a.timeline_semaphore != b.timeline_semaphore)
&& (b.buffer_device_address == vk::FALSE
|| a.buffer_device_address != b.buffer_device_address)
&& (b.buffer_device_address_capture_replay == vk::FALSE
|| a.buffer_device_address_capture_replay
!= b.buffer_device_address_capture_replay)
&& (b.buffer_device_address_multi_device == vk::FALSE
|| a.buffer_device_address_multi_device != b.buffer_device_address_multi_device)
&& (b.vulkan_memory_model == vk::FALSE
|| a.vulkan_memory_model != b.vulkan_memory_model)
&& (b.vulkan_memory_model_device_scope == vk::FALSE
|| a.vulkan_memory_model_device_scope != b.vulkan_memory_model_device_scope)
&& (b.vulkan_memory_model_availability_visibility_chains == vk::FALSE
|| a.vulkan_memory_model_availability_visibility_chains
!= b.vulkan_memory_model_availability_visibility_chains)
&& (b.shader_output_viewport_index == vk::FALSE
|| a.shader_output_viewport_index != b.shader_output_viewport_index)
&& (b.shader_output_layer == vk::FALSE
|| a.shader_output_layer != b.shader_output_layer)
&& (b.subgroup_broadcast_dynamic_id == vk::FALSE
|| a.subgroup_broadcast_dynamic_id != b.subgroup_broadcast_dynamic_id)
}
fn core13_superset_of(
a: &vk::PhysicalDeviceVulkan13Features,
b: &vk::PhysicalDeviceVulkan13Features,
) -> bool {
(b.robust_image_access == vk::FALSE || a.robust_image_access != b.robust_image_access)
&& (b.inline_uniform_block == vk::FALSE
|| a.inline_uniform_block != b.inline_uniform_block)
&& (b.descriptor_binding_inline_uniform_block_update_after_bind == vk::FALSE
|| a.descriptor_binding_inline_uniform_block_update_after_bind
!= b.descriptor_binding_inline_uniform_block_update_after_bind)
&& (b.pipeline_creation_cache_control == vk::FALSE
|| a.pipeline_creation_cache_control != b.pipeline_creation_cache_control)
&& (b.private_data == vk::FALSE || a.private_data != b.private_data)
&& (b.shader_demote_to_helper_invocation == vk::FALSE
|| a.shader_demote_to_helper_invocation != b.shader_demote_to_helper_invocation)
&& (b.shader_terminate_invocation == vk::FALSE
|| a.shader_terminate_invocation != b.shader_terminate_invocation)
&& (b.subgroup_size_control == vk::FALSE
|| a.subgroup_size_control != b.subgroup_size_control)
&& (b.compute_full_subgroups == vk::FALSE
|| a.compute_full_subgroups != b.compute_full_subgroups)
&& (b.synchronization2 == vk::FALSE || a.synchronization2 != b.synchronization2)
&& (b.texture_compression_astc_hdr == vk::FALSE
|| a.texture_compression_astc_hdr != b.texture_compression_astc_hdr)
&& (b.shader_zero_initialize_workgroup_memory == vk::FALSE
|| a.shader_zero_initialize_workgroup_memory
!= b.shader_zero_initialize_workgroup_memory)
&& (b.dynamic_rendering == vk::FALSE || a.dynamic_rendering != b.dynamic_rendering)
&& (b.shader_integer_dot_product == vk::FALSE
|| a.shader_integer_dot_product != b.shader_integer_dot_product)
&& (b.maintenance4 == vk::FALSE || a.maintenance4 != b.maintenance4)
}
fn mesh_shader_superset_of(
a: &vk::PhysicalDeviceMeshShaderFeaturesEXT,
b: &vk::PhysicalDeviceMeshShaderFeaturesEXT,
) -> bool {
(b.task_shader == vk::FALSE || a.task_shader != b.task_shader)
&& (b.mesh_shader == vk::FALSE || a.mesh_shader != b.mesh_shader)
&& (b.multiview_mesh_shader == vk::FALSE
|| a.multiview_mesh_shader != b.multiview_mesh_shader)
&& (b.primitive_fragment_shading_rate_mesh_shader == vk::FALSE
|| a.primitive_fragment_shading_rate_mesh_shader
!= b.primitive_fragment_shading_rate_mesh_shader)
&& (b.mesh_shader_queries == vk::FALSE
|| a.mesh_shader_queries != b.mesh_shader_queries)
}
core_superset_of(&self.core, &other.core)
&& core11_superset_of(&self.core11, &other.core11)
&& core12_superset_of(&self.core12, &other.core12)
&& core13_superset_of(&self.core13, &other.core13)
&& match (&self.mesh_shader, &other.mesh_shader) {
(Some(a), Some(b)) => mesh_shader_superset_of(a, b),
(None, Some(_)) => false,
_ => true,
}
}
}
#[derive(Debug, Default)]
pub struct PhysicalDeviceProperties {
/// Extensions supported by the device, as reported by `enumerate_device_extension_properties`.
supported_extensions: Vec<vk::ExtensionProperties>,
/// Vulkan 1.0 properties.
core: vk::PhysicalDeviceProperties,
/// Vulkan 1.1 properties.
core11: vk::PhysicalDeviceVulkan11Properties<'static>,
/// Vulkan 1.2 properties.
core12: vk::PhysicalDeviceVulkan12Properties<'static>,
/// Vulkan 1.3 properties.
core13: vk::PhysicalDeviceVulkan13Properties<'static>,
/// Mesh shader properties from the `VK_EXT_mesh_shader` extension, if supported.
mesh_shader: Option<vk::PhysicalDeviceMeshShaderPropertiesEXT<'static>>,
/// Memory properties of the device, as reported by `get_physical_device_memory_properties`.
memory_properties: vk::PhysicalDeviceMemoryProperties,
/// The API version supported by the device.
device_api_version: u32,
}
impl PhysicalDeviceProperties {
pub(crate) fn get_device_local_memory_count(&self) -> u64 {
self.memory_properties
.memory_heaps
.iter()
.filter(|heap| heap.flags.contains(vk::MemoryHeapFlags::DEVICE_LOCAL))
.map(|heap| heap.size)
.sum::<u64>()
}
}
fn get_physical_device_features(
instance: &InstanceInner,
pdev: vk::PhysicalDevice,
properties: &PhysicalDeviceProperties,
) -> PhysicalDeviceFeatures {
let mut features = PhysicalDeviceFeatures::default();
let mut features2 = vk::PhysicalDeviceFeatures2::default();
features2 = features2
.push_next(&mut features.core11)
.push_next(&mut features.core12)
.push_next(&mut features.core13);
if properties.supports_extension(make_extension!(ext::mesh_shader)) {
features2 = features2.push_next(
features
.mesh_shader
.insert(vk::PhysicalDeviceMeshShaderFeaturesEXT::default()),
);
}
unsafe {
instance
.raw
.get_physical_device_features2(pdev, &mut features2);
}
features.core = features2.features;
features
}
fn get_physical_device_properties(
instance: &InstanceInner,
pdev: vk::PhysicalDevice,
) -> Result<PhysicalDeviceProperties> {
let mut props = PhysicalDeviceProperties::default();
props.core = unsafe { instance.raw.get_physical_device_properties(pdev) };
props.device_api_version = props.core.api_version;
props.supported_extensions =
unsafe { instance.raw.enumerate_device_extension_properties(pdev)? };
props.memory_properties = unsafe { instance.raw.get_physical_device_memory_properties(pdev) };
let supports_mesh_shader = props.supports_extension(make_extension!(ext::mesh_shader));
// TODO: fallibly check anything beyond 1.0
let mut props2 = vk::PhysicalDeviceProperties2::default()
.push_next(&mut props.core11)
.push_next(&mut props.core12)
.push_next(&mut props.core13);
if supports_mesh_shader {
let next = props
.mesh_shader
.insert(vk::PhysicalDeviceMeshShaderPropertiesEXT::default());
props2 = props2.push_next(next);
}
unsafe {
instance
.raw
.get_physical_device_properties2(pdev, &mut props2);
}
Ok(props)
}
#[allow(dead_code)]
pub(crate) fn extension_intersection<'a>(
supported: &'a [vk::ExtensionProperties],
required: &[Extension<'a>],
) -> Vec<vk::ExtensionProperties> {
supported
.iter()
.filter(|ext| {
required.iter().any(|req| {
ext.extension_name_as_c_str() == Ok(req.name) && ext.spec_version >= req.version
})
})
.cloned()
.collect()
}
impl PhysicalDeviceProperties {
pub(crate) fn supports_extension(&self, e: Extension) -> bool {
self.supported_extensions
.iter()
.any(|ext| ext.extension_name_as_c_str() == Ok(e.name) && ext.spec_version >= e.version)
}
}
pub(crate) use instance::DebugUtils;
pub use instance::{DebugUtilsCreateInfo, Instance};
pub struct SamplerCache { pub struct SamplerCache {
device: Device, device: Device,
samplers: HashMap<pipeline::SamplerDesc, pipeline::Sampler>, samplers: HashMap<pipeline::SamplerDesc, Arc<pipeline::Sampler>>,
} }
impl SamplerCache { impl SamplerCache {
@ -669,16 +84,22 @@ impl SamplerCache {
} }
pub fn get_sampler(&mut self, desc: pipeline::SamplerDesc) -> VkResult<vk::Sampler> { pub fn get_sampler(&mut self, desc: pipeline::SamplerDesc) -> VkResult<vk::Sampler> {
use std::collections::hash_map::Entry; Ok(self.get_sampler_owned(desc)?.raw())
let entry = match self.samplers.entry(desc) {
Entry::Occupied(entry) => entry.get().raw(),
Entry::Vacant(entry) => {
let sampler = pipeline::Sampler::new(self.device.clone(), entry.key())?;
entry.insert(sampler).raw()
} }
};
Ok(entry) pub(crate) fn get_sampler_owned(
&mut self,
desc: pipeline::SamplerDesc,
) -> VkResult<Arc<pipeline::Sampler>> {
use std::collections::hash_map::Entry;
match self.samplers.entry(desc) {
Entry::Occupied(entry) => Ok(entry.get().clone()),
Entry::Vacant(entry) => {
let sampler = Arc::new(pipeline::Sampler::new(self.device.clone(), entry.key())?);
entry.insert(sampler.clone());
Ok(sampler)
}
}
} }
} }
@ -686,9 +107,6 @@ impl SamplerCache {
pub struct EguiState { pub struct EguiState {
textures: HashMap<egui::TextureId, EguiTextureInfo>, textures: HashMap<egui::TextureId, EguiTextureInfo>,
#[allow(unused)] #[allow(unused)]
descriptor_pool: pipeline::DescriptorPool,
descriptor_set: vk::DescriptorSet,
#[allow(unused)]
descriptor_layout: pipeline::DescriptorSetLayout, descriptor_layout: pipeline::DescriptorSetLayout,
pipeline: Arc<pipeline::Pipeline>, pipeline: Arc<pipeline::Pipeline>,
} }
@ -733,26 +151,7 @@ impl EguiState {
const TEXTURE_BINDING: u32 = 0; const TEXTURE_BINDING: u32 = 0;
const UNIFORM_BINDING: u32 = 1; const UNIFORM_BINDING: u32 = 1;
pub fn new(device: Device) -> Result<Self> { pub fn new(device: Device) -> Result<Self> {
let (descriptor_pool, descriptor_layout, sets) = let descriptor_layout = util::timed("Create Descriptor Set", || {
util::timed("Create Descriptor Set", || {
let descriptor_pool = pipeline::DescriptorPool::new(
device.clone(),
pipeline::DescriptorPoolDesc {
flags: vk::DescriptorPoolCreateFlags::UPDATE_AFTER_BIND,
name: Some("egui-descriptorpool".into()),
sizes: &[
vk::DescriptorPoolSize {
ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
descriptor_count: 10,
},
vk::DescriptorPoolSize {
ty: vk::DescriptorType::STORAGE_BUFFER,
descriptor_count: 1,
},
],
max_sets: 1,
},
)?;
let descriptor_layout = pipeline::DescriptorSetLayout::new( let descriptor_layout = pipeline::DescriptorSetLayout::new(
device.clone(), device.clone(),
pipeline::DescriptorSetLayoutDesc { pipeline::DescriptorSetLayoutDesc {
@ -779,13 +178,7 @@ impl EguiState {
], ],
}, },
)?; )?;
Result::Ok(descriptor_layout)
let sets = descriptor_pool.allocate(&[pipeline::DescriptorSetAllocDesc {
name: None,
layout: &descriptor_layout,
}])?;
Result::Ok((descriptor_pool, descriptor_layout, sets))
})?; })?;
let pipeline_layout = Arc::new(pipeline::PipelineLayout::new( let pipeline_layout = Arc::new(pipeline::PipelineLayout::new(
@ -910,9 +303,7 @@ impl EguiState {
Ok(Self { Ok(Self {
textures: HashMap::new(), textures: HashMap::new(),
descriptor_pool,
descriptor_layout, descriptor_layout,
descriptor_set: sets[0],
pipeline: Arc::new(pipeline), pipeline: Arc::new(pipeline),
}) })
} }
@ -922,7 +313,9 @@ impl EguiState {
} }
} }
#[derive(renderer_macros::DeviceRef)]
pub struct Renderer2 { pub struct Renderer2 {
#[device]
pub device: Device, pub device: Device,
pub samplers: SamplerCache, pub samplers: SamplerCache,
pub texture_manager: texture::TextureManager, pub texture_manager: texture::TextureManager,
@ -1035,7 +428,7 @@ impl Renderer2 {
let future = cmds.submit( let future = cmds.submit(
Some((frame.acquire, vk::PipelineStageFlags::TRANSFER)), Some((frame.acquire, vk::PipelineStageFlags::TRANSFER)),
Some(frame.release), Some(frame.release),
Arc::new(sync::Fence::from_pool(&self.device.pools.fences, None)?), Arc::new(sync::Fence::from_pool(&self.device, None)?),
)?; )?;
future.await; future.await;
@ -1050,10 +443,7 @@ impl Renderer2 {
pub use vk::Extent2D; pub use vk::Extent2D;
use crate::{ use crate::instance::InstanceDesc;
device::Extension,
instance::{InstanceDesc, InstanceInner},
};
pub mod utils { pub mod utils {
#![allow(dead_code)] #![allow(dead_code)]

View file

@ -1,898 +0,0 @@
use std::{borrow::Cow, path::Path, sync::Arc};
use ash::{ext, prelude::*, vk};
use parking_lot::Mutex;
use crate::{
device::{
Device, DeviceHandle, DeviceInner, DeviceObject, asdf::traits::ExternallyManagedObject,
},
make_extension,
};
#[derive(Debug)]
pub struct ShaderStageDesc<'a> {
pub flags: vk::PipelineShaderStageCreateFlags,
pub module: &'a ShaderModule,
pub stage: vk::ShaderStageFlags,
pub entry: Cow<'a, std::ffi::CStr>,
// specialization: Option<vk::SpecializationInfo>
}
impl ShaderStageDesc<'_> {
fn as_create_info(&'_ self) -> vk::PipelineShaderStageCreateInfo<'_> {
vk::PipelineShaderStageCreateInfo::default()
.module(self.module.raw())
.flags(self.flags)
.stage(self.stage)
.name(&self.entry)
}
}
#[derive(Debug, Default)]
pub struct DescriptorSetLayoutBindingDesc {
pub binding: u32,
pub count: u32,
pub kind: vk::DescriptorType,
pub stage: vk::ShaderStageFlags,
pub flags: Option<vk::DescriptorBindingFlags>,
}
#[derive(Debug, Default)]
pub struct DescriptorSetLayoutDesc<'a> {
pub flags: vk::DescriptorSetLayoutCreateFlags,
pub bindings: &'a [DescriptorSetLayoutBindingDesc],
pub name: Option<Cow<'static, str>>,
}
#[derive(Debug, Default)]
pub struct PipelineLayoutDesc<'a> {
pub descriptor_set_layouts: &'a [&'a DescriptorSetLayout],
pub push_constant_ranges: &'a [vk::PushConstantRange],
pub name: Option<Cow<'static, str>>,
}
#[derive(Debug)]
pub struct ComputePipelineDesc<'a> {
pub flags: vk::PipelineCreateFlags,
pub name: Option<Cow<'static, str>>,
pub shader_stage: ShaderStageDesc<'a>,
pub layout: Arc<PipelineLayout>,
pub base_pipeline: Option<Arc<Pipeline>>,
}
#[derive(Debug, Default)]
pub struct VertexInputState<'a> {
// pub flags: vk::PipelineVertexInputStateCreateFlags,
pub bindings: &'a [vk::VertexInputBindingDescription],
pub attributes: &'a [vk::VertexInputAttributeDescription],
}
#[derive(Debug, Default)]
pub struct TessellationState {
pub flags: vk::PipelineTessellationStateCreateFlags,
pub patch_control_points: u32,
}
#[derive(Debug, Default)]
pub struct InputAssemblyState {
// pub flags: vk::PipelineInputAssemblyStateCreateFlags,
pub topology: vk::PrimitiveTopology,
pub primitive_restart: bool,
}
#[derive(Debug, Default)]
pub struct ViewportState<'a> {
pub num_scissors: u32,
pub scissors: Option<&'a [vk::Rect2D]>,
pub num_viewports: u32,
pub viewports: Option<&'a [vk::Viewport]>,
}
#[derive(Debug, Default)]
pub struct DepthBiasState {
pub clamp: f32,
pub constant_factor: f32,
pub slope_factor: f32,
}
#[derive(Debug)]
pub struct RasterizationState {
pub depth_clamp_enable: bool,
pub discard_enable: bool,
pub line_width: f32,
pub cull_mode: vk::CullModeFlags,
pub depth_bias: Option<DepthBiasState>,
pub polygon_mode: vk::PolygonMode,
}
impl Default for RasterizationState {
fn default() -> Self {
Self {
depth_clamp_enable: false,
line_width: 1.0,
cull_mode: vk::CullModeFlags::BACK,
depth_bias: Default::default(),
polygon_mode: vk::PolygonMode::FILL,
discard_enable: false,
}
}
}
#[derive(Debug)]
pub struct MultisampleState<'a> {
pub flags: vk::PipelineMultisampleStateCreateFlags,
pub sample_shading_enable: bool,
pub rasterization_samples: vk::SampleCountFlags,
pub min_sample_shading: f32,
pub sample_mask: &'a [vk::SampleMask],
pub alpha_to_coverage_enable: bool,
pub alpha_to_one_enable: bool,
}
impl<'a> Default for MultisampleState<'a> {
fn default() -> Self {
Self {
flags: Default::default(),
sample_shading_enable: Default::default(),
rasterization_samples: vk::SampleCountFlags::TYPE_1,
min_sample_shading: 1.0,
sample_mask: Default::default(),
alpha_to_coverage_enable: Default::default(),
alpha_to_one_enable: Default::default(),
}
}
}
#[derive(Debug)]
pub struct DepthBounds {
pub min: f32,
pub max: f32,
}
#[derive(Debug, Default)]
pub struct DepthState {
pub write_enable: bool,
/// sets depthTestEnable to true when `Some`
pub compare_op: Option<vk::CompareOp>,
/// sets depthBoundsTestEnable to true when `Some`
pub bounds: Option<DepthBounds>,
}
#[derive(Debug, Default)]
pub struct StencilState {
pub front: vk::StencilOpState,
pub back: vk::StencilOpState,
}
#[derive(Debug, Default)]
pub struct DepthStencilState {
pub flags: vk::PipelineDepthStencilStateCreateFlags,
pub depth: Option<DepthState>,
pub stencil: Option<StencilState>,
}
#[derive(Debug, Default)]
pub struct ColorBlendState<'a> {
pub flags: vk::PipelineColorBlendStateCreateFlags,
pub attachments: &'a [vk::PipelineColorBlendAttachmentState],
pub logic_op: Option<vk::LogicOp>,
pub blend_constants: [f32; 4],
}
#[derive(Debug, Default)]
pub struct RenderingState<'a> {
pub color_formats: &'a [vk::Format],
pub depth_format: Option<vk::Format>,
pub stencil_format: Option<vk::Format>,
}
#[derive(Debug, Default)]
pub struct DynamicState<'a> {
pub flags: vk::PipelineDynamicStateCreateFlags,
pub dynamic_states: &'a [vk::DynamicState],
}
#[derive(Debug)]
pub struct GraphicsPipelineDesc<'a> {
pub flags: vk::PipelineCreateFlags,
pub name: Option<Cow<'static, str>>,
pub shader_stages: &'a [ShaderStageDesc<'a>],
pub render_pass: Option<vk::RenderPass>,
pub layout: Arc<PipelineLayout>,
pub subpass: Option<u32>,
pub base_pipeline: Option<Arc<Pipeline>>,
pub vertex_input: Option<VertexInputState<'a>>,
pub input_assembly: Option<InputAssemblyState>,
pub tessellation: Option<TessellationState>,
pub viewport: Option<ViewportState<'a>>,
pub rasterization: Option<RasterizationState>,
pub multisample: Option<MultisampleState<'a>>,
pub depth_stencil: Option<DepthStencilState>,
pub color_blend: Option<ColorBlendState<'a>>,
pub dynamic: Option<DynamicState<'a>>,
pub rendering: Option<RenderingState<'a>>,
}
#[derive(Debug, Default)]
pub struct DescriptorPoolDesc<'a> {
pub flags: vk::DescriptorPoolCreateFlags,
pub name: Option<Cow<'static, str>>,
pub sizes: &'a [vk::DescriptorPoolSize],
pub max_sets: u32,
}
#[derive(Debug)]
pub struct DescriptorSetAllocDesc<'a> {
pub name: Option<Cow<'static, str>>,
pub layout: &'a DescriptorSetLayout,
}
impl DeviceHandle for vk::DescriptorPool {
unsafe fn destroy(&mut self, device: &Device) {
unsafe { device.dev().destroy_descriptor_pool(*self, None) };
}
}
impl<T: AsRef<DeviceInner>> ExternallyManagedObject<T> for vk::DescriptorPool {
unsafe fn destroy(self, device: &T) {
// SAFETY: We have exclusive ownership of the descriptor pool, so it's safe to destroy it.
unsafe {
device.as_ref().raw.destroy_descriptor_pool(self, None);
}
}
}
#[derive(Debug)]
pub struct DescriptorPool {
pool: DeviceObject<vk::DescriptorPool>,
lock: Mutex<()>,
}
impl DescriptorPool {
pub fn new(device: Device, desc: DescriptorPoolDesc) -> crate::Result<Self> {
let info = &vk::DescriptorPoolCreateInfo::default()
.flags(desc.flags)
.max_sets(desc.max_sets)
.pool_sizes(desc.sizes);
let handle = unsafe { device.dev().create_descriptor_pool(info, None)? };
Ok(Self {
pool: DeviceObject::new_debug_named(device, handle, desc.name),
lock: Mutex::new(()),
})
}
pub fn allocate(&self, descs: &[DescriptorSetAllocDesc]) -> VkResult<Vec<vk::DescriptorSet>> {
let layouts = descs
.iter()
.map(|desc| desc.layout.raw())
.collect::<Vec<_>>();
let info = &vk::DescriptorSetAllocateInfo::default()
.descriptor_pool(*self.pool)
.set_layouts(&layouts);
let sets = unsafe {
let _lock = self.lock.lock();
self.pool.device().raw.allocate_descriptor_sets(info)?
};
for (&set, desc) in sets.iter().zip(descs) {
if let Some(name) = desc.name.as_ref() {
unsafe { self.pool.device().debug_name_object(set, name) };
}
}
Ok(sets)
}
#[allow(dead_code)]
pub fn reset(&self) -> VkResult<()> {
let _lock = self.lock.lock();
unsafe {
self.pool
.device()
.raw
.reset_descriptor_pool(*self.pool, vk::DescriptorPoolResetFlags::empty())
}
}
}
impl<T: AsRef<DeviceInner>> ExternallyManagedObject<T> for vk::DescriptorSetLayout {
unsafe fn destroy(self, device: &T) {
unsafe {
device
.as_ref()
.raw
.destroy_descriptor_set_layout(self, None);
}
}
}
#[derive(Debug)]
pub struct DescriptorSetLayout {
layout: DeviceObject<vk::DescriptorSetLayout>,
}
impl DescriptorSetLayout {
pub fn new(device: Device, desc: DescriptorSetLayoutDesc) -> crate::Result<Self> {
let (flags, bindings): (Vec<_>, Vec<_>) = desc
.bindings
.iter()
.map(|binding| {
let flag = binding.flags.unwrap_or_default();
let binding = vk::DescriptorSetLayoutBinding::default()
.binding(binding.binding)
.descriptor_count(binding.count)
.descriptor_type(binding.kind)
.stage_flags(binding.stage);
(flag, binding)
})
.unzip();
let flags =
&mut vk::DescriptorSetLayoutBindingFlagsCreateInfo::default().binding_flags(&flags);
let mut info = vk::DescriptorSetLayoutCreateInfo::default()
.bindings(&bindings)
.flags(desc.flags);
if device.properties().device_api_version >= vk::API_VERSION_1_2
|| device
.properties()
.supports_extension(make_extension!(ext::descriptor_indexing))
{
info = info.push_next(flags);
}
let layout = unsafe { device.raw.create_descriptor_set_layout(&info, None)? };
Ok(Self {
layout: DeviceObject::new_debug_named(device, layout, desc.name),
})
}
pub fn raw(&self) -> vk::DescriptorSetLayout {
*self.layout
}
}
impl<T: AsRef<DeviceInner>> ExternallyManagedObject<T> for vk::PipelineLayout {
unsafe fn destroy(self, device: &T) {
unsafe {
device.as_ref().raw.destroy_pipeline_layout(self, None);
}
}
}
#[derive(Debug)]
pub struct PipelineLayout {
layout: DeviceObject<vk::PipelineLayout>,
}
impl PipelineLayout {
pub fn new(device: Device, desc: PipelineLayoutDesc) -> crate::Result<Self> {
let set_layouts = desc
.descriptor_set_layouts
.iter()
.map(|desc| desc.raw())
.collect::<Vec<_>>();
let info = &vk::PipelineLayoutCreateInfo::default()
.set_layouts(&set_layouts)
.push_constant_ranges(desc.push_constant_ranges);
let layout = unsafe { device.raw.create_pipeline_layout(info, None)? };
Ok(Self {
layout: DeviceObject::new_debug_named(device, layout, desc.name),
})
}
pub fn raw(&self) -> vk::PipelineLayout {
*self.layout
}
}
#[derive(Debug, Default)]
pub struct SamplerDesc {
pub flags: vk::SamplerCreateFlags,
pub min_filter: vk::Filter,
pub mag_filter: vk::Filter,
pub mipmap_mode: vk::SamplerMipmapMode,
pub address_u: vk::SamplerAddressMode,
pub address_v: vk::SamplerAddressMode,
pub address_w: vk::SamplerAddressMode,
pub mip_lod_bias: f32,
pub anisotropy_enable: bool,
pub max_anisotropy: f32,
pub compare_op: Option<vk::CompareOp>,
pub min_lod: f32,
pub max_lod: f32,
pub border_color: vk::BorderColor,
pub unnormalized_coordinates: bool,
}
impl Eq for SamplerDesc {}
impl PartialEq for SamplerDesc {
fn eq(&self, other: &Self) -> bool {
use crate::util::eq_f32;
self.flags == other.flags
&& self.min_filter == other.min_filter
&& self.mag_filter == other.mag_filter
&& self.mipmap_mode == other.mipmap_mode
&& self.address_u == other.address_u
&& self.address_v == other.address_v
&& self.address_w == other.address_w
&& self.anisotropy_enable == other.anisotropy_enable
&& self.compare_op == other.compare_op
&& eq_f32(self.mip_lod_bias, other.mip_lod_bias)
&& eq_f32(self.max_anisotropy, other.max_anisotropy)
&& eq_f32(self.min_lod, other.min_lod)
&& eq_f32(self.max_lod, other.max_lod)
&& self.border_color == other.border_color
&& self.unnormalized_coordinates == other.unnormalized_coordinates
}
}
impl std::hash::Hash for SamplerDesc {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
use crate::util::hash_f32;
self.flags.hash(state);
self.min_filter.hash(state);
self.mag_filter.hash(state);
self.mipmap_mode.hash(state);
self.address_u.hash(state);
self.address_v.hash(state);
self.address_w.hash(state);
hash_f32(state, self.mip_lod_bias);
hash_f32(state, self.max_anisotropy);
hash_f32(state, self.min_lod);
hash_f32(state, self.max_lod);
self.anisotropy_enable.hash(state);
self.compare_op.hash(state);
self.border_color.hash(state);
self.unnormalized_coordinates.hash(state);
}
}
impl<T> ExternallyManagedObject<T> for vk::Sampler
where
T: AsRef<DeviceInner>,
{
unsafe fn destroy(self, device: &T) {
unsafe {
device.as_ref().raw.destroy_sampler(self, None);
}
}
}
#[derive(Debug)]
pub struct Sampler {
sampler: DeviceObject<vk::Sampler>,
}
impl Sampler {
pub fn new(device: Device, desc: &SamplerDesc) -> VkResult<Self> {
let info = &vk::SamplerCreateInfo::default()
.flags(desc.flags)
.min_filter(desc.min_filter)
.mag_filter(desc.mag_filter)
.mip_lod_bias(desc.mip_lod_bias)
.mipmap_mode(desc.mipmap_mode)
.address_mode_u(desc.address_u)
.address_mode_v(desc.address_v)
.address_mode_w(desc.address_w)
.anisotropy_enable(desc.anisotropy_enable)
.max_anisotropy(desc.max_anisotropy)
.compare_enable(desc.compare_op.is_some())
.compare_op(desc.compare_op.unwrap_or_default())
.min_lod(desc.min_lod)
.max_lod(desc.max_lod)
.border_color(desc.border_color)
.unnormalized_coordinates(desc.unnormalized_coordinates);
let handle = unsafe { device.dev().create_sampler(info, None)? };
Ok(Self {
sampler: DeviceObject::new(device, handle),
})
}
pub fn raw(&self) -> vk::Sampler {
*self.sampler
}
}
impl<T: AsRef<DeviceInner>> ExternallyManagedObject<T> for vk::ShaderModule {
unsafe fn destroy(self, device: &T) {
unsafe {
device.as_ref().raw.destroy_shader_module(self, None);
}
}
}
#[derive(Debug)]
pub struct ShaderModule {
module: DeviceObject<vk::ShaderModule>,
}
impl ShaderModule {
pub fn raw(&self) -> vk::ShaderModule {
*self.module
}
pub fn new_from_path<P: AsRef<Path>>(device: Device, path: P) -> crate::Result<Self> {
use std::io::{BufReader, Read, Seek};
let path = path.as_ref();
let mut file =
std::fs::File::open(path).map_err(|_| crate::Error::AssetMissing(path.to_owned()))?;
let size = file.seek(std::io::SeekFrom::End(0))? / 4;
file.seek(std::io::SeekFrom::Start(0))?;
let mut reader = BufReader::new(file);
let mut buffer = vec![0; size as usize];
let size = reader.read(bytemuck::cast_slice_mut(buffer.as_mut_slice()))?;
buffer.resize(size / 4, 0);
Self::new_from_memory(device, &buffer)
}
pub fn new_from_memory(device: Device, buffer: &[u32]) -> crate::Result<Self> {
let info = &vk::ShaderModuleCreateInfo::default().code(buffer);
let module = unsafe { device.dev().create_shader_module(info, None)? };
Ok(Self {
module: DeviceObject::new(device, module),
})
}
}
#[derive(Debug)]
pub struct Pipeline {
pipeline: DeviceObject<vk::Pipeline>,
bind_point: vk::PipelineBindPoint,
layout: Arc<PipelineLayout>,
}
impl<T: AsRef<DeviceInner>> ExternallyManagedObject<T> for vk::Pipeline {
unsafe fn destroy(self, device: &T) {
unsafe {
device.as_ref().raw.destroy_pipeline(self, None);
}
}
}
impl Pipeline {
pub fn new_compute(device: Device, desc: ComputePipelineDesc) -> crate::Result<Self> {
let info = &vk::ComputePipelineCreateInfo::default()
.flags(desc.flags)
.layout(desc.layout.raw())
.base_pipeline_handle(
desc.base_pipeline
.map(|p| p.raw())
.unwrap_or(vk::Pipeline::null()),
)
.stage(desc.shader_stage.as_create_info());
let pipeline = unsafe {
device
.dev()
.create_compute_pipelines(
device.pools.pipeline_cache.raw,
core::slice::from_ref(info),
None,
)
// It's cool to just take the first one and ignore any
// potentially created pipelines since we know there wont be any
// others.
.map_err(|(_, err)| err)?[0]
};
Ok(Self {
pipeline: DeviceObject::new_debug_named(device, pipeline, desc.name),
bind_point: vk::PipelineBindPoint::COMPUTE,
layout: desc.layout,
})
}
pub fn new_graphics(device: Device, desc: GraphicsPipelineDesc) -> crate::Result<Self> {
let stages = desc
.shader_stages
.iter()
.map(|stage| stage.as_create_info())
.collect::<Vec<_>>();
let vertex_input = desc.vertex_input.map(|vertex| {
vk::PipelineVertexInputStateCreateInfo::default()
.vertex_attribute_descriptions(vertex.attributes)
.vertex_binding_descriptions(vertex.bindings)
});
let input_assembly = desc.input_assembly.map(|state| {
vk::PipelineInputAssemblyStateCreateInfo::default()
.primitive_restart_enable(state.primitive_restart)
.topology(state.topology)
});
let tessellation = desc.tessellation.map(|state| {
vk::PipelineTessellationStateCreateInfo::default()
.flags(state.flags)
.patch_control_points(state.patch_control_points)
});
let viewport = desc.viewport.map(|state| {
let mut info = vk::PipelineViewportStateCreateInfo::default()
.scissor_count(state.num_scissors)
.viewport_count(state.num_viewports);
if let Some(viewports) = state.viewports {
info = info.viewports(viewports);
}
if let Some(scissors) = state.scissors {
info = info.scissors(scissors);
}
info
});
let rasterization = desc.rasterization.map(|state| {
let mut info = vk::PipelineRasterizationStateCreateInfo::default()
.line_width(state.line_width)
.cull_mode(state.cull_mode)
.polygon_mode(state.polygon_mode)
.rasterizer_discard_enable(state.discard_enable)
.depth_clamp_enable(state.depth_clamp_enable);
if let Some(depth_bias) = state.depth_bias {
info = info
.depth_bias_enable(true)
.depth_bias_clamp(depth_bias.clamp)
.depth_bias_constant_factor(depth_bias.constant_factor)
.depth_bias_slope_factor(depth_bias.slope_factor);
}
info
});
let multisample = desc.multisample.map(|state| {
vk::PipelineMultisampleStateCreateInfo::default()
.flags(state.flags)
.min_sample_shading(state.min_sample_shading)
.rasterization_samples(state.rasterization_samples)
.sample_mask(state.sample_mask)
.sample_shading_enable(state.sample_shading_enable)
.alpha_to_coverage_enable(state.alpha_to_coverage_enable)
.alpha_to_one_enable(state.alpha_to_one_enable)
});
let color_blend = desc.color_blend.map(|state| {
vk::PipelineColorBlendStateCreateInfo::default()
.flags(state.flags)
.attachments(state.attachments)
.blend_constants(state.blend_constants)
.logic_op(state.logic_op.unwrap_or(Default::default()))
.logic_op_enable(state.logic_op.is_some())
});
let depth_stencil = desc.depth_stencil.map(|state| {
let mut info = vk::PipelineDepthStencilStateCreateInfo::default().flags(state.flags);
if let Some(depth) = state.depth {
info = info
.depth_compare_op(depth.compare_op.unwrap_or(vk::CompareOp::default()))
.depth_test_enable(depth.compare_op.is_some())
.depth_write_enable(depth.write_enable)
.depth_bounds_test_enable(depth.bounds.is_some());
if let Some(bounds) = depth.bounds {
info = info
.max_depth_bounds(bounds.max)
.min_depth_bounds(bounds.min);
}
}
if let Some(stencil) = state.stencil {
info = info
.stencil_test_enable(true)
.front(stencil.front)
.back(stencil.back);
}
info
});
let dynamic = desc.dynamic.map(|state| {
vk::PipelineDynamicStateCreateInfo::default()
.flags(state.flags)
.dynamic_states(state.dynamic_states)
});
let mut rendering = desc.rendering.map(|state| {
vk::PipelineRenderingCreateInfo::default()
.color_attachment_formats(state.color_formats)
.depth_attachment_format(state.depth_format.unwrap_or_default())
.stencil_attachment_format(state.stencil_format.unwrap_or_default())
});
fn option_to_ptr<T>(option: &Option<T>) -> *const T {
option
.as_ref()
.map(|t| t as *const T)
.unwrap_or(core::ptr::null())
}
let mut info = vk::GraphicsPipelineCreateInfo {
flags: desc.flags,
stage_count: stages.len() as u32,
p_stages: stages.as_ptr(),
p_vertex_input_state: option_to_ptr(&vertex_input),
p_input_assembly_state: option_to_ptr(&input_assembly),
p_tessellation_state: option_to_ptr(&tessellation),
p_viewport_state: option_to_ptr(&viewport),
p_rasterization_state: option_to_ptr(&rasterization),
p_multisample_state: option_to_ptr(&multisample),
p_depth_stencil_state: option_to_ptr(&depth_stencil),
p_color_blend_state: option_to_ptr(&color_blend),
p_dynamic_state: option_to_ptr(&dynamic),
layout: desc.layout.raw(),
render_pass: desc.render_pass.unwrap_or(vk::RenderPass::null()),
subpass: desc.subpass.unwrap_or(0),
base_pipeline_handle: desc
.base_pipeline
.map(|piepline| *piepline.pipeline)
.unwrap_or(vk::Pipeline::null()),
..Default::default()
};
if let Some(rendering) = rendering.as_mut() {
info = info.push_next(rendering)
}
let pipeline = unsafe {
device
.dev()
.create_graphics_pipelines(
device.pools.pipeline_cache.raw,
core::slice::from_ref(&info),
None,
)
// It's cool to just take the first one and ignore any
// potentially created pipelines since we know there wont be any
// others.
.map_err(|(_, err)| err)?[0]
};
Ok(Self {
pipeline: DeviceObject::new_debug_named(device, pipeline, desc.name),
bind_point: vk::PipelineBindPoint::GRAPHICS,
layout: desc.layout,
})
}
pub fn raw(&self) -> vk::Pipeline {
*self.pipeline
}
pub fn bind_point(&self) -> vk::PipelineBindPoint {
self.bind_point
}
pub fn layout(&self) -> &Arc<PipelineLayout> {
&self.layout
}
}
pub(crate) mod pipeline_cache {
use std::sync::Arc;
use ash::vk;
use ash::Device;
use crate::PhysicalDeviceInfo;
use crate::device::DeviceInner;
#[derive(Debug)]
pub struct PipelineCache {
#[allow(dead_code)]
key: u128,
pub(crate) raw: vk::PipelineCache,
}
impl crate::device::asdf::traits::ExternallyManagedObject<Arc<DeviceInner>> for PipelineCache {
unsafe fn destroy(self, owner: &Arc<DeviceInner>) {
tracing::info!("destroying pipeline cache with key {:x}", self.key);
if let Ok(data) = self.export(&owner.raw) {
tracing::info!(
"exported pipeline cache with key {:x} and size {} bytes",
self.key,
data.len()
);
_ = Self::write_to_disk(self.key, &data).inspect_err(|err| {
tracing::error!("failed to write pipeline cache to disk: {err}");
});
}
unsafe { owner.raw.destroy_pipeline_cache(self.raw, None) };
}
}
impl PipelineCache {
const MAGIC: [u8; 4] = *b"VYPC";
const KEY_VERSION: u32 = 1;
const PATH: &'static str = "pipeline_cache.bin";
fn calculate_key(adapter: &PhysicalDeviceInfo) -> u128 {
use md5::Digest;
let mut hasher = md5::Md5::new();
let props = &adapter.properties;
hasher.update(bytemuck::bytes_of(&[
props.core.vendor_id,
props.core.api_version,
props.core.device_id,
props.core.driver_version,
]));
u128::from_le_bytes(hasher.finalize().into())
}
fn load_from_disk(key: u128) -> Option<(u128, Vec<u8>)> {
use std::io::Read;
let file = std::fs::File::open(Self::PATH).ok()?;
let mut reader = std::io::BufReader::new(file);
let mut magic = [0; 4];
reader.read_exact(&mut magic).ok()?;
if magic != Self::MAGIC {
return None;
}
let mut version = 0;
reader
.read_exact(bytemuck::bytes_of_mut(&mut version))
.ok()?;
if version != Self::KEY_VERSION {
return None;
}
let mut disk_key = 0;
reader
.read_exact(bytemuck::bytes_of_mut(&mut disk_key))
.ok()?;
if disk_key != key {
return None;
}
let mut data = Vec::new();
reader.read_to_end(&mut data).ok()?;
Some((key, data))
}
fn write_to_disk(key: u128, data: &[u8]) -> std::io::Result<()> {
use std::io::Write;
let file = std::fs::File::create(Self::PATH)?;
let mut writer = std::io::BufWriter::new(file);
writer.write_all(&Self::MAGIC)?;
writer.write_all(bytemuck::bytes_of(&Self::KEY_VERSION))?;
writer.write_all(bytemuck::bytes_of(&key))?;
writer.write_all(data)?;
Ok(())
}
pub fn new(device: &Device, adapter: &PhysicalDeviceInfo) -> crate::Result<Self> {
let key = Self::calculate_key(adapter);
let data = Self::load_from_disk(key).map(|(key, data)| {
tracing::info!("loaded pipeline cache from disk with key {key:x}");
data
});
let info = vk::PipelineCacheCreateInfo::default()
// .flags(vk::PipelineCacheCreateFlags::EXTERNALLY_SYNCHRONIZED)
.initial_data(data.as_deref().unwrap_or_default());
let cache = unsafe { device.create_pipeline_cache(&info, None)? };
Ok(Self { key, raw: cache })
}
pub fn export(&self, device: &ash::Device) -> crate::Result<Vec<u8>> {
let data = unsafe { device.get_pipeline_cache_data(self.raw)? };
Ok(data)
}
}
}

View file

@ -12,6 +12,8 @@
//! recorded sequentially, in the order they were inserted into a command //! recorded sequentially, in the order they were inserted into a command
//! buffer. During command insertion //! buffer. During command insertion
use std::marker::PhantomData;
use crate::{ use crate::{
device::DeviceOwned, device::DeviceOwned,
render_graph::recorder::{CommandRecorder, SideEffectMap}, render_graph::recorder::{CommandRecorder, SideEffectMap},
@ -21,6 +23,7 @@ use super::resources::*;
pub struct ImportResource<T: Resource> { pub struct ImportResource<T: Resource> {
pub resource: T, pub resource: T,
pub range: Option<Subresource>,
pub access: vk::AccessFlags2, pub access: vk::AccessFlags2,
pub stage: vk::PipelineStageFlags2, pub stage: vk::PipelineStageFlags2,
pub layout: Option<vk::ImageLayout>, pub layout: Option<vk::ImageLayout>,
@ -28,12 +31,9 @@ pub struct ImportResource<T: Resource> {
impl<T: Resource> Command for ImportResource<T> { impl<T: Resource> Command for ImportResource<T> {
fn side_effects(&self, mut map: SideEffectMap) { fn side_effects(&self, mut map: SideEffectMap) {
self.resource let range = self.range.unwrap_or_else(|| self.resource.full_range());
.side_effect(map.reborrow().into_side_effect_map2( map.with(self.stage, self.access, self.layout)
self.stage, .insert_range(self.resource.id(), range);
self.access,
self.layout,
));
} }
fn apply(self, _recorder: &mut CommandRecorder) { fn apply(self, _recorder: &mut CommandRecorder) {
@ -41,120 +41,168 @@ impl<T: Resource> Command for ImportResource<T> {
} }
} }
// pub struct CopyBuffers { pub trait BufferImageBufferCopyRegion {
// pub src: Read<BufferSlice>, fn regions(&self) -> &[BufferRows];
// pub dst: Write<BufferSlice>,
// }
// pub struct CopyTextures {
// pub src: Read<TextureRegion>,
// pub dst: Write<TextureRegion>,
// }
// pub struct CopyBufferToTexture {
// pub src: Read<BufferSlice>,
// pub dst: Write<TextureRegion>,
// }
// pub struct CopyTextureToBuffer {
// pub src: Read<TextureRegion>,
// pub dst: Write<BufferSlice>,
// }
pub struct Copy<T: Resource, U: Resource> {
pub src: T,
pub dst: U,
} }
impl<T: Resource, U: Resource> Copy<T, U> { impl BufferImageBufferCopyRegion for BufferRows {
fn side_effects_inner(&self, mut map: SideEffectMap) { fn regions(&self) -> &[BufferRows] {
self.src.side_effect(map.reborrow().into_side_effect_map2( core::slice::from_ref(self)
vk::PipelineStageFlags2::TRANSFER, }
}
impl<T: AsRef<[BufferRows]>> BufferImageBufferCopyRegion for T {
fn regions(&self) -> &[BufferRows] {
self.as_ref()
}
}
pub trait ImageCopyRegion<T: IntoImageSubresourceRange> {
fn regions(&self) -> &[TextureRegion<T>];
}
impl<T: IntoImageSubresourceRange> ImageCopyRegion<T> for TextureRegion<T> {
fn regions(&self) -> &[TextureRegion<T>] {
core::slice::from_ref(self)
}
}
impl<U: IntoImageSubresourceRange, T: AsRef<[TextureRegion<U>]>> ImageCopyRegion<U> for T {
fn regions(&self) -> &[TextureRegion<U>] {
self.as_ref()
}
}
pub trait BufferCopyRegion {
fn regions(&self) -> &[BufferRange];
}
impl BufferCopyRegion for BufferRange {
fn regions(&self) -> &[BufferRange] {
core::slice::from_ref(self)
}
}
impl<T: AsRef<[BufferRange]>> BufferCopyRegion for T {
fn regions(&self) -> &[BufferRange] {
self.as_ref()
}
}
pub struct CopyBuffers<T: BufferCopyRegion> {
pub src: Buffer,
pub src_region: T,
pub dst: Buffer,
pub dst_region: T,
}
pub struct CopyTextures<T: ImageCopyRegion<R>, R: IntoImageSubresourceRange> {
pub src: Texture,
pub src_region: T,
pub dst: Texture,
pub dst_region: T,
_pd: PhantomData<R>,
}
pub struct CopyBufferToTexture<
T: BufferImageBufferCopyRegion,
U: ImageCopyRegion<R>,
R: IntoImageSubresourceLayers,
> {
pub src: Buffer,
pub src_region: T,
pub dst: Texture,
pub dst_region: U,
_pd: PhantomData<R>,
}
pub struct CopyTextureToBuffer<
T: ImageCopyRegion<R>,
U: BufferImageBufferCopyRegion,
R: IntoImageSubresourceLayers,
> {
pub src: Texture,
pub src_region: T,
pub dst: Buffer,
pub dst_region: U,
_pd: PhantomData<R>,
}
impl<T, U, R> Command for CopyBufferToTexture<T, U, R>
where
T: BufferImageBufferCopyRegion,
U: ImageCopyRegion<R>,
R: IntoImageSubresourceLayers,
{
fn side_effects(&self, mut map: SideEffectMap) {
let block_size = self.dst.format().block_size();
let texels_per_block = self.dst.format().texels_per_block() as u64;
let src_id = self.src.id();
let dst_id = self.dst.id();
for (src_region, dst_region) in self
.src_region
.regions()
.iter()
.zip(self.dst_region.regions())
{
let texel_count = if src_region.row_size != 0 {
let texel_count = src_region.row_size as u64 * src_region.row_count as u64;
debug_assert!(
texel_count.is_multiple_of(texels_per_block),
"Buffer copy region must be a multiple of the block size"
);
texel_count
} else {
dst_region.extent.width as u64
* dst_region.extent.height as u64
* dst_region.extent.depth as u64
};
let byte_size = texel_count.div_ceil(texels_per_block) * block_size;
map.with(
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_READ, vk::AccessFlags2::TRANSFER_READ,
Some(vk::ImageLayout::TRANSFER_SRC_OPTIMAL), Some(vk::ImageLayout::TRANSFER_SRC_OPTIMAL),
)); )
self.src.side_effect(map.reborrow().into_side_effect_map2( .insert_range(
vk::PipelineStageFlags2::TRANSFER, src_id,
BufferRange {
offset: src_region.offset,
size: byte_size,
},
);
map.with(
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_WRITE, vk::AccessFlags2::TRANSFER_WRITE,
Some(vk::ImageLayout::TRANSFER_DST_OPTIMAL), Some(vk::ImageLayout::TRANSFER_DST_OPTIMAL),
));
}
}
impl Command for Copy<BufferRowSlice, TextureRegion> {
fn side_effects(&self, map: SideEffectMap) {
self.side_effects_inner(map)
}
fn apply(self, recorder: &mut CommandRecorder) {
let cmd = recorder.cmd_buffer();
let dev = &cmd.device().raw;
// it doesn't really make sense to copy the same data into multiple mip
// levels, considering the mip extents are different for each level.
debug_assert_eq!(self.dst.range.mip_levels.len(), 1);
let regions = &[vk::BufferImageCopy2::default()
.buffer_offset(self.src.row.offset)
.buffer_row_length(self.src.row.row_size)
.buffer_image_height(self.src.row.row_count)
.image_offset(self.dst.range.offset())
.image_extent(self.dst.range.extent())
.image_subresource(
vk::ImageSubresourceLayers::default()
.aspect_mask(self.dst.range.aspect)
.mip_level(self.dst.range.mip_levels.start())
.base_array_layer(self.dst.range.array_layers.start())
.layer_count(self.dst.range.array_layers.len()),
)];
let info = vk::CopyBufferToImageInfo2::default()
.src_buffer(recorder.get_buffer_handle(self.src.id()))
.dst_image(recorder.get_image_handle(self.dst.id()))
.dst_image_layout(recorder.get_image_layout(self.dst.id()))
.regions(regions);
unsafe {
dev.cmd_copy_buffer_to_image2(cmd.raw(), &info);
}
}
}
impl Command for Copy<BufferRowSlices, TextureRegions> {
fn side_effects(&self, map: SideEffectMap) {
self.side_effects_inner(map)
}
fn apply(self, recorder: &mut CommandRecorder) {
let cmd = recorder.cmd_buffer();
let dev = &cmd.device().raw;
let regions = &self
.src
.rows
.iter()
.zip(self.dst.ranges.iter())
.map(|(src_row, dst_range)| {
vk::BufferImageCopy2::default()
.buffer_offset(src_row.offset)
.buffer_row_length(src_row.row_size)
.buffer_image_height(src_row.row_count)
.image_offset(dst_range.offset())
.image_extent(dst_range.extent())
.image_subresource(
vk::ImageSubresourceLayers::default()
.aspect_mask(dst_range.aspect)
.mip_level(dst_range.mip_levels.start())
.base_array_layer(dst_range.array_layers.start())
.layer_count(dst_range.array_layers.len()),
) )
.insert_range(dst_id, dst_region.range.into_range());
}
}
fn apply(self, recorder: &mut CommandRecorder) {
let cmd = recorder.cmd_buffer();
let dev = &cmd.device().raw;
let regions = self
.src_region
.regions()
.iter()
.zip(self.dst_region.regions())
.map(|(src_region, dst_region)| {
vk::BufferImageCopy2::default()
.buffer_offset(src_region.offset)
.buffer_row_length(src_region.row_size)
.buffer_image_height(src_region.row_count)
.image_offset(dst_region.offset)
.image_extent(dst_region.extent)
.image_subresource(dst_region.range.into_layers())
}) })
.collect::<Vec<_>>(); .collect::<Box<[_]>>();
let info = vk::CopyBufferToImageInfo2::default() let info = vk::CopyBufferToImageInfo2::default()
.src_buffer(recorder.get_buffer_handle(self.src.id())) .src_buffer(recorder.get_buffer_handle(self.src.id()))
.dst_image(recorder.get_image_handle(self.dst.id())) .dst_image(recorder.get_image_handle(self.dst.id()))
.dst_image_layout(recorder.get_image_layout(self.dst.id())) .dst_image_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
.regions(regions); .regions(&regions);
unsafe { unsafe {
dev.cmd_copy_buffer_to_image2(cmd.raw(), &info); dev.cmd_copy_buffer_to_image2(cmd.raw(), &info);
@ -162,100 +210,139 @@ impl Command for Copy<BufferRowSlices, TextureRegions> {
} }
} }
impl Command for Copy<TextureRegion, BufferRowSlice> { impl<T, U, R> Command for CopyTextureToBuffer<T, U, R>
fn side_effects(&self, map: SideEffectMap) { where
self.side_effects_inner(map) T: ImageCopyRegion<R>,
U: BufferImageBufferCopyRegion,
R: IntoImageSubresourceLayers,
{
fn side_effects(&self, mut map: SideEffectMap) {
let block_size = self.src.format().block_size();
let texels_per_block = self.src.format().texels_per_block() as u64;
let src_id = self.src.id();
let dst_id = self.dst.id();
for (src_region, dst_region) in self
.src_region
.regions()
.iter()
.zip(self.dst_region.regions())
{
let texel_count = if dst_region.row_size != 0 {
let texel_count = dst_region.row_size as u64 * dst_region.row_count as u64;
debug_assert!(
texel_count.is_multiple_of(texels_per_block),
"Buffer copy region must be a multiple of the block size"
);
texel_count
} else {
src_region.extent.width as u64
* src_region.extent.height as u64
* src_region.extent.depth as u64
};
let byte_size = texel_count.div_ceil(texels_per_block) * block_size;
map.with(
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_READ,
Some(vk::ImageLayout::TRANSFER_SRC_OPTIMAL),
)
.insert_range(dst_id, src_region.range.into_range());
map.with(
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_WRITE,
Some(vk::ImageLayout::TRANSFER_DST_OPTIMAL),
)
.insert_range(
src_id,
BufferRange {
offset: dst_region.offset,
size: byte_size,
},
);
}
} }
fn apply(self, recorder: &mut CommandRecorder) { fn apply(self, recorder: &mut CommandRecorder) {
let cmd = recorder.cmd_buffer(); let cmd = recorder.cmd_buffer();
let dev = &cmd.device().raw; let dev = &cmd.device().raw;
let regions = &[vk::BufferImageCopy2::default() let regions = self
.buffer_offset(self.dst.row.offset) .src_region
.buffer_row_length(self.dst.row.row_size) .regions()
.buffer_image_height(self.dst.row.row_count) .iter()
.image_offset(self.src.range.offset()) .zip(self.dst_region.regions())
.image_extent(self.src.range.extent()) .map(|(src_region, dst_region)| {
.image_subresource( vk::BufferImageCopy2::default()
vk::ImageSubresourceLayers::default() .buffer_offset(dst_region.offset)
.aspect_mask(self.src.range.aspect) .buffer_row_length(dst_region.row_size)
.mip_level(self.src.range.mip_levels.start()) .buffer_image_height(dst_region.row_count)
.base_array_layer(self.src.range.array_layers.start()) .image_offset(src_region.offset)
.layer_count(self.src.range.array_layers.len()), .image_extent(src_region.extent)
)]; .image_subresource(src_region.range.into_layers())
})
.collect::<Box<[_]>>();
let info = vk::CopyImageToBufferInfo2::default() let info = vk::CopyImageToBufferInfo2::default()
.dst_buffer(recorder.get_buffer_handle(self.dst.id())) .dst_buffer(recorder.get_buffer_handle(self.src.id()))
.src_image(recorder.get_image_handle(self.src.id())) .src_image(recorder.get_image_handle(self.dst.id()))
.src_image_layout(recorder.get_image_layout(self.src.id())) .src_image_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
.regions(regions); .regions(&regions);
unsafe { unsafe {
dev.cmd_copy_image_to_buffer2(cmd.raw(), &info); dev.cmd_copy_image_to_buffer2(cmd.raw(), &info);
} }
} }
} }
impl Command for Copy<TextureRegion, TextureRegion> {
fn side_effects(&self, map: SideEffectMap) {
self.side_effects_inner(map)
}
fn apply(self, recorder: &mut CommandRecorder) { impl<T> Command for CopyBuffers<T>
let cmd = recorder.cmd_buffer(); where
let dev = &cmd.device().raw; T: BufferCopyRegion,
{
let regions = &[vk::ImageCopy2::default() fn side_effects(&self, mut map: SideEffectMap) {
.extent(self.src.range.extent()) for region in self.src_region.regions() {
.dst_offset(self.dst.range.offset()) map.with(
.src_offset(self.src.range.offset()) vk::PipelineStageFlags2::COPY,
.dst_subresource( vk::AccessFlags2::TRANSFER_READ,
vk::ImageSubresourceLayers::default() None,
.aspect_mask(self.dst.range.aspect)
.mip_level(self.dst.range.mip_levels.start())
.base_array_layer(self.dst.range.array_layers.start())
.layer_count(self.dst.range.array_layers.len()),
) )
.src_subresource( .insert_range(self.src.id(), *region);
vk::ImageSubresourceLayers::default() }
.aspect_mask(self.src.range.aspect)
.mip_level(self.src.range.mip_levels.start())
.base_array_layer(self.src.range.array_layers.start())
.layer_count(self.src.range.array_layers.len()),
)];
let info = vk::CopyImageInfo2::default() for region in self.dst_region.regions() {
.dst_image(recorder.get_image_handle(self.dst.id())) map.with(
.dst_image_layout(recorder.get_image_layout(self.dst.id())) vk::PipelineStageFlags2::COPY,
.src_image(recorder.get_image_handle(self.src.id())) vk::AccessFlags2::TRANSFER_WRITE,
.src_image_layout(recorder.get_image_layout(self.src.id())) None,
.regions(regions); )
.insert_range(self.dst.id(), *region);
unsafe {
dev.cmd_copy_image2(cmd.raw(), &info);
} }
} }
}
impl Command for Copy<BufferSlice, BufferSlice> {
fn side_effects(&self, map: SideEffectMap) {
self.side_effects_inner(map)
}
fn apply(self, recorder: &mut CommandRecorder) { fn apply(self, recorder: &mut CommandRecorder) {
let cmd = recorder.cmd_buffer(); let cmd = recorder.cmd_buffer();
let dev = &cmd.device().raw; let dev = &cmd.device().raw;
debug_assert_eq!(self.src.range.size, self.dst.range.size); let regions = self
.src_region
let regions = &[vk::BufferCopy2::default() .regions()
.dst_offset(self.dst.range.offset) .iter()
.src_offset(self.src.range.offset) .zip(self.dst_region.regions())
.size(self.src.range.size)]; .map(|(src_region, dst_region)| {
vk::BufferCopy2::default()
.src_offset(src_region.offset)
.dst_offset(dst_region.offset)
.size(src_region.size)
})
.collect::<Box<[_]>>();
let info = vk::CopyBufferInfo2::default() let info = vk::CopyBufferInfo2::default()
.dst_buffer(recorder.get_buffer_handle(self.dst.id())) .dst_buffer(recorder.get_buffer_handle(self.dst.id()))
.src_buffer(recorder.get_buffer_handle(self.src.id())) .src_buffer(recorder.get_buffer_handle(self.src.id()))
.regions(regions); .regions(&regions);
unsafe { unsafe {
dev.cmd_copy_buffer2(cmd.raw(), &info); dev.cmd_copy_buffer2(cmd.raw(), &info);
@ -263,48 +350,137 @@ impl Command for Copy<BufferSlice, BufferSlice> {
} }
} }
pub struct ClearTexture { impl<T, R> Command for CopyTextures<T, R>
pub dst: Write<TextureRegion>, where
T: ImageCopyRegion<R>,
R: IntoImageSubresourceLayers,
{
fn side_effects(&self, mut map: SideEffectMap) {
for region in self.src_region.regions() {
map.with(
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_READ,
None,
)
.insert_range(self.src.id(), region.range.into_range());
}
for region in self.dst_region.regions() {
map.with(
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_WRITE,
None,
)
.insert_range(self.dst.id(), region.range.into_range());
}
}
fn apply(self, recorder: &mut CommandRecorder) {
let cmd = recorder.cmd_buffer();
let dev = &cmd.device().raw;
let regions = self
.src_region
.regions()
.iter()
.zip(self.dst_region.regions())
.map(|(src_region, dst_region)| {
vk::ImageCopy2::default()
.src_offset(src_region.offset)
.src_subresource(src_region.range.into_layers())
.dst_offset(dst_region.offset)
.dst_subresource(dst_region.range.into_layers())
.extent(src_region.extent)
})
.collect::<Box<[_]>>();
let info = vk::CopyImageInfo2::default()
.src_image(recorder.get_image_handle(self.src.id()))
.src_image_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
.dst_image(recorder.get_image_handle(self.dst.id()))
.dst_image_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
.regions(&regions);
unsafe {
dev.cmd_copy_image2(cmd.raw(), &info);
}
}
}
pub struct ClearTexture<R: IntoImageSubresourceRange> {
pub dst: Texture,
pub dst_region: TextureRegion<R>,
pub clear_value: [f32; 4], pub clear_value: [f32; 4],
} }
impl Command for ClearTexture { impl<R: IntoImageSubresourceRange> Command for ClearTexture<R> {
fn side_effects(&self, mut map: SideEffectMap) { fn side_effects(&self, mut map: SideEffectMap) {
self.dst.side_effect(map.reborrow().into_side_effect_map2( map.with(
vk::PipelineStageFlags2::TRANSFER, vk::PipelineStageFlags2::CLEAR,
vk::AccessFlags2::TRANSFER_WRITE, vk::AccessFlags2::TRANSFER_WRITE,
Some(vk::ImageLayout::TRANSFER_DST_OPTIMAL), Some(vk::ImageLayout::TRANSFER_DST_OPTIMAL),
)); )
.insert_range(self.dst.id(), self.dst_region.range.into_range());
} }
fn apply(self, _recorder: &mut CommandRecorder) {} fn apply(self, recorder: &mut CommandRecorder) {
let cmd = recorder.cmd_buffer();
let dev = &cmd.device().raw;
let values = vk::ClearColorValue {
float32: self.clear_value,
};
unsafe {
dev.cmd_clear_color_image(
cmd.raw(),
recorder.get_image_handle(self.dst.id()),
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
&values,
&[self.dst_region.range.into_range()],
);
}
}
} }
pub struct UpdateBuffer { pub struct UpdateBuffer {
pub dst: Write<BufferSlice>, pub dst: Buffer,
pub dst_region: BufferRange,
pub data: Vec<u8>, pub data: Vec<u8>,
} }
impl Command for UpdateBuffer { impl Command for UpdateBuffer {
fn side_effects(&self, mut map: SideEffectMap) { fn side_effects(&self, mut map: SideEffectMap) {
self.dst.side_effect(map.reborrow().into_side_effect_map2( map.with(
vk::PipelineStageFlags2::TRANSFER, vk::PipelineStageFlags2::TRANSFER,
vk::AccessFlags2::TRANSFER_WRITE, vk::AccessFlags2::TRANSFER_WRITE,
None, None,
)); )
.insert_range(self.dst.id(), self.dst_region);
} }
fn apply(self, _recorder: &mut CommandRecorder) {} fn apply(self, _recorder: &mut CommandRecorder) {}
} }
#[derive(Debug, Clone)]
pub struct RenderAttachment {
pub texture: Texture,
pub region: vk::ImageSubresourceRange,
pub layout: vk::ImageLayout,
pub load_op: vk::AttachmentLoadOp,
pub store_op: vk::AttachmentStoreOp,
}
pub struct BeginRendering { pub struct BeginRendering {
pub color_attachments: Vec<TextureRegion>, pub color_attachments: Vec<RenderAttachment>,
pub depth_attachment: Option<TextureRegion>, pub depth_attachment: Option<RenderAttachment>,
pub stencil_attachment: Option<TextureRegion>, pub stencil_attachment: Option<RenderAttachment>,
pub area: (u32, u32), pub area: (u32, u32),
pub layers: u32, pub layers: u32,
} }
pub struct EndRendering;
impl Command for BeginRendering { impl Command for BeginRendering {
fn side_effects(&self, mut map: SideEffectMap) { fn side_effects(&self, mut map: SideEffectMap) {
for attachment in &self.color_attachments { for attachment in &self.color_attachments {
@ -313,37 +489,49 @@ impl Command for BeginRendering {
// pass instance referencing framebuffer-local stages must contain // pass instance referencing framebuffer-local stages must contain
// only framebuffer-local stages. // only framebuffer-local stages.
// TODO: consider loadop and storeop? map.with(
attachment.side_effect(map.reborrow().into_side_effect_map2(
vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT, vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT,
vk::AccessFlags2::COLOR_ATTACHMENT_READ, if attachment.load_op == vk::AttachmentLoadOp::LOAD {
Some(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL), vk::AccessFlags2::COLOR_ATTACHMENT_READ
)); } else {
vk::AccessFlags2::empty()
},
Some(attachment.layout),
)
.insert_range(attachment.texture.id(), attachment.region);
} }
if let Some(depth) = &self.depth_attachment { if let Some(depth) = &self.depth_attachment {
depth.side_effect(map.reborrow().into_side_effect_map2( map.with(
vk::PipelineStageFlags2::EARLY_FRAGMENT_TESTS, vk::PipelineStageFlags2::EARLY_FRAGMENT_TESTS,
vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_READ, if depth.load_op == vk::AttachmentLoadOp::LOAD {
Some(vk::ImageLayout::DEPTH_ATTACHMENT_OPTIMAL), vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_READ
)); } else {
vk::AccessFlags2::empty()
},
Some(depth.layout),
)
.insert_range(depth.texture.id(), depth.region);
} }
if let Some(stencil) = &self.stencil_attachment { if let Some(stencil) = &self.stencil_attachment {
stencil.side_effect(map.reborrow().into_side_effect_map2( map.with(
vk::PipelineStageFlags2::EARLY_FRAGMENT_TESTS, vk::PipelineStageFlags2::EARLY_FRAGMENT_TESTS,
vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_READ, if stencil.load_op == vk::AttachmentLoadOp::LOAD {
Some(vk::ImageLayout::STENCIL_ATTACHMENT_OPTIMAL), vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_READ
)); } else {
vk::AccessFlags2::empty()
},
Some(stencil.layout),
)
.insert_range(stencil.texture.id(), stencil.region);
} }
} }
fn apply(self, _recorder: &mut CommandRecorder) {} fn apply(self, _recorder: &mut CommandRecorder) {}
} }
pub struct EndRendering;
impl Command for EndRendering { impl Command for EndRendering {
fn side_effects(&self, _map: SideEffectMap) { fn side_effects(&self, _map: SideEffectMap) {
// No resource access, but ends the render pass // TODO: need the beginrendering struct to declare the store-op dependencies.
} }
fn apply(self, _recorder: &mut CommandRecorder) {} fn apply(self, _recorder: &mut CommandRecorder) {}
@ -374,11 +562,12 @@ pub struct BindVertexBuffers {
impl Command for BindVertexBuffers { impl Command for BindVertexBuffers {
fn side_effects(&self, mut map: SideEffectMap) { fn side_effects(&self, mut map: SideEffectMap) {
for buffer in &self.buffers { for buffer in &self.buffers {
buffer.side_effect(map.reborrow().into_side_effect_map2( map.with(
vk::PipelineStageFlags2::VERTEX_ATTRIBUTE_INPUT, vk::PipelineStageFlags2::VERTEX_ATTRIBUTE_INPUT,
vk::AccessFlags2::VERTEX_ATTRIBUTE_READ, vk::AccessFlags2::VERTEX_ATTRIBUTE_READ,
None, None,
)); )
.insert_range(buffer.id(), buffer.range);
} }
} }
@ -408,11 +597,12 @@ pub struct BindIndexBuffer(pub BufferSlice, pub IndexFormat);
impl Command for BindIndexBuffer { impl Command for BindIndexBuffer {
fn side_effects(&self, mut map: SideEffectMap) { fn side_effects(&self, mut map: SideEffectMap) {
self.0.side_effect(map.reborrow().into_side_effect_map2( map.with(
vk::PipelineStageFlags2::INDEX_INPUT, vk::PipelineStageFlags2::INDEX_INPUT,
vk::AccessFlags2::INDEX_READ, vk::AccessFlags2::INDEX_READ,
None, None,
)); )
.insert_range(self.0.id(), self.0.range);
} }
fn apply(self, recorder: &mut CommandRecorder) { fn apply(self, recorder: &mut CommandRecorder) {
@ -557,27 +747,11 @@ pub enum IndexFormat {
pub struct Draw<T: IsDrawData> { pub struct Draw<T: IsDrawData> {
pub data: T, pub data: T,
pub vertex_buffers: Vec<BufferSlice>,
pub index_buffer: Option<(BufferSlice, IndexFormat)>,
} }
impl<T: IsDrawData> Command for Draw<T> { impl<T: IsDrawData> Command for Draw<T> {
fn side_effects(&self, mut map: SideEffectMap) { fn side_effects(&self, mut map: SideEffectMap) {
self.data.side_effects(map.reborrow()); self.data.side_effects(map.reborrow());
for vertex_buffer in &self.vertex_buffers {
vertex_buffer.side_effect(map.reborrow().into_side_effect_map2(
vk::PipelineStageFlags2::VERTEX_ATTRIBUTE_INPUT,
vk::AccessFlags2::VERTEX_ATTRIBUTE_READ,
None,
));
}
if let Some((index_buffer, _)) = &self.index_buffer {
index_buffer.side_effect(map.reborrow().into_side_effect_map2(
vk::PipelineStageFlags2::INDEX_INPUT,
vk::AccessFlags2::INDEX_READ,
None,
));
}
} }
fn apply(self, _recorder: &mut CommandRecorder) {} fn apply(self, _recorder: &mut CommandRecorder) {}
@ -606,35 +780,35 @@ mod sealed {
pub trait OutsideRenderPass {} pub trait OutsideRenderPass {}
} }
use ash::vk; use ash::vk;
use ash_formats::FormatExt;
pub(super) use sealed::*; pub(super) use sealed::*;
impl IsDrawData for DrawData {} impl IsDrawData for DrawData {}
impl IsDrawData for DrawIndexedData {} impl IsDrawData for DrawIndexedData {}
impl IsDrawData for DrawIndirectData { impl IsDrawData for DrawIndirectData {
fn side_effects(&self, mut map: SideEffectMap) { fn side_effects(&self, mut map: SideEffectMap) {
self.indirect_buffer map.with(
.side_effect(map.reborrow().into_side_effect_map2(
vk::PipelineStageFlags2::DRAW_INDIRECT, vk::PipelineStageFlags2::DRAW_INDIRECT,
vk::AccessFlags2::INDIRECT_COMMAND_READ, vk::AccessFlags2::INDIRECT_COMMAND_READ,
None, None,
)); )
.insert_range(self.indirect_buffer.id(), self.indirect_buffer.range);
} }
} }
impl IsDrawData for DrawIndexedIndirectData { impl IsDrawData for DrawIndexedIndirectData {
fn side_effects(&self, mut map: SideEffectMap) { fn side_effects(&self, mut map: SideEffectMap) {
self.indirect_buffer map.with(
.side_effect(map.reborrow().into_side_effect_map2(
vk::PipelineStageFlags2::DRAW_INDIRECT, vk::PipelineStageFlags2::DRAW_INDIRECT,
vk::AccessFlags2::INDIRECT_COMMAND_READ, vk::AccessFlags2::INDIRECT_COMMAND_READ,
None, None,
)); )
.insert_range(self.indirect_buffer.id(), self.indirect_buffer.range);
} }
} }
impl OutsideRenderPass for BeginRendering {} impl OutsideRenderPass for BeginRendering {}
impl InsideRenderPass for EndRendering {} impl InsideRenderPass for EndRendering {}
impl<T: Resource, U: Resource> OutsideRenderPass for Copy<T, U> {} impl<T: IntoImageSubresourceRange> OutsideRenderPass for ClearTexture<T> {}
impl OutsideRenderPass for ClearTexture {}
impl OutsideRenderPass for UpdateBuffer {} impl OutsideRenderPass for UpdateBuffer {}
impl InsideRenderPass for BindPipeline {} impl InsideRenderPass for BindPipeline {}

View file

@ -4,7 +4,9 @@ use ash::vk;
use derive_more::derive::{Deref, DerefMut}; use derive_more::derive::{Deref, DerefMut};
use crate::{ use crate::{
buffers, images, buffers,
device::MemoryLocation,
images,
render_graph::{ render_graph::{
recorder::CommandList, recorder::CommandList,
resources::{self, ResourceId}, resources::{self, ResourceId},
@ -44,11 +46,29 @@ impl<'r> GraphBuilder<'r> {
todo!() todo!()
} }
fn make_persistent_buffer(
&self,
_size: u64,
_usage: vk::BufferUsageFlags,
_location: MemoryLocation,
) -> Buffer<'r> {
todo!()
}
fn make_transient_buffer(
&self,
_size: u64,
_usage: vk::BufferUsageFlags,
_location: MemoryLocation,
) -> Buffer<'r> {
todo!()
}
fn make_buffer( fn make_buffer(
&self, &self,
_size: u64, _size: u64,
_usage: vk::BufferUsageFlags, _usage: vk::BufferUsageFlags,
_location: gpu_allocator::MemoryLocation, _location: MemoryLocation,
) -> Buffer<'r> { ) -> Buffer<'r> {
todo!() todo!()
} }
@ -66,7 +86,6 @@ mod api_demo {
use super::*; use super::*;
use ash::vk::{self, BufferUsageFlags}; use ash::vk::{self, BufferUsageFlags};
use gpu_allocator::MemoryLocation;
use indexmap::IndexMap; use indexmap::IndexMap;
fn egui_graph<'a>( fn egui_graph<'a>(
@ -132,13 +151,17 @@ mod api_demo {
let staging_size = vertex_size + index_size + draw_calls_size + textures_indices_size; let staging_size = vertex_size + index_size + draw_calls_size + textures_indices_size;
let mut staging = builder.make_staging_buffer(staging_size as u64); let mut staging = builder.make_staging_buffer(staging_size as u64);
use core::io::BorrowedBuf; let staging_map = staging.map_mut().unwrap();
let mut buf = BorrowedBuf::from(staging.map_mut().unwrap()); let mut offset = 0;
let mut cursor = buf.unfilled(); for data in [
cursor.append(bytemuck::cast_slice(&vertices)); bytemuck::cast_slice(&vertices),
cursor.append(bytemuck::cast_slice(&indices)); bytemuck::cast_slice(&indices),
cursor.append(bytemuck::cast_slice(&draw_calls)); bytemuck::cast_slice(&draw_calls),
cursor.append(bytemuck::cast_slice(&textures_indices)); bytemuck::cast_slice(&textures_indices),
] {
staging_map[offset..offset + data.len()].copy_from_slice(data);
offset += data.len();
}
let vertex_buffer = builder.make_buffer( let vertex_buffer = builder.make_buffer(
vertex_size as u64, vertex_size as u64,
@ -155,91 +178,93 @@ mod api_demo {
BufferUsageFlags::TRANSFER_DST | BufferUsageFlags::INDIRECT_BUFFER, BufferUsageFlags::TRANSFER_DST | BufferUsageFlags::INDIRECT_BUFFER,
MemoryLocation::GpuOnly, MemoryLocation::GpuOnly,
); );
let texture_indices = builder.make_buffer( let _texture_indices = builder.make_buffer(
textures_indices_size as u64, textures_indices_size as u64,
BufferUsageFlags::TRANSFER_DST | BufferUsageFlags::STORAGE_BUFFER, BufferUsageFlags::TRANSFER_DST | BufferUsageFlags::STORAGE_BUFFER,
MemoryLocation::GpuOnly, MemoryLocation::GpuOnly,
); );
let mut cmd = builder.make_command_list(); let mut cmd = builder.make_command_list();
cmd.push(commands::Copy { // cmd.push(commands::Copy {
src: resources::BufferSlice { // src: resources::BufferSlice {
buffer: resources::Buffer(staging.id), // buffer: resources::Buffer(staging.id),
range: resources::BufferRange { // range: resources::BufferRange {
offset: 0, // offset: 0,
size: vertex_size as u64, // size: vertex_size as u64,
}, // },
}, // },
dst: resources::BufferSlice { // dst: resources::BufferSlice {
buffer: resources::Buffer(vertex_buffer.id), // buffer: resources::Buffer(vertex_buffer.id),
range: resources::BufferRange { // range: resources::BufferRange {
offset: 0, // offset: 0,
size: vertex_size as u64, // size: vertex_size as u64,
}, // },
}, // },
}); // });
cmd.push(commands::Copy { // cmd.push(commands::Copy {
src: resources::BufferSlice { // src: resources::BufferSlice {
buffer: resources::Buffer(staging.id), // buffer: resources::Buffer(staging.id),
range: resources::BufferRange { // range: resources::BufferRange {
offset: vertex_size as u64, // offset: vertex_size as u64,
size: (vertex_size + index_size) as u64, // size: (vertex_size + index_size) as u64,
}, // },
}, // },
dst: resources::BufferSlice { // dst: resources::BufferSlice {
buffer: resources::Buffer(index_buffer.id), // buffer: resources::Buffer(index_buffer.id),
range: resources::BufferRange { // range: resources::BufferRange {
offset: 0, // offset: 0,
size: index_size as u64, // size: index_size as u64,
}, // },
}, // },
}); // });
cmd.push(commands::Copy { // cmd.push(commands::Copy {
src: resources::BufferSlice { // src: resources::BufferSlice {
buffer: resources::Buffer(staging.id), // buffer: resources::Buffer(staging.id),
range: resources::BufferRange { // range: resources::BufferRange {
offset: (vertex_size + index_size) as u64, // offset: (vertex_size + index_size) as u64,
size: draw_calls_size as u64, // size: draw_calls_size as u64,
}, // },
}, // },
dst: resources::BufferSlice { // dst: resources::BufferSlice {
buffer: resources::Buffer(indirect_buffer.id), // buffer: resources::Buffer(indirect_buffer.id),
range: resources::BufferRange { // range: resources::BufferRange {
offset: 0, // offset: 0,
size: draw_calls_size as u64, // size: draw_calls_size as u64,
}, // },
}, // },
}); // });
cmd.push(commands::Copy { // cmd.push(commands::Copy {
src: resources::BufferSlice { // src: resources::BufferSlice {
buffer: resources::Buffer(staging.id), // buffer: resources::Buffer(staging.id),
range: resources::BufferRange { // range: resources::BufferRange {
offset: (vertex_size + index_size + draw_calls_size) as u64, // offset: (vertex_size + index_size + draw_calls_size) as u64,
size: textures_indices_size as u64, // size: textures_indices_size as u64,
}, // },
}, // },
dst: resources::BufferSlice { // dst: resources::BufferSlice {
buffer: resources::Buffer(texture_indices.id), // buffer: resources::Buffer(texture_indices.id),
range: resources::BufferRange { // range: resources::BufferRange {
offset: 0, // offset: 0,
size: textures_indices_size as u64, // size: textures_indices_size as u64,
}, // },
}, // },
}); // });
let mut pass_cmd = cmd.begin_rendering( let mut pass_cmd = cmd.begin_rendering(
vec![resources::TextureRegion { vec![commands::RenderAttachment {
texture: resources::Texture(render_target), texture: resources::Texture(render_target),
range: resources::TextureRange { region: vk::ImageSubresourceRange::default()
aspect: vk::ImageAspectFlags::COLOR, .aspect_mask(vk::ImageAspectFlags::COLOR)
mip_levels: (0..).into(), .base_mip_level(0)
array_layers: (0..).into(), .level_count(1)
origin: (0, 0, 0), .base_array_layer(0)
extent: (extent.0, extent.1, 1), .layer_count(1),
}, layout: vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
load_op: vk::AttachmentLoadOp::LOAD,
store_op: vk::AttachmentStoreOp::STORE,
}], }],
None, None,
None, None,
@ -283,20 +308,17 @@ mod api_demo {
&[extent.0 as f32, extent.1 as f32].map(f32::to_bits), &[extent.0 as f32, extent.1 as f32].map(f32::to_bits),
), ),
}); });
pass_cmd.push(commands::Draw { pass_cmd.push(commands::BindVertexBuffers {
data: commands::DrawIndexedIndirectData { buffers: vec![resources::BufferSlice {
indirect_buffer: resources::Buffer(indirect_buffer.id).full_slice(),
count: draw_calls.len() as u32,
stride: size_of::<DrawCall>() as u32,
},
vertex_buffers: vec![resources::BufferSlice {
buffer: resources::Buffer(vertex_buffer.id), buffer: resources::Buffer(vertex_buffer.id),
range: resources::BufferRange { range: resources::BufferRange {
offset: 0, offset: 0,
size: vertex_size as u64, size: vertex_size as u64,
}, },
}], }],
index_buffer: Some(( first_binding: 0,
});
pass_cmd.push(commands::BindIndexBuffer(
resources::BufferSlice { resources::BufferSlice {
buffer: resources::Buffer(index_buffer.id), buffer: resources::Buffer(index_buffer.id),
range: resources::BufferRange { range: resources::BufferRange {
@ -305,7 +327,13 @@ mod api_demo {
}, },
}, },
commands::IndexFormat::Uint32, commands::IndexFormat::Uint32,
)), ));
pass_cmd.push(commands::Draw {
data: commands::DrawIndexedIndirectData {
indirect_buffer: resources::Buffer(indirect_buffer.id).full_slice(),
count: draw_calls.len() as u32,
stride: size_of::<DrawCall>() as u32,
},
}); });
pass_cmd.finalise(); pass_cmd.finalise();

View file

@ -1,7 +1,7 @@
use std::{ use std::{
collections::{BTreeMap, BTreeSet}, collections::{BTreeMap, BTreeSet},
marker::PhantomData, marker::PhantomData,
mem::{MaybeUninit, size_of}, mem::{ManuallyDrop, MaybeUninit, size_of},
ptr::NonNull, ptr::NonNull,
}; };
@ -11,8 +11,8 @@ use crate::{
commands::CommandBuffer, commands::CommandBuffer,
images::ImageDesc, images::ImageDesc,
render_graph::{ render_graph::{
commands::{Command, InsideRenderPass, OutsideRenderPass}, commands::{Command, InsideRenderPass, OutsideRenderPass, RenderAttachment},
resources::{ResourceAccess, ResourceId, ResourceRange, TextureRegion}, resources::{ResourceId, Subresource, SubresourceScope, SubresourceScopeInner},
}, },
}; };
@ -50,18 +50,27 @@ struct CommandMeta {
enum VecOrSingle<T> { enum VecOrSingle<T> {
Vec(Vec<T>), Vec(Vec<T>),
Single(T), Single(ManuallyDrop<T>),
}
impl<T> From<T> for VecOrSingle<T> {
fn from(value: T) -> Self {
Self::Single(ManuallyDrop::new(value))
}
}
impl<T> From<Vec<T>> for VecOrSingle<T> {
fn from(vec: Vec<T>) -> Self {
Self::Vec(vec)
}
} }
impl<T> VecOrSingle<T> { impl<T> VecOrSingle<T> {
fn push(&mut self, value: T) fn push(&mut self, value: T) {
where
T: Default,
{
match self { match self {
VecOrSingle::Vec(vec) => vec.push(value), VecOrSingle::Vec(vec) => vec.push(value),
VecOrSingle::Single(existing) => { VecOrSingle::Single(existing) => {
let existing = std::mem::take(existing); let existing = unsafe { ManuallyDrop::take(existing) };
*self = VecOrSingle::Vec(vec![existing, value]); *self = VecOrSingle::Vec(vec![existing, value]);
} }
} }
@ -69,27 +78,55 @@ impl<T> VecOrSingle<T> {
fn iter(&self) -> core::slice::Iter<'_, T> { fn iter(&self) -> core::slice::Iter<'_, T> {
match self { match self {
VecOrSingle::Vec(items) => items.as_slice().iter(), VecOrSingle::Vec(items) => items.as_slice().iter(),
VecOrSingle::Single(item) => core::slice::from_ref(item).iter(), VecOrSingle::Single(item) => core::slice::from_ref::<T>(item).iter(),
}
}
fn iter_mut(&mut self) -> core::slice::IterMut<'_, T> {
match self {
VecOrSingle::Vec(items) => items.as_mut_slice().iter_mut(),
VecOrSingle::Single(item) => core::slice::from_mut::<T>(item).iter_mut(),
} }
} }
} }
#[derive(Default)] #[derive(Default)]
struct SideEffects { struct SideEffects {
map: BTreeMap<(ResourceId, u32), VecOrSingle<ResourceAccess>>, map: BTreeMap<(ResourceId, u32), VecOrSingle<SubresourceScope>>,
resources: BTreeSet<ResourceId>, resources: BTreeSet<ResourceId>,
} }
pub struct SideEffectMap<'a>(&'a mut SideEffects, u32); pub struct SideEffectMap<'a>(&'a mut SideEffects, u32);
impl<'a> SideEffectMap<'a> { impl<'a> SideEffectMap<'a> {
pub fn insert(&mut self, id: ResourceId, access: ResourceAccess) { pub fn insert(&mut self, id: ResourceId, new_scope: SubresourceScope) {
self.0.resources.insert(id); self.0.resources.insert(id);
'inserted: {
'outer: for ((_rid, _cid), scopes) in self.0.map.range_mut((id, 0)..=(id, self.1)).rev()
{
for scope in scopes.iter_mut().rev() {
if scope.try_merge(&new_scope) {
// if we can fold this scope with a previous one, we can
// stop iterating, as the barrier already exists if
// needed
break 'inserted;
} else if scope.conflicts(&new_scope) {
// if this scope conflicts with a previous one, we need
// to insert a barrier before the current command index;
// we can stop iterating, as we can no longer fold into
// any scopes before this conflict
break 'outer;
}
}
}
// if we couldn't fold this scope with any previous one, add it as a new entry
self.0 self.0
.map .map
.entry((id, self.1)) .entry((id, self.1))
.and_modify(|entry| entry.push(access)) .and_modify(|entry| entry.push(new_scope))
.or_insert_with(|| VecOrSingle::Single(access)); .or_insert_with(|| new_scope.into());
}
} }
pub fn reborrow(&mut self) -> SideEffectMap<'_> { pub fn reborrow(&mut self) -> SideEffectMap<'_> {
@ -110,6 +147,16 @@ impl<'a> SideEffectMap<'a> {
required_layout, required_layout,
} }
} }
pub fn with(
&mut self,
stage_flags: vk::PipelineStageFlags2,
access_flags: vk::AccessFlags2,
required_layout: Option<vk::ImageLayout>,
) -> SideEffectMap2<'_> {
self.reborrow()
.into_side_effect_map2(stage_flags, access_flags, required_layout)
}
} }
pub struct SideEffectMap2<'a> { pub struct SideEffectMap2<'a> {
@ -132,18 +179,30 @@ impl<'a> SideEffectMap2<'a> {
} }
pub fn insert_range<T>(&mut self, id: ResourceId, range: T) pub fn insert_range<T>(&mut self, id: ResourceId, range: T)
where where
(T, vk::ImageLayout): Into<ResourceRange>, T: Into<Subresource>,
{ {
self.inner.resources.insert(id); self.inner.resources.insert(id);
SideEffectMap(self.inner, self.command_index).insert( let subresource: Subresource = range.into();
id, let scope = match subresource {
ResourceAccess { Subresource::Buffer(buffer) => SubresourceScope {
range: (range, self.required_layout.unwrap_or_default()).into(),
stages: self.stage_flags,
access: self.access_flags, access: self.access_flags,
stage: self.stage_flags,
subresource: SubresourceScopeInner::Buffer {
subresource: buffer,
}, },
); },
Subresource::Texture(texture) => SubresourceScope {
access: self.access_flags,
stage: self.stage_flags,
subresource: SubresourceScopeInner::Texture {
subresource: texture,
layout: self.required_layout.unwrap_or_default(),
},
},
};
SideEffectMap(self.inner, self.command_index).insert(id, scope);
} }
} }
@ -165,7 +224,7 @@ pub struct RenderPass<'cmd, 'a> {
impl<'cmd, 'a> RenderPass<'cmd, 'a> { impl<'cmd, 'a> RenderPass<'cmd, 'a> {
pub fn finalise(self) { pub fn finalise(self) {
self.cmd_list.push_inner(super::commands::EndRendering); self.cmd_list.push_inner(super::commands::EndRendering {});
} }
pub fn push<C: Command + InsideRenderPass + 'cmd>(&mut self, command: C) { pub fn push<C: Command + InsideRenderPass + 'cmd>(&mut self, command: C) {
@ -186,9 +245,9 @@ impl<'cmd> CommandList<'cmd> {
pub fn begin_rendering( pub fn begin_rendering(
&mut self, &mut self,
color_attachments: Vec<TextureRegion>, color_attachments: Vec<RenderAttachment>,
depth_attachment: Option<TextureRegion>, depth_attachment: Option<RenderAttachment>,
stencil_attachment: Option<TextureRegion>, stencil_attachment: Option<RenderAttachment>,
area: (u32, u32), area: (u32, u32),
layers: u32, layers: u32,
) -> RenderPass<'cmd, '_> { ) -> RenderPass<'cmd, '_> {
@ -258,12 +317,12 @@ impl<'cmd> CommandList<'cmd> {
struct Barrier { struct Barrier {
resource_id: ResourceId, resource_id: ResourceId,
command_index: u32, command_index: u32,
from: ResourceAccess, from: SubresourceScope,
to: usize, to: usize,
} }
// map to keep track of the current state of each subresource // map to keep track of the current state of each subresource
let mut subresource_state: BTreeMap<ResourceId, Vec<(ResourceAccess, SubresourceState)>> = let mut subresource_state: BTreeMap<ResourceId, Vec<(SubresourceScope, SubresourceState)>> =
Default::default(); Default::default();
let mut barriers = Vec::new(); let mut barriers = Vec::new();
@ -293,7 +352,7 @@ impl<'cmd> CommandList<'cmd> {
let mut folded = false; let mut folded = false;
let len = entry.len(); let len = entry.len();
for (_i, (sub, _state)) in &mut entry.iter_mut().enumerate().rev() { for (_i, (sub, _state)) in &mut entry.iter_mut().enumerate().rev() {
if !folded && sub.try_fold(&access) { if !folded && sub.try_merge(&access) {
access = *sub; access = *sub;
folded = true; folded = true;
} else if sub.conflicts(&access) { } else if sub.conflicts(&access) {
@ -337,7 +396,7 @@ impl<'cmd> CommandList<'cmd> {
// construct barriers // construct barriers
// TODO: need to know each images aspects at this point // TODO: need to know each images aspects at this point
for barrier in barriers { for _barrier in barriers {
// stuff // stuff
} }

View file

@ -2,295 +2,242 @@ use std::ops::Deref;
use ash::vk; use ash::vk;
use crate::{images::MipRange, render_graph::recorder::SideEffectMap2}; use crate::images::MipRange;
pub trait Resource: Sized { pub trait Resource: Sized {
fn id(&self) -> ResourceId; fn id(&self) -> ResourceId;
fn side_effect(&self, map: SideEffectMap2); fn full_range(&self) -> Subresource;
} }
mod impls { mod sealed {
use super::*; use ash::vk;
impl Resource for Buffer { pub trait Texture {
fn id(&self) -> ResourceId { fn format(&self) -> vk::Format {
self.0 todo!()
}
}
pub trait Buffer {
fn size(&self) -> u64 {
todo!()
}
}
}
pub use sealed::{Buffer as AsBuffer, Texture as AsTexture};
#[derive(Debug, Clone, Copy)]
pub enum Subresource {
Buffer(BufferRange),
Texture(vk::ImageSubresourceRange),
} }
fn side_effect(&self, mut map: SideEffectMap2) { impl Subresource {
map.insert_range(self.id(), BufferRange::full()); pub fn full_buffer() -> Self {
} Self::Buffer(BufferRange {
} offset: 0,
impl Resource for BufferSlice {
fn id(&self) -> ResourceId {
self.buffer.id()
}
fn side_effect(&self, mut map: SideEffectMap2) {
map.insert_range(self.id(), self.range);
}
}
impl Resource for BufferSlices {
fn id(&self) -> ResourceId {
self.buffer.id()
}
fn side_effect(&self, mut map: SideEffectMap2) {
for range in &self.ranges {
map.insert_range(self.id(), *range);
}
}
}
// TODO: the rows are not in bytes, so this is plain wrong. A BufferRowSlice needs to be generic over Buffer/BufferSlice, or commands taking a BufferRowSlice need to take the row parameters separately.
impl Resource for BufferRowSlice {
fn id(&self) -> ResourceId {
self.buffer.id()
}
fn side_effect(&self, mut map: SideEffectMap2) {
map.insert_range(
self.id(),
BufferRange {
offset: self.row.offset,
size: vk::WHOLE_SIZE, size: vk::WHOLE_SIZE,
}, })
);
}
}
impl Resource for BufferRowSlices {
fn id(&self) -> ResourceId {
self.buffer.id()
} }
fn side_effect(&self, mut map: SideEffectMap2) { pub fn full_texture(format: vk::Format) -> Self {
for row in &self.rows { use ash_formats::FormatExt;
map.insert_range( Self::Texture(vk::ImageSubresourceRange {
self.id(), aspect_mask: format.aspect_flags(),
BufferRange { base_mip_level: 0,
offset: row.offset, level_count: vk::REMAINING_MIP_LEVELS,
size: vk::WHOLE_SIZE, base_array_layer: 0,
}, layer_count: vk::REMAINING_ARRAY_LAYERS,
); })
}
} }
} }
impl Resource for Texture { #[derive(Debug, Clone, Copy)]
fn id(&self) -> ResourceId { pub struct SubresourceScope {
self.0
}
fn side_effect(&self, mut map: SideEffectMap2) {
map.insert_range(self.id(), TextureRange::full());
}
}
impl Resource for TextureRegion {
fn id(&self) -> ResourceId {
self.texture.id()
}
fn side_effect(&self, mut map: SideEffectMap2) {
map.insert_range(self.id(), self.range);
}
}
impl Resource for TextureRegions {
fn id(&self) -> ResourceId {
self.texture.id()
}
fn side_effect(&self, mut map: SideEffectMap2) {
for range in &self.ranges {
map.insert_range(self.id(), *range);
}
}
}
impl Resource for TextureView {
fn id(&self) -> ResourceId {
self.texture.id()
}
fn side_effect(&self, mut map: SideEffectMap2) {
map.insert_range(self.id(), self.range);
}
}
impl<T: Resource> Resource for Read<T> {
fn id(&self) -> ResourceId {
self.0.id()
}
fn side_effect(&self, map: SideEffectMap2) {
self.0.side_effect(map);
}
}
impl<T: Resource> Resource for Write<T> {
fn id(&self) -> ResourceId {
self.0.id()
}
fn side_effect(&self, map: SideEffectMap2) {
self.0.side_effect(map);
}
}
impl<T: Resource> Resource for ReadWrite<T> {
fn id(&self) -> ResourceId {
self.0.id()
}
fn side_effect(&self, map: SideEffectMap2) {
self.0.side_effect(map);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(transparent)]
pub struct ResourceId(pub u64);
#[derive(Debug, Default, Clone, Copy)]
pub struct ResourceAccess {
pub range: ResourceRange,
pub access: vk::AccessFlags2, pub access: vk::AccessFlags2,
pub stages: vk::PipelineStageFlags2, pub stage: vk::PipelineStageFlags2,
pub subresource: SubresourceScopeInner,
} }
impl ResourceAccess { #[derive(Debug, Clone, Copy)]
/// Attempts to fold another ResourceAccess into this one. Returns true if pub enum SubresourceScopeInner {
/// successful, false if they are incompatible. Buffer {
pub fn try_fold(&mut self, other: &Self) -> bool { subresource: BufferRange,
// if `other` has access flags that aren't in `self`, we can't fold
// them.
if !self.access.contains(other.access) {
return false;
}
match (&mut self.range, &other.range) {
(
ResourceRange::Buffer { offset, size },
ResourceRange::Buffer {
offset: offset_b,
size: size_b,
}, },
) => { Texture {
// only fold if other lies within self subresource: vk::ImageSubresourceRange,
if *offset > *offset_b || *offset + *size < *offset_b + *size_b { layout: vk::ImageLayout,
},
}
impl SubresourceScope {
pub fn try_merge(&mut self, other: &Self) -> bool {
// cannot merge a read and a write.
if access_flag_is_read(self.access) != access_flag_is_read(other.access) {
return false; return false;
} }
// merge stages: we need the barrier to scope all stages that use SubresourceScopeInner::*;
// touch the resource. match (self.subresource, other.subresource) {
self.stages |= other.stages; (Buffer { subresource: lhs }, Buffer { subresource: rhs }) => {
if lhs.offset > rhs.offset || lhs.offset + lhs.size < rhs.offset + rhs.size {
// other is not a subresource of self, so we can't merge them.
return false;
}
self.stage |= other.stage;
self.access |= other.access;
true true
} }
( (
ResourceRange::Texture { Texture {
aspect, subresource: lhs,
mip_level, layout: lhs_layout,
array_layers,
layout,
..
}, },
ResourceRange::Texture { Texture {
aspect: aspect_b, subresource: rhs,
mip_level: mip_level_b, layout: rhs_layout,
array_layers: array_layers_b,
layout: layout_b,
..
}, },
) => { ) => {
// layout transition required even on reads, so can't fold if layouts differ.
if lhs_layout != rhs_layout {
// TODO: consider if we can still merge `other` into `self`
// if they overlap, and reduce the second barrier to only a
// layout transition.
return false;
}
// two accesses conflict if they overlap; // two accesses conflict if they overlap;
// they overlap if they share mip levels or array layers with // they overlap if they share mip levels or array layers with
// the same aspects. // the same aspects.
// If one access contains the other, we can merge them (i // If one access contains the other, we can merge them (i
// think), however: if a previous access requires a less general // think), however: if a previous access requires a less general
// barrier, then is it better to split? // barrier, then is it better to split?
if !aspect.contains(*aspect_b)
|| layout != layout_b let overlaps = lhs.aspect_mask.intersects(rhs.aspect_mask)
|| !mip_level.intersects(mip_level_b) && lhs.mip_levels().intersects(rhs.mip_levels())
|| !array_layers.intersects(array_layers_b) && lhs.layers().intersects(rhs.layers());
{
// non-overlapping sub-resources shouldn't be merged.
if !overlaps {
return false; return false;
} }
*aspect = *aspect | *aspect_b; let lhs_contains_rhs = lhs.aspect_mask.contains(rhs.aspect_mask)
*mip_level = mip_level.union(mip_level_b); && lhs.mip_levels().contains(rhs.mip_levels())
*array_layers = array_layers.union(array_layers_b); && lhs.layers().contains(rhs.layers());
// let lower_left = Offset::from(*origin).min(Offset::from(*origin_b));
// let upper_right = (Offset::from(*origin) + Extent::from(*extent).as_offset())
// .max(Offset::from(*origin_b) + Extent::from(*extent_b).as_offset());
// *origin = lower_left.into();
// *extent = Extent::from_offset(upper_right - lower_left).into();
// merge stages: we need the barrier to scope all stages that // other is not a subresource of self, so we can't merge them.
// touch the resource. if !lhs_contains_rhs {
self.stages |= other.stages; return false;
}
self.stage |= other.stage;
self.access |= other.access;
true true
} }
// different resource types can't fold // different resource types can't merge
_ => false, _ => false,
} }
} }
pub fn conflicts(&self, other: &Self) -> bool { pub fn conflicts(&self, other: &Self) -> bool {
match (&self.range, &other.range) { use SubresourceScopeInner::*;
(ResourceRange::Buffer { .. }, ResourceRange::Texture { .. }) => false, match (self.subresource, other.subresource) {
(ResourceRange::Texture { .. }, ResourceRange::Buffer { .. }) => false, (Buffer { subresource: lhs }, Buffer { subresource: rhs }) => {
(
ResourceRange::Buffer { offset, size },
ResourceRange::Buffer {
offset: offset_b,
size: size_b,
},
) => {
// two reads don't conflict // two reads don't conflict
// TODO: two writes may conflict if the user cares about the order of writes. if access_flag_is_read(self.access) && access_flag_is_read(other.access) {
if access_flag_is_read(self.access) == access_flag_is_read(other.access) {
return false; return false;
} }
// must be a read/write conflict, check if sub-resources intersect // if they overlap, they conflict
!(*offset > *offset_b + *size_b || *offset_b > *offset + *size) !(lhs.offset > rhs.offset + rhs.size || rhs.offset > lhs.offset + lhs.size)
} }
( (
ResourceRange::Texture { Texture {
aspect, subresource: lhs,
mip_level, layout: lhs_layout,
array_layers,
layout,
..
}, },
ResourceRange::Texture { Texture {
aspect: aspect_b, subresource: rhs,
mip_level: mip_level_b, layout: rhs_layout,
array_layers: array_layers_b,
layout: layout_b,
..
}, },
) => { ) => {
// check if sub-resources intersect // image subresource ranges overlap if they share mip levels and
let overlaps = aspect.intersects(*aspect_b) // array layers with the same aspects.
&& (mip_level.intersects(mip_level_b) let overlaps = lhs.aspect_mask.intersects(rhs.aspect_mask)
|| array_layers.intersects(array_layers_b)); && lhs.mip_levels().intersects(rhs.mip_levels())
&& lhs.layers().intersects(rhs.layers());
// non-overlapping sub-resources never conflict
if !overlaps { if !overlaps {
return false; return false;
} }
// layout transition required even on reads // if they require different layouts, they conflict even on
if layout != layout_b { // reads, since a layout transition is required.
if lhs_layout != rhs_layout {
return true; return true;
} }
// two reads don't conflict // finally, two reads don't conflict
access_flag_is_read(self.access) != access_flag_is_read(other.access) access_flag_is_read(self.access) && access_flag_is_read(other.access)
}
// different resource types can't conflict
_ => false,
} }
} }
} }
mod impls {
use super::*;
impl sealed::Texture for Texture {}
impl sealed::Buffer for Buffer {}
impl sealed::Buffer for BufferSlice {}
impl Resource for Buffer {
fn id(&self) -> ResourceId {
self.0
}
fn full_range(&self) -> Subresource {
Subresource::full_buffer()
}
}
impl Resource for BufferSlice {
fn id(&self) -> ResourceId {
self.buffer.id()
}
fn full_range(&self) -> Subresource {
self.buffer.full_range()
}
}
impl Resource for Texture {
fn id(&self) -> ResourceId {
self.0
}
fn full_range(&self) -> Subresource {
Subresource::full_texture(vk::Format::UNDEFINED)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(transparent)]
pub struct ResourceId(u32);
impl ResourceId {
pub fn from_u32(index: u32) -> Self {
Self(index)
}
pub fn index(self) -> usize {
self.0 as usize
}
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -303,8 +250,6 @@ pub enum ResourceRange {
aspect: vk::ImageAspectFlags, aspect: vk::ImageAspectFlags,
mip_level: MipRange, mip_level: MipRange,
array_layers: MipRange, array_layers: MipRange,
origin: (i32, i32, i32),
extent: (u32, u32, u32),
layout: vk::ImageLayout, layout: vk::ImageLayout,
}, },
} }
@ -333,6 +278,12 @@ impl BufferRange {
} }
} }
impl From<BufferRange> for Subresource {
fn from(value: BufferRange) -> Self {
Subresource::Buffer(value)
}
}
#[derive(Debug, Default, Clone, Copy)] #[derive(Debug, Default, Clone, Copy)]
pub struct BufferRows { pub struct BufferRows {
pub offset: u64, pub offset: u64,
@ -340,29 +291,106 @@ pub struct BufferRows {
pub row_size: u32, pub row_size: u32,
} }
#[derive(Debug, Default, Clone, Copy)] pub trait IntoImageSubresourceLayers {
pub struct TextureRange { fn into_layers(&self) -> vk::ImageSubresourceLayers;
pub aspect: vk::ImageAspectFlags,
pub mip_levels: MipRange,
pub array_layers: MipRange,
pub origin: (i32, i32, i32),
pub extent: (u32, u32, u32),
} }
impl TextureRange { pub trait IntoImageSubresourceRange {
pub fn offset(&self) -> vk::Offset3D { fn into_range(&self) -> vk::ImageSubresourceRange;
vk::Offset3D { }
x: self.origin.0,
y: self.origin.1, impl IntoImageSubresourceLayers for vk::ImageSubresource {
z: self.origin.2, fn into_layers(&self) -> vk::ImageSubresourceLayers {
vk::ImageSubresourceLayers {
aspect_mask: self.aspect_mask,
mip_level: self.mip_level,
base_array_layer: self.array_layer,
layer_count: 1,
} }
} }
pub fn extent(&self) -> vk::Extent3D {
vk::Extent3D {
width: self.extent.0,
height: self.extent.1,
depth: self.extent.2,
} }
impl<T> IntoImageSubresourceRange for T
where
T: IntoImageSubresourceLayers,
{
fn into_range(&self) -> vk::ImageSubresourceRange {
self.into_layers().into_range()
}
}
impl IntoImageSubresourceRange for vk::ImageSubresourceLayers {
fn into_range(&self) -> vk::ImageSubresourceRange {
vk::ImageSubresourceRange {
aspect_mask: self.aspect_mask,
base_mip_level: self.mip_level,
level_count: 1,
base_array_layer: self.base_array_layer,
layer_count: self.layer_count,
}
}
}
impl IntoImageSubresourceRange for vk::ImageSubresourceRange {
fn into_range(&self) -> vk::ImageSubresourceRange {
*self
}
}
pub trait ImageSubresourceExt {
fn aspects(self) -> vk::ImageAspectFlags;
fn layers(self) -> MipRange;
fn mip_levels(self) -> MipRange;
}
impl ImageSubresourceExt for vk::ImageSubresourceLayers {
fn aspects(self) -> vk::ImageAspectFlags {
self.aspect_mask
}
fn layers(self) -> MipRange {
MipRange::new(
self.base_array_layer,
self.base_array_layer + self.layer_count,
)
}
fn mip_levels(self) -> MipRange {
MipRange::single(self.mip_level)
}
}
impl ImageSubresourceExt for vk::ImageSubresourceRange {
fn aspects(self) -> vk::ImageAspectFlags {
self.aspect_mask
}
fn layers(self) -> MipRange {
MipRange::new(
self.base_array_layer,
self.base_array_layer + self.layer_count,
)
}
fn mip_levels(self) -> MipRange {
MipRange::new(self.base_mip_level, self.base_mip_level + self.level_count)
}
}
impl ImageSubresourceExt for vk::ImageSubresource {
fn aspects(self) -> vk::ImageAspectFlags {
self.aspect_mask
}
fn layers(self) -> MipRange {
MipRange::single(self.mip_level)
}
fn mip_levels(self) -> MipRange {
MipRange::single(self.array_layer)
}
}
impl From<vk::ImageSubresourceRange> for Subresource {
fn from(value: vk::ImageSubresourceRange) -> Self {
Subresource::Texture(value)
} }
} }
@ -375,19 +403,6 @@ impl From<(BufferRange, vk::ImageLayout)> for ResourceRange {
} }
} }
impl From<(TextureRange, vk::ImageLayout)> for ResourceRange {
fn from((value, layout): (TextureRange, vk::ImageLayout)) -> Self {
ResourceRange::Texture {
aspect: value.aspect,
mip_level: value.mip_levels,
array_layers: value.array_layers,
origin: value.origin,
extent: value.extent,
layout,
}
}
}
impl From<BufferRange> for ResourceRange { impl From<BufferRange> for ResourceRange {
fn from(value: BufferRange) -> Self { fn from(value: BufferRange) -> Self {
ResourceRange::Buffer { ResourceRange::Buffer {
@ -397,20 +412,6 @@ impl From<BufferRange> for ResourceRange {
} }
} }
impl TextureRange {
pub fn full() -> Self {
Self {
aspect: vk::ImageAspectFlags::COLOR
| vk::ImageAspectFlags::DEPTH
| vk::ImageAspectFlags::STENCIL,
mip_levels: MipRange::default(),
array_layers: MipRange::default(),
origin: (0, 0, 0),
extent: (u32::MAX, u32::MAX, u32::MAX),
}
}
}
pub struct Buffer(pub ResourceId); pub struct Buffer(pub ResourceId);
impl Buffer { impl Buffer {
@ -425,6 +426,7 @@ impl Buffer {
} }
} }
#[derive(Debug, Clone)]
pub struct Texture(pub ResourceId); pub struct Texture(pub ResourceId);
pub struct BufferSlice { pub struct BufferSlice {
@ -447,21 +449,10 @@ pub struct BufferRowSlices {
pub rows: Vec<BufferRows>, pub rows: Vec<BufferRows>,
} }
pub struct TextureRegion { pub struct TextureRegion<T: IntoImageSubresourceRange = vk::ImageSubresourceRange> {
pub texture: Texture, pub range: T,
pub range: TextureRange, pub offset: vk::Offset3D,
} pub extent: vk::Extent3D,
pub struct TextureRegions {
pub texture: Texture,
pub ranges: Vec<TextureRange>,
}
pub struct TextureView {
pub texture: Texture,
pub format: vk::Format,
pub mapping: vk::ComponentMapping,
pub range: TextureRange,
} }
pub struct Read<T>(pub T); pub struct Read<T>(pub T);

View file

@ -1,16 +1,18 @@
use std::sync::Arc; use std::{mem::ManuallyDrop, sync::Arc};
use ash::vk;
use glam::{f32::Mat4, vec3};
use gpu_allocator::MemoryLocation;
pub use crate::egui_pass::{egui_pass, egui_pre_pass}; pub use crate::egui_pass::{egui_pass, egui_pre_pass};
use ash::vk;
use glam::{
camera::rh::{proj, view},
vec3,
};
use crate::{ use crate::{
Result, Result,
buffers::{Buffer, BufferDesc}, buffers::{Buffer, BufferDesc},
commands::{self, traits::CommandBufferExt}, commands::{self, traits::CommandBufferExt},
device::Device, device::{Device, MemoryLocation},
images::ImageViewDesc, images::ImageViewDesc,
pipeline, pipeline,
render_graph::{ render_graph::{
@ -161,11 +163,7 @@ impl Wireframe {
); );
} }
let future = cmd.submit_async( let future = cmd.submit_async(None, None, Arc::new(sync::Fence::from_pool(&dev, None)?))?;
None,
None,
Arc::new(sync::Fence::from_pool(&dev.pools.fences, None)?),
)?;
let pipeline = Self::create_pipeline(dev.clone())?; let pipeline = Self::create_pipeline(dev.clone())?;
@ -319,19 +317,21 @@ impl Wireframe {
let cmd = &ctx.cmd; let cmd = &ctx.cmd;
//let model = Mat4::from_scale(vec3(0.01, 0.01, 0.01)); //let model = Mat4::from_scale(vec3(0.01, 0.01, 0.01));
let proj = Mat4::perspective_lh( let proj = proj::vulkan::perspective(
core::f32::consts::FRAC_PI_4, core::f32::consts::FRAC_PI_4,
target.width() as f32 / target.height() as f32, target.width() as f32 / target.height() as f32,
0.1, 0.1,
100.0, 100.0,
); );
let pos = vec3(0.0, 0.0, -5.0); let pos = vec3(0.0, 0.0, -5.0);
let view = Mat4::look_at_rh(pos, pos + vec3(0.0, 0.0, 1.0), vec3(0.0, 1.0, 0.0)); let view = view::look_at_mat4(pos, pos + vec3(0.0, 0.0, 1.0), vec3(0.0, 1.0, 0.0));
let to_clip = proj * view; // * model; let to_clip = proj * view; // * model;
let color_attachment = &vk::RenderingAttachmentInfo::default() let color_attachment = &vk::RenderingAttachmentInfo::default()
.image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) .image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
.image_view(target.create_view(ImageViewDesc::color_2d())?.raw()) .image_view(
ManuallyDrop::new(target.create_view(ImageViewDesc::color_2d())?).raw(),
)
.load_op(vk::AttachmentLoadOp::LOAD) .load_op(vk::AttachmentLoadOp::LOAD)
.store_op(vk::AttachmentStoreOp::STORE); .store_op(vk::AttachmentStoreOp::STORE);

View file

@ -1,5 +1,4 @@
use std::{ use std::{
mem::ManuallyDrop,
num::NonZero, num::NonZero,
ops::Deref, ops::Deref,
sync::{ sync::{
@ -15,6 +14,7 @@ use ash::{
}; };
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use raw_window_handle::{RawDisplayHandle, RawWindowHandle}; use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
use renderer_device::queue::QueueFamilyIndices;
use crate::{ use crate::{
Instance, PhysicalDeviceInfo, Result, SurfaceCapabilities, Instance, PhysicalDeviceInfo, Result, SurfaceCapabilities,
@ -147,7 +147,12 @@ impl Surface {
adapter: &PhysicalDeviceInfo, adapter: &PhysicalDeviceInfo,
config: &mut SwapchainConfiguration, config: &mut SwapchainConfiguration,
) -> Result<SurfaceCapabilities> { ) -> Result<SurfaceCapabilities> {
let surface_caps = instance.get_adapter_surface_capabilities(adapter.pdev, self, None)?; let surface_caps = instance.get_adapter_surface_capabilities(
adapter.pdev,
&self.functor,
self.raw,
None,
)?;
let max_image_dim = adapter.properties.core.limits.max_image_dimension2_d; let max_image_dim = adapter.properties.core.limits.max_image_dimension2_d;
if config.extent.width > max_image_dim || config.extent.height > max_image_dim { if config.extent.width > max_image_dim || config.extent.height > max_image_dim {
@ -203,7 +208,8 @@ impl Surface {
// capabilities to get the correct image counts. // capabilities to get the correct image counts.
let surface_caps = instance.get_adapter_surface_capabilities( let surface_caps = instance.get_adapter_surface_capabilities(
adapter.pdev, adapter.pdev,
self, &self.functor,
self.raw,
Some(config.present_mode), Some(config.present_mode),
)?; )?;
@ -240,7 +246,12 @@ impl Surface {
instance: &Instance, instance: &Instance,
adapter: &PhysicalDeviceInfo, adapter: &PhysicalDeviceInfo,
) -> Result<SwapchainConfiguration> { ) -> Result<SwapchainConfiguration> {
let surface_caps = instance.get_adapter_surface_capabilities(adapter.pdev, self, None)?; let surface_caps = instance.get_adapter_surface_capabilities(
adapter.pdev,
&self.functor,
self.raw,
None,
)?;
let present_mode = surface_caps let present_mode = surface_caps
.present_modes .present_modes
@ -422,7 +433,7 @@ impl Swapchain {
} else { } else {
vk::SharingMode::CONCURRENT vk::SharingMode::CONCURRENT
}) })
.queue_family_indices(queue_families) .queue_family_indices(queue_families.as_u32_slice())
.pre_transform(vk::SurfaceTransformFlagsKHR::IDENTITY) .pre_transform(vk::SurfaceTransformFlagsKHR::IDENTITY)
.composite_alpha(config.composite_alpha_mode) .composite_alpha(config.composite_alpha_mode)
.old_swapchain(old_swapchain) .old_swapchain(old_swapchain)
@ -531,7 +542,7 @@ impl Swapchain {
tracing::trace!(frame, "acquiring image for frame {frame}"); tracing::trace!(frame, "acquiring image for frame {frame}");
async move { async move {
let fence = Fence::from_pool(&self.swapchain.device().pools.fences, None)?; let fence = Fence::from_pool(self.swapchain.device(), None)?;
let acquire = self.acquire_semaphores[frame]; let acquire = self.acquire_semaphores[frame];
// spawn on threadpool because it might block. // spawn on threadpool because it might block.
@ -551,7 +562,17 @@ impl Swapchain {
let idx = idx as usize; let idx = idx as usize;
let image = self.images[idx]; let image = self.images[idx];
let image = Arc::new(images::Image::from_swapchain_image(image, &self)); let image = Arc::new(images::Image::from_swapchain_raw(
image,
self.swapchain.device().clone(),
self.config.format,
vk::Extent3D {
width: self.config.extent.width,
height: self.config.extent.height,
depth: 1,
},
self.config.usage,
));
let view = image.create_view(ImageViewDesc { let view = image.create_view(ImageViewDesc {
name: Some(format!("swapchain-{:x}-image-view-{idx}", self.raw().as_raw()).into()), name: Some(format!("swapchain-{:x}-image-view-{idx}", self.raw().as_raw()).into()),
kind: vk::ImageViewType::TYPE_2D, kind: vk::ImageViewType::TYPE_2D,
@ -567,7 +588,7 @@ impl Swapchain {
SwapchainImage { SwapchainImage {
index: idx as u32, index: idx as u32,
swapchain: self, swapchain: self,
view: ManuallyDrop::into_inner(view), view,
acquire, acquire,
release, release,
}, },
@ -832,7 +853,7 @@ mod tests {
let surface = Arc::new(Surface::headless(&instance)?); let surface = Arc::new(Surface::headless(&instance)?);
let adapter = instance.choose_adapter_default(Some(&surface), &[], Some(&features))?; let adapter = instance.choose_adapter_default(Some(surface.raw), &[], Some(&features))?;
let device = adapter.create_logical_device(&instance, &[], features, None)?; let device = adapter.create_logical_device(&instance, &[], features, None)?;
surface.configure( surface.configure(

View file

@ -1,5 +1,4 @@
use std::{ use std::{
borrow::Cow,
mem::ManuallyDrop, mem::ManuallyDrop,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
}; };
@ -7,75 +6,7 @@ use std::{
use ash::vk; use ash::vk;
use bytemuck::{Pod, Zeroable}; use bytemuck::{Pod, Zeroable};
pub(crate) mod weak_vec { pub use renderer_resources::util::{F32, eq_f32, hash_f32, ord_f32};
#![allow(dead_code)]
//! Module containing the [`WeakVec`] API.
use std::{sync::Weak, vec::Vec};
/// An optimized container for `Weak` references of `T` that minimizes reallocations by
/// dropping older elements that no longer have strong references to them.
#[derive(Debug)]
pub(crate) struct WeakVec<T> {
inner: Vec<Weak<T>>,
}
impl<T> Default for WeakVec<T> {
fn default() -> Self {
Self {
inner: Default::default(),
}
}
}
impl<T> WeakVec<T> {
pub(crate) fn new() -> Self {
Self { inner: Vec::new() }
}
/// Pushes a new element to this collection.
///
/// If the inner Vec needs to be reallocated, we will first drop older elements that
/// no longer have strong references to them.
pub(crate) fn push(&mut self, value: Weak<T>) {
if self.inner.len() == self.inner.capacity() {
// Iterating backwards has the advantage that we don't do more work than we have to.
for i in (0..self.inner.len()).rev() {
if self.inner[i].strong_count() == 0 {
self.inner.swap_remove(i);
}
}
// Make sure our capacity is twice the number of live elements.
// Leaving some spare capacity ensures that we won't re-scan immediately.
self.inner.reserve_exact(self.inner.len());
}
self.inner.push(value);
}
}
pub(crate) struct WeakVecIter<T> {
inner: std::vec::IntoIter<Weak<T>>,
}
impl<T> Iterator for WeakVecIter<T> {
type Item = Weak<T>;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
}
impl<T> IntoIterator for WeakVec<T> {
type Item = Weak<T>;
type IntoIter = WeakVecIter<T>;
fn into_iter(self) -> Self::IntoIter {
WeakVecIter {
inner: self.inner.into_iter(),
}
}
}
}
#[derive(derive_more::Debug)] #[derive(derive_more::Debug)]
pub(crate) struct DropGuard(#[debug(skip)] ManuallyDrop<Box<dyn FnOnce()>>); pub(crate) struct DropGuard(#[debug(skip)] ManuallyDrop<Box<dyn FnOnce()>>);
@ -94,62 +25,6 @@ impl Drop for DropGuard {
} }
} }
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct DebugName {
#[cfg(debug_assertions)]
name: Cow<'static, str>,
}
impl core::fmt::Display for DebugName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &**self)
}
}
impl core::fmt::Debug for DebugName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &**self)
}
}
impl core::ops::Deref for DebugName {
type Target = str;
fn deref(&self) -> &Self::Target {
#[cfg(debug_assertions)]
return &self.name;
#[cfg(not(debug_assertions))]
return "FEATURE_DISABLED";
}
}
impl From<Cow<'static, str>> for DebugName {
fn from(_value: Cow<'static, str>) -> Self {
Self {
#[cfg(debug_assertions)]
name: _value,
}
}
}
impl From<&'static str> for DebugName {
fn from(_value: &'static str) -> Self {
Self {
#[cfg(debug_assertions)]
name: Cow::Borrowed(_value),
}
}
}
impl From<String> for DebugName {
fn from(_value: String) -> Self {
Self {
#[cfg(debug_assertions)]
name: Cow::Owned(_value),
}
}
}
#[macro_export] #[macro_export]
macro_rules! def_monotonic_id { macro_rules! def_monotonic_id {
($vis:vis $ty:ident) => { ($vis:vis $ty:ident) => {
@ -476,104 +351,6 @@ impl Rect2D {
} }
} }
pub fn eq_f32(lhs: f32, rhs: f32) -> bool {
use core::num::FpCategory::*;
match (lhs.classify(), rhs.classify()) {
(Zero, Zero) | (Nan, Nan) => true,
(Infinite, Infinite) => lhs.signum() == rhs.signum(),
_ => lhs == rhs,
}
}
pub fn ord_f32(lhs: f32, rhs: f32) -> core::cmp::Ordering {
use core::cmp::Ordering;
use core::num::FpCategory::*;
match (lhs.classify(), rhs.classify()) {
(Zero, Zero) | (Nan, Nan) => Ordering::Equal,
(Infinite, Infinite) => {
core::cmp::PartialOrd::partial_cmp(&lhs.signum(), &rhs.signum()).unwrap()
}
_ => core::cmp::PartialOrd::partial_cmp(&lhs, &rhs).unwrap(),
}
}
pub fn hash_f32<H: std::hash::Hasher>(state: &mut H, f: f32) {
use std::hash::Hash;
let classify = f.classify();
match classify {
std::num::FpCategory::Nan => (classify as u8).hash(state),
std::num::FpCategory::Infinite | std::num::FpCategory::Zero => {
(classify as u8, f.signum() as i8).hash(state)
}
std::num::FpCategory::Subnormal | std::num::FpCategory::Normal => f.to_bits().hash(state),
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy)]
pub struct F32(f32);
impl F32 {
#[inline]
pub fn from_bits(bits: u32) -> Self {
Self(f32::from_bits(bits))
}
#[inline]
pub fn into_bits(self) -> u32 {
self.0.to_bits()
}
}
impl From<f32> for F32 {
fn from(value: f32) -> Self {
Self(value)
}
}
impl From<F32> for f32 {
fn from(value: F32) -> Self {
value.0
}
}
impl Deref for F32 {
type Target = f32;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for F32 {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl PartialEq for F32 {
fn eq(&self, other: &Self) -> bool {
eq_f32(self.0, other.0)
}
}
impl Eq for F32 {}
impl PartialOrd for F32 {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(ord_f32(self.0, other.0))
}
}
impl Ord for F32 {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
ord_f32(self.0, other.0)
}
}
impl core::hash::Hash for F32 {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
hash_f32(state, self.0)
}
}
pub fn timed<T, F: FnOnce() -> T>(label: &str, f: F) -> T { pub fn timed<T, F: FnOnce() -> T>(label: &str, f: F) -> T {
let now = std::time::Instant::now(); let now = std::time::Instant::now();
let out = f(); let out = f();

View file

@ -0,0 +1,125 @@
# renderer_device
## Purpose
`renderer_device` is the Vulkan instance, logical-device, queue, synchronization, and device-owned-object layer extracted from `renderer`. It owns Vulkan device lifecycle and shared device services; higher-level resource and rendering abstractions belong in `renderer`.
## Module map
- `lib.rs`: crate error/result types, physical-device feature/property descriptions and queries, surface capabilities, extension helpers, and public module exports. `extern crate self as renderer_device` supports generated derive paths.
- `device.rs`: `Device`/`DeviceInner`, logical-device creation, pools, ownership traits/wrappers, extension and allocation helpers.
- `instance.rs`: `Instance`/`InstanceInner`, instance setup, adapter discovery/selection, surface capability queries, debug messenger lifecycle.
- `queue.rs`: queue-family selection, `Queue`, queue synchronization, and `DeviceQueues`.
- `sync.rs`: fence/semaphore wrappers and pools, `FenceFuture`, and background fence waiters.
- `pipeline_cache.rs`: keyed Vulkan pipeline cache with `pipeline_cache.bin` load/export.
- `debug.rs`: Vulkan debug callback forwarding messages to tracing.
- `testing.rs` (feature `testing`): `TestVulkan` creates real validation-enabled Vulkan devices and captures validation messages for smoke/integration tests.
- `renderer_macros/src/lib.rs`: separate workspace proc-macro source; derives `DeviceRef` for a struct field marked `#[device]`.
## Key types and ownership
- `Device(Arc<DeviceInner>)` is the cloneable owning handle. It dereferences to `DeviceInner`; equality is `Arc::ptr_eq`.
- `DeviceInner` keeps the raw `ash::Device`, VMA (`vk_mem`) allocator mutex, adapter and instance, queues, synchronization threadpool, extension loaders, and inline `DevicePools`.
- `Device::enabled_features()` returns the feature snapshot requested for logical-device creation; it is distinct from `Device::features()`, which reports adapter-supported features. Descriptor capability validation must use the enabled snapshot, not support data.
- `PhysicalDeviceInfo::create_logical_device` selects queues, enables requested/supported extensions and features, initializes the allocator/cache, and constructs `DeviceInner` with `Arc::new_cyclic`.
- `DeviceInner::dev`, `instance`, `queues`, `physical_device`, `main_queue`, `compute_queue`, and `transfer_queue` are the main convenience accessors for downstream code.
- `DevicePools` contains a `PipelineCache` wrapper plus pools for raw fences and binary/timeline semaphore newtypes. A `Pool<T>` stores bare handles in `Mutex<Vec<T>>` and a `Weak<DeviceInner>`; `get()` reuses a handle or creates one through `Pooled`.
- `PoolObject<T: Recycle>` strongly owns a `Device`; dropping it returns its handle to the relevant pool. The pool's weak reference prevents a `DeviceInner` ownership cycle.
- `asdf::DeviceObject<T>` strongly owns its `Device`, dereferences to its handle, and destroys that handle through `asdf::traits::ExternallyManagedObject<Device>` on drop. `DeviceOwned<T>` exposes the owning device and raw handle.
- `DeviceRef` supplies `device()` for `Device`, Arc/reference forms, and structs supported by the `DeviceRef` derive (`#[device]` identifies the field). `Pooled` creates pooled handles; `Recycle` returns them.
- The `DeviceRef` proc macro is implemented in the separate `renderer_macros` package and emits paths rooted at `::renderer_device::`.
- `sync::Fence` and `sync::Semaphore` distinguish `Dedicated` (`DeviceObject`) from `Pooled` (`PoolObject`) ownership. `SemaphoreInner`, `BinarySemaphore`, and `TimelineSemaphore` represent semaphore-kind handles.
- `PhysicalDeviceInfo`, `PhysicalDeviceFeatures`, `PhysicalDeviceProperties`, and `SurfaceCapabilities` carry adapter/capability data. `Instance::choose_adapter_default` accepts `Option<vk::SurfaceKHR>` (a raw handle), not a swapchain/surface wrapper.
- `Error` and `Result<T>` are the crate-wide error types; `Error` covers Vulkan/VMA (`vk_mem`), loading, I/O, and renderer-facing resource/capability failures.
- `InstanceDesc` configures borrowed instance extensions/layers, optional app metadata, and a raw display handle; its default requests surface and validation layers, with validation settings in debug builds. `Instance::new` creates a shared `Instance` and requests debug-utils plus optional capability-query extensions alongside requested/platform extensions.
- `Instance::expose_adapter` queries one adapter. `choose_adapter_default` ranks enumerated adapters against requested extensions/features and prefers discrete devices, then newer APIs, device-local memory, and texture size.
- `PhysicalDeviceFeatures` stores core, Vulkan 1.1/1.2/1.3, and optional mesh-shader feature structures; `push_to_device_create_info` chains enabled feature structs.
- `PhysicalDeviceProperties` stores supported extensions, core/versioned properties, optional mesh-shader properties, memory properties, and API version; `supports_extension` checks name and minimum version.
- `get_physical_device_features` and `get_physical_device_properties` query the feature/property chains. Extension query results are consumed by the instance and logical-device setup helpers.
- `DeviceExtensions` holds the debug-utils device loader and optional swapchain/mesh-shader loaders. `DeviceInner.enabled_extensions` records enabled extension names.
- `MemoryLocation` is the public allocator-independent memory preference; `AllocationDesc` combines it with a resource name and `AllocationStrategy` (`AllocatorManaged` or `Dedicated`). `GpuAllocation` wraps the private `vk_mem::Allocation` and retains memory properties needed by callers.
- `DeviceInner::allocate_for_buffer` / `allocate_for_image` obtain VMA allocations, and `bind_buffer_memory` / `bind_image_memory` bind them to Vulkan resources without exposing `vk_mem` types to upper layers. CPU-mapped locations require host-visible, host-coherent memory.
- `Allocation` represents `Owned`, `Shared`, or `Unmanaged` GPU allocations.
- `get_extensions` returns supported/enabled and unsupported requested extension sets; `get_layers` returns available requested layers or the supported/unsupported split.
- `Queue` shares a `QueueInner` through `Arc`; `with_locked` serializes operations on the Vulkan queue. `QueueFamily` records index, count, flags, granularity, and timestamp bits.
- `DeviceQueues` provides graphics, compute, and transfer queues; absent compute/transfer queues alias the graphics queue. Queue selection requires presentation support only when a display handle is provided.
- `DeviceQueueInfos` chooses queue families and produces `vk::DeviceQueueCreateInfo` values before `retrieve_queues` obtains the raw Vulkan queues.
- `QueueFlags` in `device.rs` describes requested graphics/async-compute/transfer/present families; `queue::QueueFlags` describes discovered Vulkan family capabilities. They are distinct types.
- `FenceFuture` waits and resets a fence, and `SyncThreadpool` schedules blocking fence waits for async wakers. `Fence::from_pool` and `Semaphore::from_pool` wrap recycled pool entries.
- `SemaphoreType` selects binary or timeline construction. `SemaphoreInner` wraps the common Vulkan handle while `BinarySemaphore` and `TimelineSemaphore` tag their respective pools.
- `Fence::wait_on`, `is_signaled`, and `reset` operate on either dedicated or pooled fence handles through their owning device.
- Pooled timeline semaphores are created at value zero; `Semaphore::from_pool` signals the requested initial value when it obtains one.
- `PipelineCache::new` computes its key from adapter vendor/API/device/driver properties and tries to load compatible data; cache persistence uses a magic and format version.
- `DebugName` is the wrapper for Vulkan object debug names.
- `testing::TestVulkan::acquire` returns a real Vulkan test device or skips with a reason; `expect` requires Vulkan. `take_messages`, `assert_no_validation_errors`, and `assert_no_warnings` inspect the captured debug-utils callback output. Run the device smoke tests using `direnv exec . cargo test -p renderer_device --features testing` from the workspace root.
- `Extension` is the name/version pair used by extension helpers and adapter creation. `make_extension!`, `get_extensions`, `get_layers`, and the support/intersection functions centralize Vulkan extension discovery.
- `SurfaceCapabilities` returns raw `SurfaceCapabilitiesKHR` plus the effective minimum image count, surface formats, and present modes queried for the raw surface.
- `DebugUtilsCreateInfo` is the small severity/message-type configuration record used with debug utilities; `DebugUtils` itself owns the instance loader and messenger handle.
- `InstanceExtensions` stores optional surface-capability-query extension loaders and an optional surface-maintenance marker.
- `DeviceHandle` is a separate unsafe mutable-handle destruction trait used by `vk::DescriptorPool`; the normal `DeviceObject` destruction path uses `ExternallyManagedObject` instead.
- `asdf::ExternallyManagedObject<T, O>` is the generic owner/inner wrapper: it uses `ManuallyDrop`, invokes `destroy` exactly once, and retains the owner until that call.
- `DeviceObject<T>` exposes `device()`, `handle()` for `Copy` handles, and optional `name()`. It only implements `DeviceOwned<T>` when `T` is a Vulkan handle.
- `Queue::with_locked` serializes calls on a queue; `DeviceQueues::family_indices` deduplicates family IDs for resource-sharing modes.
- `Fence::new_dedicated` and `Semaphore::new_dedicated` create per-object device-owned primitives; `from_pool` constructors create pooled sync objects.
- `FenceFuture::block` waits then resets. Polling an unsignaled future schedules a blocking waiter through `SyncThreadpool`; the pool's worker count is bounded by its configured maximum.
## Invariants and gotchas
- `asdf` is the literal legacy module name for `DeviceObject` and its traits. Do not silently rename paths; generated/client code refers to these APIs.
- Vulkan child handles left bare in pools at device teardown are implicitly released by `vkDestroyDevice`; pool entries must not independently retain device-owning wrappers.
- `_drop: DeviceDrop` is the private shutdown path: it waits for the device to idle, exports and destroys the pipeline cache to `pipeline_cache.bin`, then destroys the Vulkan device. The cache is also exposed as a copyable wrapper through `DevicePools` for pipeline creation.
- `Device` cloning is cheap and prolongs the full device lifetime; any live `DeviceObject`, `PoolObject`, sync wrapper, or other owner retaining a `Device` can delay shutdown.
- Device-owned wrapper structs live outside `DeviceInner` and its pools. Pools hold bare handles, never `DeviceObject` or `PoolObject` wrappers.
- Keep enabled-feature state distinct from adapter-supported feature queries: `enabled_features()` records what the logical device was created with, while `features()` is support information and must not be treated as enabled capability.
- `Pool::device()` upgrades its `Weak` and panics if the pool outlives its device; don't retain pools independently.
- `PoolObject` uses `ManuallyDrop` to transfer its inner handle to `Recycle` exactly once. New `Recycle` implementations must route to the matching pool and handle type.
- The derive parser requires a named-field struct with exactly one `#[device]` field and does not add generic bounds itself; generic field types must satisfy `DeviceRef` through the input declaration. Its generated paths require the crate self-alias in `lib.rs`.
- `vk::Semaphore` and `SemaphoreInner` have destruction implementations for dedicated ownership; pooled semaphore ownership instead recycles `SemaphoreInner` into a kind-specific pool.
- `vk::Buffer`, `vk::Image`, `vk::ImageView`, descriptor/pipeline objects, swapchains, semaphores, and GPU allocations have concrete destruction paths; inspect `device.rs` and `sync.rs` before introducing another handle wrapper.
- Queue `lock()`/`unlock()` and `DeviceInner::lock_queues`/`unlock_queues` are unsafe paired operations. Prefer `Queue::with_locked` or `wait_idle` unless the caller can uphold their contracts.
- `Instance` shares its raw Vulkan instance through `Arc`; `DebugUtils` owns and destroys its messenger. Device references retain their `Instance`.
- `superset_of` compares requested-true flags with `a != b`, which looks inverted for a superset test — verify before relying on adapter feature selection.
- Physical-device feature/property queries chain Vulkan 1.1/1.2/1.3 structures and optional mesh-shader data without checking each version's availability; property querying marks beyond-1.0 probing as not yet fallible.
- `choose_adapter_default` ignores its optional raw surface argument (`TODO`); it does not test surface compatibility.
- Instance/queue selection accepts `RawDisplayHandle`; surface capability lookup uses a raw `vk::SurfaceKHR`. This crate has no `renderer::swapchain::Surface` dependency.
- `queue::QueueFlags` and `device::QueueFlags` intentionally serve different purposes. Keep imports qualified when adding queue-family selection logic.
- `PipelineCache` persistence uses a single working-directory path and one cache record. Changes to the key or file header should account for compatibility with existing cache files.
- Logical-device setup always includes the swapchain extension in its required extension list, regardless of the caller's extra requested extensions.
- Queue-family selection uses the optional display handle to query presentation support and requires its chosen graphics family to be present-capable only when a display handle is provided.
- Extension selectors and feature checks drive selection; do not infer that every optional extension is enabled just because a loader type exists.
- `Instance::query_presentation_support` handles platform-specific display variants and has an unimplemented XCB branch.
- `DebugUtils` is stored inside `InstanceInner`; its destructor destroys the messenger, so retain the loader/messenger for the instance lifetime.
- `DeviceObject`'s concrete destruction impls are split between `device.rs` and `sync.rs`; search both before adding a duplicate or assuming a type has an implementation.
- `PhysicalDeviceInfo` construction can discard adapters that fail property queries; `Instance::choose_adapter` filters failed exposures before sorting.
- `DeviceQueueInfos::retrieve_queues` reuses the graphics queue handle for absent compute or transfer roles. These aliases share the same `QueueInner` lock.
- `QueueFamily::granularity` uses the source spelling `TransferGranuality`; avoid renaming it as unrelated cleanup.
- `sync::Semaphore::from_pool` signals pooled timeline semaphores to the provided value but does not reset them to zero first; ensure requested timeline value semantics remain valid on reuse.
- Fence waiters wake on successful waits, log and stop on non-timeout errors, and can time out of the worker pool after its idle duration.
- `PipelineCache` uses MD5 only to derive a local adapter key; this is a cache identity, not a security mechanism.
- Device-level extension loaders are constructed only for extensions recorded as enabled; callers should check optional loaders such as `device_extensions.swapchain` before use.
- `Device::PartialEq` compares `Arc` identity, not raw Vulkan handles or physical-device identity.
- `Instance::get_adapter_surface_capabilities` receives the surface loader separately from the raw handle and returns format/mode data alongside generic capabilities.
- In debug builds `DebugName` stores its string; in non-debug builds its dereference returns a placeholder and does not retain the supplied name. `debug_name_object` assigns names through debug utils.
- GPU allocation free is routed through `DeviceObject<GpuAllocation>` into the VMA allocator mutex with `free_memory`. Do not call allocator free on an allocation after ownership has moved or dropped.
- `ExternallyManagedObject<Device>` must destroy each owned handle once. The `vk::CommandBuffer` implementation is intentionally a no-op: its outer command-buffer wrapper frees it through its command pool. `vk::CommandPool` itself is really destroyed.
- Device handle types are hand-written around `DeviceObject` with `DeviceOwned` implementations where needed; the old `define_device_owned_handle!` macro no longer exists.
## Conventions
- Nightly crate features: `debug_closure_helpers`, `slice_partition_dedup`.
- Keep unsafe Vulkan destruction and queue locking contracts explicit; inspect each trait's safety requirements when adding implementations.
- Do not commit changes. Verify with `cargo check --workspace` and `cargo test -p renderer_device` from the workspace root.
- Keep `renderer_device` independent of higher-level render resource/swapchain wrapper types; public raw Vulkan handles and capability records form this crate's boundary.
## Change guidance
- A new device-owned Vulkan handle needs a concrete `ExternallyManagedObject<Device>` implementation before it can be wrapped in `DeviceObject<T>`.
- Keep `DeviceOwned<T>` aligned with its wrapper: `device()` returns the strong owner used for destruction, and `handle()` returns the wrapped raw handle.
- A pooled type needs both `Pooled::create_from_pool` and `Recycle::recycle`, a matching inline entry in `DevicePools`, and initialization from its `Weak<DeviceInner>`.
- Keep dedicated and pooled sync paths semantically separate. Reused fences may require reset; timeline semaphore reuse must honor counter values.
- `DeviceObject::new_debug_named` requires `DebugNameable`; use `DeviceObject::new` when no valid debug naming route exists.
- Never put `Device` or `Arc<Device>` inside a value stored inline in `DeviceInner` unless the ownership cycle is deliberately redesigned.
- Raw Vulkan calls must use the instance/device associated with their handle. Document synchronization and lifetime obligations when exposing raw handles.
- New queue operations must use `Queue::with_locked` or explicitly document their external-synchronization safety contract.
- When adding required features/extensions, update adapter support checks, physical-device queries, and logical-device enablement together.
- `required_extensions` warns when an additional requested device extension is unsupported and always includes swapchain; warnings are not a hard-failure policy.
- Keep platform surface creation out of adapter APIs; selection/capability methods use raw Vulkan surface and display handles.
- Pipeline cache data is an optimization: load failures fall back to an empty cache; export/write failures are logged during teardown.
- Preserve `extern crate self as renderer_device`: proc-macro expansion uses this absolute path inside crate tests as well as downstream code.
- The derive parser's diagnostics require named-field structs and exactly one `#[device]` field. Do not rely on the incomplete tuple-field branch.
- `get_available_extensions` ignores failed per-layer extension enumeration results; extension compatibility otherwise compares names and minimum spec versions.
- On `Instance`/`Device` teardown changes, inspect explicit `Drop` implementations and field drop order, not only local wrapper drops.

View file

@ -0,0 +1,25 @@
[package]
name = "renderer_device"
version = "0.1.0"
edition = "2024"
[features]
testing = []
[dependencies]
ash = { workspace = true }
ash-window = { workspace = true }
vk-mem = { workspace = true }
parking_lot = { workspace = true }
tracing = { workspace = true }
raw-window-handle = { workspace = true }
bitflags = { workspace = true }
tinyvec = { workspace = true }
thiserror = { workspace = true }
md-5 = { workspace = true }
crossbeam = "0.8.4"
dyn-clone = "1"
bytemuck = { version = "1.21.0", features = ["derive"] }
[dev-dependencies]
renderer_macros = { path = "../renderer/renderer_macros" }

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,6 @@ use crate::{
Error, PhysicalDeviceFeatures, PhysicalDeviceInfo, SurfaceCapabilities, Error, PhysicalDeviceFeatures, PhysicalDeviceInfo, SurfaceCapabilities,
device::{Extension, get_extensions, get_layers}, device::{Extension, get_extensions, get_layers},
get_physical_device_features, get_physical_device_properties, make_extension, get_physical_device_features, get_physical_device_properties, make_extension,
swapchain::Surface,
}; };
pub struct DebugUtilsCreateInfo { pub struct DebugUtilsCreateInfo {
@ -21,21 +20,24 @@ pub struct DebugUtilsCreateInfo {
pub message_type: vk::DebugUtilsMessageTypeFlagsEXT, pub message_type: vk::DebugUtilsMessageTypeFlagsEXT,
} }
pub(crate) struct InstanceExtensions { pub struct InstanceExtensions {
pub(crate) get_surface_capabilities2: Option<khr::get_surface_capabilities2::Instance>, pub get_surface_capabilities2: Option<khr::get_surface_capabilities2::Instance>,
pub(crate) surface_maintenance1: Option<()>, pub surface_maintenance1: Option<()>,
} }
pub struct InstanceInner { pub struct InstanceInner {
// Messengers must be destroyed while the raw instance is still alive. Keep this first so
// it drops before `raw` during instance teardown.
pub _debug_utils: DebugUtils,
pub raw: ash::Instance, pub raw: ash::Instance,
pub(crate) extensions: InstanceExtensions, pub api_version: u32,
pub extensions: InstanceExtensions,
pub entry: Entry, pub entry: Entry,
pub(crate) _debug_utils: DebugUtils,
} }
#[derive(Clone)] #[derive(Clone)]
pub struct Instance { pub struct Instance {
pub(crate) inner: Arc<InstanceInner>, pub inner: Arc<InstanceInner>,
} }
impl Deref for Instance { impl Deref for Instance {
@ -74,7 +76,7 @@ const DEBUG_LAYER_SETTINGS: &[vk::LayerSettingEXT] = &[
p_layer_name: VALIDATION_LAYER.as_ptr(), p_layer_name: VALIDATION_LAYER.as_ptr(),
p_setting_name: c"validate_best_practices".as_ptr(), p_setting_name: c"validate_best_practices".as_ptr(),
value_count: 1, value_count: 1,
p_values: &[1u8; 1] as *const u8 as _, p_values: &[vk::TRUE] as *const u32 as _,
ty: vk::LayerSettingTypeEXT::BOOL32, ty: vk::LayerSettingTypeEXT::BOOL32,
_marker: core::marker::PhantomData, _marker: core::marker::PhantomData,
}, },
@ -82,7 +84,7 @@ const DEBUG_LAYER_SETTINGS: &[vk::LayerSettingEXT] = &[
p_layer_name: VALIDATION_LAYER.as_ptr(), p_layer_name: VALIDATION_LAYER.as_ptr(),
p_setting_name: c"validate_best_practices_amd".as_ptr(), p_setting_name: c"validate_best_practices_amd".as_ptr(),
value_count: 1, value_count: 1,
p_values: &[1u8; 1] as *const u8 as _, p_values: &[vk::TRUE] as *const u32 as _,
ty: vk::LayerSettingTypeEXT::BOOL32, ty: vk::LayerSettingTypeEXT::BOOL32,
_marker: core::marker::PhantomData, _marker: core::marker::PhantomData,
}, },
@ -90,7 +92,7 @@ const DEBUG_LAYER_SETTINGS: &[vk::LayerSettingEXT] = &[
p_layer_name: VALIDATION_LAYER.as_ptr(), p_layer_name: VALIDATION_LAYER.as_ptr(),
p_setting_name: c"validate_sync".as_ptr(), p_setting_name: c"validate_sync".as_ptr(),
value_count: 1, value_count: 1,
p_values: &[1u8; 1] as *const u8 as _, p_values: &[vk::TRUE] as *const u32 as _,
ty: vk::LayerSettingTypeEXT::BOOL32, ty: vk::LayerSettingTypeEXT::BOOL32,
_marker: core::marker::PhantomData, _marker: core::marker::PhantomData,
}, },
@ -122,16 +124,14 @@ impl Instance {
unsafe { entry.try_enumerate_instance_version()? }.unwrap_or(vk::API_VERSION_1_0); unsafe { entry.try_enumerate_instance_version()? }.unwrap_or(vk::API_VERSION_1_0);
let app_info = vk::ApplicationInfo::default() let app_info = vk::ApplicationInfo::default()
.api_version(if version < vk::API_VERSION_1_1 { // ash/VMA support Vulkan 1.3, and the application must not request a
vk::API_VERSION_1_0 // version newer than the loader supports.
} else { .api_version(version.min(vk::API_VERSION_1_3))
// ash doesn't support 1.4 yet
vk::API_VERSION_1_3
})
.application_name(&app_name) .application_name(&app_name)
.application_version(desc.app_version) .application_version(desc.app_version)
.engine_name(c"Bevy Engine") .engine_name(c"Bevy Engine")
.engine_version(vk::make_api_version(0, 0, 1, 0)); .engine_version(vk::make_api_version(0, 0, 1, 0));
let api_version = app_info.api_version;
let mut validation_info = let mut validation_info =
vk::LayerSettingsCreateInfoEXT::default().settings(desc.layer_settings); vk::LayerSettingsCreateInfoEXT::default().settings(desc.layer_settings);
@ -151,8 +151,9 @@ impl Instance {
let mut requested_extensions = desc.instance_extensions.to_vec(); let mut requested_extensions = desc.instance_extensions.to_vec();
requested_extensions.push(make_extension!(ext::debug_utils as 1)); requested_extensions.push(make_extension!(ext::debug_utils as 1));
#[cfg(debug_assertions)] if !desc.layer_settings.is_empty() {
requested_extensions.push(make_extension!(ext::layer_settings)); requested_extensions.push(make_extension!(ext::layer_settings));
}
// These are wanted for surface capabilities querying, but not strictly // These are wanted for surface capabilities querying, but not strictly
// required, and are only supported on newer devices, especially on // required, and are only supported on newer devices, especially on
@ -224,9 +225,10 @@ impl Instance {
}; };
let instance = Arc::new(InstanceInner { let instance = Arc::new(InstanceInner {
raw: instance,
extensions,
_debug_utils: debug_utils, _debug_utils: debug_utils,
raw: instance,
api_version,
extensions,
entry, entry,
}); });
@ -255,37 +257,26 @@ impl Instance {
/// extensions are available, it will also query present mode compatibility /// extensions are available, it will also query present mode compatibility
/// information to filter the present modes based on the provided initial /// information to filter the present modes based on the provided initial
/// mode. /// mode.
pub(crate) fn get_adapter_surface_capabilities( pub fn get_adapter_surface_capabilities(
&self, &self,
pdev: vk::PhysicalDevice, pdev: vk::PhysicalDevice,
surface: &Surface, functor: &khr::surface::Instance,
raw: vk::SurfaceKHR,
present_mode: Option<vk::PresentModeKHR>, present_mode: Option<vk::PresentModeKHR>,
) -> crate::Result<SurfaceCapabilities> { ) -> crate::Result<SurfaceCapabilities> {
let formats = unsafe { let formats = unsafe { functor.get_physical_device_surface_formats(pdev, raw)? };
surface
.functor
.get_physical_device_surface_formats(pdev, surface.raw)?
};
let present_modes = unsafe { let present_modes =
surface unsafe { functor.get_physical_device_surface_present_modes(pdev, raw)? };
.functor
.get_physical_device_surface_present_modes(pdev, surface.raw)?
};
let generic_caps = unsafe { let generic_caps = unsafe { functor.get_physical_device_surface_capabilities(pdev, raw)? };
surface
.functor
.get_physical_device_surface_capabilities(pdev, surface.raw)?
};
let (capabilities, present_modes) = if let Some(ref functor) = let (capabilities, present_modes) = if let Some(ref functor) =
self.extensions.get_surface_capabilities2 self.extensions.get_surface_capabilities2
&& self.extensions.surface_maintenance1.is_some() && self.extensions.surface_maintenance1.is_some()
{ {
let mut capabilities = vk::SurfaceCapabilities2KHR::default(); let mut capabilities = vk::SurfaceCapabilities2KHR::default();
let mut surface_info = let mut surface_info = vk::PhysicalDeviceSurfaceInfo2KHR::default().surface(raw);
vk::PhysicalDeviceSurfaceInfo2KHR::default().surface(surface.raw());
let present_mode = present_mode.unwrap_or(present_modes[0]); let present_mode = present_mode.unwrap_or(present_modes[0]);
let mut surface_present_mode = let mut surface_present_mode =
@ -324,10 +315,7 @@ impl Instance {
}) })
} }
pub(crate) fn expose_adapter( pub fn expose_adapter(&self, pdev: vk::PhysicalDevice) -> crate::Result<PhysicalDeviceInfo> {
&self,
pdev: vk::PhysicalDevice,
) -> crate::Result<PhysicalDeviceInfo> {
let properties = get_physical_device_properties(&self.inner, pdev)?; let properties = get_physical_device_properties(&self.inner, pdev)?;
let features = get_physical_device_features(&self.inner, pdev, &properties); let features = get_physical_device_features(&self.inner, pdev, &properties);
@ -338,9 +326,9 @@ impl Instance {
}) })
} }
pub(crate) fn choose_adapter_default( pub fn choose_adapter_default(
&self, &self,
surface: Option<&Surface>, surface: Option<vk::SurfaceKHR>,
required_extensions: &[Extension], required_extensions: &[Extension],
required_features: Option<&PhysicalDeviceFeatures>, required_features: Option<&PhysicalDeviceFeatures>,
) -> crate::Result<PhysicalDeviceInfo> { ) -> crate::Result<PhysicalDeviceInfo> {
@ -453,7 +441,7 @@ impl Instance {
} }
/// So... basically this probably doesn't matter because the graphics queue will always be a present queue as well... /// So... basically this probably doesn't matter because the graphics queue will always be a present queue as well...
pub(crate) fn query_presentation_support( pub fn query_presentation_support(
&self, &self,
pdev: vk::PhysicalDevice, pdev: vk::PhysicalDevice,
queue_family: u32, queue_family: u32,
@ -497,7 +485,7 @@ impl Instance {
} }
} }
pub(crate) struct DebugUtils { pub struct DebugUtils {
pub instance: ext::debug_utils::Instance, pub instance: ext::debug_utils::Instance,
pub messenger: vk::DebugUtilsMessengerEXT, pub messenger: vk::DebugUtilsMessengerEXT,
} }

View file

@ -0,0 +1,668 @@
#![feature(debug_closure_helpers, slice_partition_dedup)]
extern crate self as renderer_device;
use crate::instance::InstanceInner;
use ash::{
ext,
vk::{self, SurfaceCapabilitiesKHR},
};
use dyn_clone::DynClone;
use std::{fmt::Debug, path::PathBuf};
mod debug;
pub mod device;
pub mod instance;
pub mod pipeline_cache;
pub mod queue;
pub mod sync;
#[cfg(feature = "testing")]
#[doc(hidden)]
pub mod testing;
pub use device::{Device, DeviceOwned, DeviceRef, Extension};
pub use instance::{DebugUtils, DebugUtilsCreateInfo, Instance};
pub use queue::QueueFamilyIndex;
#[cfg(test)]
mod device_ref_derive_tests {
#![allow(dead_code)]
use crate::device::{Device, DeviceRef};
use std::sync::Arc;
#[derive(renderer_macros::DeviceRef)]
struct Wrapper {
#[device]
device: Device,
count: u32,
}
#[derive(renderer_macros::DeviceRef)]
struct ArcWrapper {
#[device]
device: Arc<Device>,
}
#[derive(renderer_macros::DeviceRef)]
struct GenericWrapper<D: DeviceRef> {
#[device]
device: D,
tag: &'static str,
}
#[test]
fn derives_impl_device_ref() {
fn assert_impl<T: DeviceRef>() {}
assert_impl::<Wrapper>();
assert_impl::<ArcWrapper>();
assert_impl::<GenericWrapper<Device>>();
assert_impl::<GenericWrapper<Arc<Device>>>();
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("No Physical Device found for this instance.")]
NoAdapter,
#[error("Swapchain suboptimal.")]
SuboptimalSwapchain,
#[error(transparent)]
LoadingError(#[from] ash::LoadingError),
#[error(transparent)]
Result(#[from] ash::vk::Result),
#[error(transparent)]
CStrError(#[from] core::ffi::c_str::FromBytesUntilNulError),
#[error(transparent)]
Utf8Error(#[from] core::str::Utf8Error),
#[error(transparent)]
NulError(#[from] std::ffi::NulError),
#[error("No Physical Device found.")]
NoPhysicalDevice,
#[error(
"Dedicated allocation requires Vulkan 1.1 or both VK_KHR_dedicated_allocation and VK_KHR_get_memory_requirements2 extensions."
)]
DedicatedAllocationRequiresApi11,
#[error(
"Offset-aware memory binding requires Vulkan 1.1 or VK_KHR_get_memory_requirements2, VK_KHR_bind_memory2, and VK_KHR_dedicated_allocation to be enabled."
)]
OffsetBindingRequiresApi11,
#[error(
"No memory type matches requirements mask {memory_type_bits:#010x} and required flags {property_flags:?}."
)]
NoCompatibleMemoryType {
memory_type_bits: u32,
property_flags: vk::MemoryPropertyFlags,
},
#[error(
"Memory allocation requirements request a dedicated allocation, which cannot be allocated without its resource."
)]
DedicatedAllocationRequiresResource,
#[error(
"Allocation offset {offset} is invalid: alignment is {alignment}, requested size is {required_size}, allocation size is {allocation_size}."
)]
InvalidAllocationOffset {
offset: u64,
alignment: u64,
required_size: u64,
allocation_size: u64,
},
#[error(
"Allocation memory type {memory_type_index} is not allowed by requirements mask {memory_type_bits:#010x}."
)]
IncompatibleAllocationMemoryType {
memory_type_index: u32,
memory_type_bits: u32,
},
#[error("The allocation and resource belong to different Vulkan devices.")]
AllocationDeviceMismatch,
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(
"Image dimensions ({width}x{height}) exceed the maximum allowed size of {max_size}x{max_size}."
)]
ImageTooLarge {
width: u32,
height: u32,
max_size: u32,
},
#[error("Image dimensions cannot be zero.")]
ImageZeroSized,
#[error("Incompatible image view kind {view_kind:?} for image kind {image_kind:?}.")]
IncompatibleImageViewKind {
view_kind: vk::ImageViewType,
image_kind: vk::ImageType,
},
#[error("Asset missing at path {0:?}")]
AssetMissing(PathBuf),
#[error("Unspecified Error")]
Unspecified,
#[error("BEEP BOOP create an error variant for {0} BEEP BOOP")]
Todo(&'static str),
}
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct DeviceExtension<'a> {
name: &'a core::ffi::CStr,
version: u32,
}
/// returns true if lhs and rhs have the same name and lhs spec_version is less
/// than or equal to rhs spec_version
#[allow(dead_code)]
pub fn compatible_extension_properties(
lhs: &vk::ExtensionProperties,
rhs: &vk::ExtensionProperties,
) -> bool {
let Some(lhs_name) = lhs.extension_name_as_c_str().ok() else {
return false;
};
let Some(rhs_name) = rhs.extension_name_as_c_str().ok() else {
return false;
};
if lhs_name == rhs_name {
lhs.spec_version <= rhs.spec_version
} else {
false
}
}
pub trait ExtendsDeviceFeatures2Debug:
vk::ExtendsPhysicalDeviceFeatures2 + Debug + Send + Sync
{
}
pub trait ExtendsDeviceProperties2Debug:
vk::ExtendsPhysicalDeviceProperties2 + Debug + DynClone + Send + Sync
{
}
impl<T: vk::ExtendsPhysicalDeviceFeatures2 + Debug + Send + Sync> ExtendsDeviceFeatures2Debug
for T
{
}
impl<T: vk::ExtendsPhysicalDeviceProperties2 + Debug + DynClone + Send + Sync>
ExtendsDeviceProperties2Debug for T
{
}
#[derive(Debug)]
pub struct PhysicalDeviceInfo {
pub pdev: vk::PhysicalDevice,
pub properties: PhysicalDeviceProperties,
pub features: PhysicalDeviceFeatures,
}
#[derive(Debug)]
pub struct SurfaceCapabilities {
pub capabilities: SurfaceCapabilitiesKHR,
/// The minimum number of images that must be present in the swapchain for
/// the surface for any present mode supported by the surface.
pub min_image_count: u32,
pub formats: Vec<vk::SurfaceFormatKHR>,
pub present_modes: Vec<vk::PresentModeKHR>,
}
#[derive(Default, Debug, Clone)]
pub struct PhysicalDeviceFeatures {
pub core: vk::PhysicalDeviceFeatures,
pub core11: vk::PhysicalDeviceVulkan11Features<'static>,
pub core12: vk::PhysicalDeviceVulkan12Features<'static>,
pub core13: vk::PhysicalDeviceVulkan13Features<'static>,
pub mesh_shader: Option<vk::PhysicalDeviceMeshShaderFeaturesEXT<'static>>,
}
impl PhysicalDeviceFeatures {
pub fn push_to_device_create_info<'a>(
&'a mut self,
mut create_info: vk::DeviceCreateInfo<'a>,
) -> vk::DeviceCreateInfo<'a> {
create_info = create_info
.enabled_features(&self.core)
.push_next(&mut self.core11)
.push_next(&mut self.core12)
.push_next(&mut self.core13);
if let Some(mesh_shader) = &mut self.mesh_shader {
create_info = create_info.push_next(mesh_shader);
}
create_info
}
pub fn superset_of(&self, other: &PhysicalDeviceFeatures) -> bool {
fn core_superset_of(
a: &vk::PhysicalDeviceFeatures,
b: &vk::PhysicalDeviceFeatures,
) -> bool {
(b.robust_buffer_access == vk::FALSE
|| a.robust_buffer_access != b.robust_buffer_access)
&& (b.full_draw_index_uint32 == vk::FALSE
|| a.full_draw_index_uint32 != b.full_draw_index_uint32)
&& (b.image_cube_array == vk::FALSE || a.image_cube_array != b.image_cube_array)
&& (b.independent_blend == vk::FALSE || a.independent_blend != b.independent_blend)
&& (b.geometry_shader == vk::FALSE || a.geometry_shader != b.geometry_shader)
&& (b.tessellation_shader == vk::FALSE
|| a.tessellation_shader != b.tessellation_shader)
&& (b.sample_rate_shading == vk::FALSE
|| a.sample_rate_shading != b.sample_rate_shading)
&& (b.dual_src_blend == vk::FALSE || a.dual_src_blend != b.dual_src_blend)
&& (b.logic_op == vk::FALSE || a.logic_op != b.logic_op)
&& (b.multi_draw_indirect == vk::FALSE
|| a.multi_draw_indirect != b.multi_draw_indirect)
&& (b.draw_indirect_first_instance == vk::FALSE
|| a.draw_indirect_first_instance != b.draw_indirect_first_instance)
&& (b.depth_clamp == vk::FALSE || a.depth_clamp != b.depth_clamp)
&& (b.depth_bias_clamp == vk::FALSE || a.depth_bias_clamp != b.depth_bias_clamp)
&& (b.fill_mode_non_solid == vk::FALSE
|| a.fill_mode_non_solid != b.fill_mode_non_solid)
&& (b.depth_bounds == vk::FALSE || a.depth_bounds != b.depth_bounds)
&& (b.wide_lines == vk::FALSE || a.wide_lines != b.wide_lines)
&& (b.large_points == vk::FALSE || a.large_points != b.large_points)
&& (b.alpha_to_one == vk::FALSE || a.alpha_to_one != b.alpha_to_one)
&& (b.multi_viewport == vk::FALSE || a.multi_viewport != b.multi_viewport)
&& (b.sampler_anisotropy == vk::FALSE
|| a.sampler_anisotropy != b.sampler_anisotropy)
&& (b.texture_compression_etc2 == vk::FALSE
|| a.texture_compression_etc2 != b.texture_compression_etc2)
&& (b.texture_compression_astc_ldr == vk::FALSE
|| a.texture_compression_astc_ldr != b.texture_compression_astc_ldr)
&& (b.texture_compression_bc == vk::FALSE
|| a.texture_compression_bc != b.texture_compression_bc)
&& (b.occlusion_query_precise == vk::FALSE
|| a.occlusion_query_precise != b.occlusion_query_precise)
&& (b.pipeline_statistics_query == vk::FALSE
|| a.pipeline_statistics_query != b.pipeline_statistics_query)
&& (b.vertex_pipeline_stores_and_atomics == vk::FALSE
|| a.vertex_pipeline_stores_and_atomics != b.vertex_pipeline_stores_and_atomics)
&& (b.fragment_stores_and_atomics == vk::FALSE
|| a.fragment_stores_and_atomics != b.fragment_stores_and_atomics)
&& (b.shader_tessellation_and_geometry_point_size == vk::FALSE
|| a.shader_tessellation_and_geometry_point_size
!= b.shader_tessellation_and_geometry_point_size)
&& (b.shader_image_gather_extended == vk::FALSE
|| a.shader_image_gather_extended != b.shader_image_gather_extended)
&& (b.shader_storage_image_extended_formats == vk::FALSE
|| a.shader_storage_image_extended_formats
!= b.shader_storage_image_extended_formats)
&& (b.shader_storage_image_multisample == vk::FALSE
|| a.shader_storage_image_multisample != b.shader_storage_image_multisample)
&& (b.shader_storage_image_read_without_format == vk::FALSE
|| a.shader_storage_image_read_without_format
!= b.shader_storage_image_read_without_format)
&& (b.shader_storage_image_write_without_format == vk::FALSE
|| a.shader_storage_image_write_without_format
!= b.shader_storage_image_write_without_format)
&& (b.shader_uniform_buffer_array_dynamic_indexing == vk::FALSE
|| a.shader_uniform_buffer_array_dynamic_indexing
!= b.shader_uniform_buffer_array_dynamic_indexing)
&& (b.shader_sampled_image_array_dynamic_indexing == vk::FALSE
|| a.shader_sampled_image_array_dynamic_indexing
!= b.shader_sampled_image_array_dynamic_indexing)
&& (b.shader_storage_buffer_array_dynamic_indexing == vk::FALSE
|| a.shader_storage_buffer_array_dynamic_indexing
!= b.shader_storage_buffer_array_dynamic_indexing)
&& (b.shader_storage_image_array_dynamic_indexing == vk::FALSE
|| a.shader_storage_image_array_dynamic_indexing
!= b.shader_storage_image_array_dynamic_indexing)
&& (b.shader_clip_distance == vk::FALSE
|| a.shader_clip_distance != b.shader_clip_distance)
&& (b.shader_cull_distance == vk::FALSE
|| a.shader_cull_distance != b.shader_cull_distance)
&& (b.shader_float64 == vk::FALSE || a.shader_float64 != b.shader_float64)
&& (b.shader_int64 == vk::FALSE || a.shader_int64 != b.shader_int64)
&& (b.shader_int16 == vk::FALSE || a.shader_int16 != b.shader_int16)
&& (b.shader_resource_residency == vk::FALSE
|| a.shader_resource_residency != b.shader_resource_residency)
&& (b.shader_resource_min_lod == vk::FALSE
|| a.shader_resource_min_lod != b.shader_resource_min_lod)
&& (b.sparse_binding == vk::FALSE || a.sparse_binding != b.sparse_binding)
&& (b.sparse_residency_buffer == vk::FALSE
|| a.sparse_residency_buffer != b.sparse_residency_buffer)
&& (b.sparse_residency_image2_d == vk::FALSE
|| a.sparse_residency_image2_d != b.sparse_residency_image2_d)
&& (b.sparse_residency_image3_d == vk::FALSE
|| a.sparse_residency_image3_d != b.sparse_residency_image3_d)
&& (b.sparse_residency2_samples == vk::FALSE
|| a.sparse_residency2_samples != b.sparse_residency2_samples)
&& (b.sparse_residency4_samples == vk::FALSE
|| a.sparse_residency4_samples != b.sparse_residency4_samples)
&& (b.sparse_residency8_samples == vk::FALSE
|| a.sparse_residency8_samples != b.sparse_residency8_samples)
&& (b.sparse_residency16_samples == vk::FALSE
|| a.sparse_residency16_samples != b.sparse_residency16_samples)
&& (b.sparse_residency_aliased == vk::FALSE
|| a.sparse_residency_aliased != b.sparse_residency_aliased)
&& (b.variable_multisample_rate == vk::FALSE
|| a.variable_multisample_rate != b.variable_multisample_rate)
&& (b.inherited_queries == vk::FALSE || a.inherited_queries != b.inherited_queries)
}
fn core11_superset_of(
a: &vk::PhysicalDeviceVulkan11Features,
b: &vk::PhysicalDeviceVulkan11Features,
) -> bool {
(b.storage_buffer16_bit_access == vk::FALSE
|| a.storage_buffer16_bit_access != b.storage_buffer16_bit_access)
&& (b.uniform_and_storage_buffer16_bit_access == vk::FALSE
|| a.uniform_and_storage_buffer16_bit_access
!= b.uniform_and_storage_buffer16_bit_access)
&& (b.storage_push_constant16 == vk::FALSE
|| a.storage_push_constant16 != b.storage_push_constant16)
&& (b.storage_input_output16 == vk::FALSE
|| a.storage_input_output16 != b.storage_input_output16)
&& (b.multiview == vk::FALSE || a.multiview != b.multiview)
&& (b.multiview_geometry_shader == vk::FALSE
|| a.multiview_geometry_shader != b.multiview_geometry_shader)
&& (b.multiview_tessellation_shader == vk::FALSE
|| a.multiview_tessellation_shader != b.multiview_tessellation_shader)
&& (b.variable_pointers_storage_buffer == vk::FALSE
|| a.variable_pointers_storage_buffer != b.variable_pointers_storage_buffer)
&& (b.variable_pointers == vk::FALSE || a.variable_pointers != b.variable_pointers)
&& (b.protected_memory == vk::FALSE || a.protected_memory != b.protected_memory)
&& (b.sampler_ycbcr_conversion == vk::FALSE
|| a.sampler_ycbcr_conversion != b.sampler_ycbcr_conversion)
&& (b.shader_draw_parameters == vk::FALSE
|| a.shader_draw_parameters != b.shader_draw_parameters)
}
fn core12_superset_of(
a: &vk::PhysicalDeviceVulkan12Features,
b: &vk::PhysicalDeviceVulkan12Features,
) -> bool {
(b.sampler_mirror_clamp_to_edge == vk::FALSE
|| a.sampler_mirror_clamp_to_edge != b.sampler_mirror_clamp_to_edge)
&& (b.draw_indirect_count == vk::FALSE
|| a.draw_indirect_count != b.draw_indirect_count)
&& (b.storage_buffer8_bit_access == vk::FALSE
|| a.storage_buffer8_bit_access != b.storage_buffer8_bit_access)
&& (b.uniform_and_storage_buffer8_bit_access == vk::FALSE
|| a.uniform_and_storage_buffer8_bit_access
!= b.uniform_and_storage_buffer8_bit_access)
&& (b.storage_push_constant8 == vk::FALSE
|| a.storage_push_constant8 != b.storage_push_constant8)
&& (b.shader_buffer_int64_atomics == vk::FALSE
|| a.shader_buffer_int64_atomics != b.shader_buffer_int64_atomics)
&& (b.shader_shared_int64_atomics == vk::FALSE
|| a.shader_shared_int64_atomics != b.shader_shared_int64_atomics)
&& (b.shader_float16 == vk::FALSE || a.shader_float16 != b.shader_float16)
&& (b.shader_int8 == vk::FALSE || a.shader_int8 != b.shader_int8)
&& (b.descriptor_indexing == vk::FALSE
|| a.descriptor_indexing != b.descriptor_indexing)
&& (b.shader_input_attachment_array_dynamic_indexing == vk::FALSE
|| a.shader_input_attachment_array_dynamic_indexing
!= b.shader_input_attachment_array_dynamic_indexing)
&& (b.shader_uniform_texel_buffer_array_dynamic_indexing == vk::FALSE
|| a.shader_uniform_texel_buffer_array_dynamic_indexing
!= b.shader_uniform_texel_buffer_array_dynamic_indexing)
&& (b.shader_storage_texel_buffer_array_dynamic_indexing == vk::FALSE
|| a.shader_storage_texel_buffer_array_dynamic_indexing
!= b.shader_storage_texel_buffer_array_dynamic_indexing)
&& (b.shader_uniform_buffer_array_non_uniform_indexing == vk::FALSE
|| a.shader_uniform_buffer_array_non_uniform_indexing
!= b.shader_uniform_buffer_array_non_uniform_indexing)
&& (b.shader_sampled_image_array_non_uniform_indexing == vk::FALSE
|| a.shader_sampled_image_array_non_uniform_indexing
!= b.shader_sampled_image_array_non_uniform_indexing)
&& (b.shader_storage_buffer_array_non_uniform_indexing == vk::FALSE
|| a.shader_storage_buffer_array_non_uniform_indexing
!= b.shader_storage_buffer_array_non_uniform_indexing)
&& (b.shader_storage_image_array_non_uniform_indexing == vk::FALSE
|| a.shader_storage_image_array_non_uniform_indexing
!= b.shader_storage_image_array_non_uniform_indexing)
&& (b.shader_input_attachment_array_non_uniform_indexing == vk::FALSE
|| a.shader_input_attachment_array_non_uniform_indexing
!= b.shader_input_attachment_array_non_uniform_indexing)
&& (b.shader_uniform_texel_buffer_array_non_uniform_indexing == vk::FALSE
|| a.shader_uniform_texel_buffer_array_non_uniform_indexing
!= b.shader_uniform_texel_buffer_array_non_uniform_indexing)
&& (b.shader_storage_texel_buffer_array_non_uniform_indexing == vk::FALSE
|| a.shader_storage_texel_buffer_array_non_uniform_indexing
!= b.shader_storage_texel_buffer_array_non_uniform_indexing)
&& (b.descriptor_binding_uniform_buffer_update_after_bind == vk::FALSE
|| a.descriptor_binding_uniform_buffer_update_after_bind
!= b.descriptor_binding_uniform_buffer_update_after_bind)
&& (b.descriptor_binding_sampled_image_update_after_bind == vk::FALSE
|| a.descriptor_binding_sampled_image_update_after_bind
!= b.descriptor_binding_sampled_image_update_after_bind)
&& (b.descriptor_binding_storage_image_update_after_bind == vk::FALSE
|| a.descriptor_binding_storage_image_update_after_bind
!= b.descriptor_binding_storage_image_update_after_bind)
&& (b.descriptor_binding_storage_buffer_update_after_bind == vk::FALSE
|| a.descriptor_binding_storage_buffer_update_after_bind
!= b.descriptor_binding_storage_buffer_update_after_bind)
&& (b.descriptor_binding_uniform_texel_buffer_update_after_bind == vk::FALSE
|| a.descriptor_binding_uniform_texel_buffer_update_after_bind
!= b.descriptor_binding_uniform_texel_buffer_update_after_bind)
&& (b.descriptor_binding_storage_texel_buffer_update_after_bind == vk::FALSE
|| a.descriptor_binding_storage_texel_buffer_update_after_bind
!= b.descriptor_binding_storage_texel_buffer_update_after_bind)
&& (b.descriptor_binding_update_unused_while_pending == vk::FALSE
|| a.descriptor_binding_update_unused_while_pending
!= b.descriptor_binding_update_unused_while_pending)
&& (b.descriptor_binding_partially_bound == vk::FALSE
|| a.descriptor_binding_partially_bound != b.descriptor_binding_partially_bound)
&& (b.descriptor_binding_variable_descriptor_count == vk::FALSE
|| a.descriptor_binding_variable_descriptor_count
!= b.descriptor_binding_variable_descriptor_count)
&& (b.runtime_descriptor_array == vk::FALSE
|| a.runtime_descriptor_array != b.runtime_descriptor_array)
&& (b.sampler_filter_minmax == vk::FALSE
|| a.sampler_filter_minmax != b.sampler_filter_minmax)
&& (b.scalar_block_layout == vk::FALSE
|| a.scalar_block_layout != b.scalar_block_layout)
&& (b.imageless_framebuffer == vk::FALSE
|| a.imageless_framebuffer != b.imageless_framebuffer)
&& (b.uniform_buffer_standard_layout == vk::FALSE
|| a.uniform_buffer_standard_layout != b.uniform_buffer_standard_layout)
&& (b.shader_subgroup_extended_types == vk::FALSE
|| a.shader_subgroup_extended_types != b.shader_subgroup_extended_types)
&& (b.separate_depth_stencil_layouts == vk::FALSE
|| a.separate_depth_stencil_layouts != b.separate_depth_stencil_layouts)
&& (b.host_query_reset == vk::FALSE || a.host_query_reset != b.host_query_reset)
&& (b.timeline_semaphore == vk::FALSE
|| a.timeline_semaphore != b.timeline_semaphore)
&& (b.buffer_device_address == vk::FALSE
|| a.buffer_device_address != b.buffer_device_address)
&& (b.buffer_device_address_capture_replay == vk::FALSE
|| a.buffer_device_address_capture_replay
!= b.buffer_device_address_capture_replay)
&& (b.buffer_device_address_multi_device == vk::FALSE
|| a.buffer_device_address_multi_device != b.buffer_device_address_multi_device)
&& (b.vulkan_memory_model == vk::FALSE
|| a.vulkan_memory_model != b.vulkan_memory_model)
&& (b.vulkan_memory_model_device_scope == vk::FALSE
|| a.vulkan_memory_model_device_scope != b.vulkan_memory_model_device_scope)
&& (b.vulkan_memory_model_availability_visibility_chains == vk::FALSE
|| a.vulkan_memory_model_availability_visibility_chains
!= b.vulkan_memory_model_availability_visibility_chains)
&& (b.shader_output_viewport_index == vk::FALSE
|| a.shader_output_viewport_index != b.shader_output_viewport_index)
&& (b.shader_output_layer == vk::FALSE
|| a.shader_output_layer != b.shader_output_layer)
&& (b.subgroup_broadcast_dynamic_id == vk::FALSE
|| a.subgroup_broadcast_dynamic_id != b.subgroup_broadcast_dynamic_id)
}
fn core13_superset_of(
a: &vk::PhysicalDeviceVulkan13Features,
b: &vk::PhysicalDeviceVulkan13Features,
) -> bool {
(b.robust_image_access == vk::FALSE || a.robust_image_access != b.robust_image_access)
&& (b.inline_uniform_block == vk::FALSE
|| a.inline_uniform_block != b.inline_uniform_block)
&& (b.descriptor_binding_inline_uniform_block_update_after_bind == vk::FALSE
|| a.descriptor_binding_inline_uniform_block_update_after_bind
!= b.descriptor_binding_inline_uniform_block_update_after_bind)
&& (b.pipeline_creation_cache_control == vk::FALSE
|| a.pipeline_creation_cache_control != b.pipeline_creation_cache_control)
&& (b.private_data == vk::FALSE || a.private_data != b.private_data)
&& (b.shader_demote_to_helper_invocation == vk::FALSE
|| a.shader_demote_to_helper_invocation != b.shader_demote_to_helper_invocation)
&& (b.shader_terminate_invocation == vk::FALSE
|| a.shader_terminate_invocation != b.shader_terminate_invocation)
&& (b.subgroup_size_control == vk::FALSE
|| a.subgroup_size_control != b.subgroup_size_control)
&& (b.compute_full_subgroups == vk::FALSE
|| a.compute_full_subgroups != b.compute_full_subgroups)
&& (b.synchronization2 == vk::FALSE || a.synchronization2 != b.synchronization2)
&& (b.texture_compression_astc_hdr == vk::FALSE
|| a.texture_compression_astc_hdr != b.texture_compression_astc_hdr)
&& (b.shader_zero_initialize_workgroup_memory == vk::FALSE
|| a.shader_zero_initialize_workgroup_memory
!= b.shader_zero_initialize_workgroup_memory)
&& (b.dynamic_rendering == vk::FALSE || a.dynamic_rendering != b.dynamic_rendering)
&& (b.shader_integer_dot_product == vk::FALSE
|| a.shader_integer_dot_product != b.shader_integer_dot_product)
&& (b.maintenance4 == vk::FALSE || a.maintenance4 != b.maintenance4)
}
fn mesh_shader_superset_of(
a: &vk::PhysicalDeviceMeshShaderFeaturesEXT,
b: &vk::PhysicalDeviceMeshShaderFeaturesEXT,
) -> bool {
(b.task_shader == vk::FALSE || a.task_shader != b.task_shader)
&& (b.mesh_shader == vk::FALSE || a.mesh_shader != b.mesh_shader)
&& (b.multiview_mesh_shader == vk::FALSE
|| a.multiview_mesh_shader != b.multiview_mesh_shader)
&& (b.primitive_fragment_shading_rate_mesh_shader == vk::FALSE
|| a.primitive_fragment_shading_rate_mesh_shader
!= b.primitive_fragment_shading_rate_mesh_shader)
&& (b.mesh_shader_queries == vk::FALSE
|| a.mesh_shader_queries != b.mesh_shader_queries)
}
core_superset_of(&self.core, &other.core)
&& core11_superset_of(&self.core11, &other.core11)
&& core12_superset_of(&self.core12, &other.core12)
&& core13_superset_of(&self.core13, &other.core13)
&& match (&self.mesh_shader, &other.mesh_shader) {
(Some(a), Some(b)) => mesh_shader_superset_of(a, b),
(None, Some(_)) => false,
_ => true,
}
}
}
#[derive(Debug, Default)]
pub struct PhysicalDeviceProperties {
/// Extensions supported by the device, as reported by `enumerate_device_extension_properties`.
pub supported_extensions: Vec<vk::ExtensionProperties>,
/// Vulkan 1.0 properties.
pub core: vk::PhysicalDeviceProperties,
/// Vulkan 1.1 properties.
pub core11: vk::PhysicalDeviceVulkan11Properties<'static>,
/// Vulkan 1.2 properties.
pub core12: vk::PhysicalDeviceVulkan12Properties<'static>,
/// Vulkan 1.3 properties.
pub core13: vk::PhysicalDeviceVulkan13Properties<'static>,
/// Mesh shader properties from the `VK_EXT_mesh_shader` extension, if supported.
pub mesh_shader: Option<vk::PhysicalDeviceMeshShaderPropertiesEXT<'static>>,
/// Memory properties of the device, as reported by `get_physical_device_memory_properties`.
pub memory_properties: vk::PhysicalDeviceMemoryProperties,
/// The API version supported by the device.
pub device_api_version: u32,
}
impl PhysicalDeviceProperties {
pub fn get_device_local_memory_count(&self) -> u64 {
self.memory_properties
.memory_heaps
.iter()
.filter(|heap| heap.flags.contains(vk::MemoryHeapFlags::DEVICE_LOCAL))
.map(|heap| heap.size)
.sum::<u64>()
}
}
pub fn get_physical_device_features(
instance: &InstanceInner,
pdev: vk::PhysicalDevice,
properties: &PhysicalDeviceProperties,
) -> PhysicalDeviceFeatures {
let mut features = PhysicalDeviceFeatures::default();
let mut features2 = vk::PhysicalDeviceFeatures2::default();
features2 = features2
.push_next(&mut features.core11)
.push_next(&mut features.core12)
.push_next(&mut features.core13);
if properties.supports_extension(make_extension!(ext::mesh_shader)) {
features2 = features2.push_next(
features
.mesh_shader
.insert(vk::PhysicalDeviceMeshShaderFeaturesEXT::default()),
);
}
unsafe {
instance
.raw
.get_physical_device_features2(pdev, &mut features2);
}
features.core = features2.features;
features
}
pub fn get_physical_device_properties(
instance: &InstanceInner,
pdev: vk::PhysicalDevice,
) -> Result<PhysicalDeviceProperties> {
let mut props = PhysicalDeviceProperties::default();
props.core = unsafe { instance.raw.get_physical_device_properties(pdev) };
props.device_api_version = props.core.api_version;
props.supported_extensions =
unsafe { instance.raw.enumerate_device_extension_properties(pdev)? };
props.memory_properties = unsafe { instance.raw.get_physical_device_memory_properties(pdev) };
let supports_mesh_shader = props.supports_extension(make_extension!(ext::mesh_shader));
// TODO: fallibly check anything beyond 1.0
let mut props2 = vk::PhysicalDeviceProperties2::default()
.push_next(&mut props.core11)
.push_next(&mut props.core12)
.push_next(&mut props.core13);
if supports_mesh_shader {
let next = props
.mesh_shader
.insert(vk::PhysicalDeviceMeshShaderPropertiesEXT::default());
props2 = props2.push_next(next);
}
unsafe {
instance
.raw
.get_physical_device_properties2(pdev, &mut props2);
}
Ok(props)
}
#[allow(dead_code)]
pub fn extension_intersection<'a>(
supported: &'a [vk::ExtensionProperties],
required: &[Extension<'a>],
) -> Vec<vk::ExtensionProperties> {
supported
.iter()
.filter(|ext| {
required.iter().any(|req| {
ext.extension_name_as_c_str() == Ok(req.name) && ext.spec_version >= req.version
})
})
.cloned()
.collect()
}
impl PhysicalDeviceProperties {
pub fn supports_extension(&self, e: Extension) -> bool {
self.supported_extensions
.iter()
.any(|ext| ext.extension_name_as_c_str() == Ok(e.name) && ext.spec_version >= e.version)
}
}

View file

@ -0,0 +1,110 @@
use ash::vk;
use ash::Device;
use crate::PhysicalDeviceInfo;
#[derive(Debug, Clone, Copy)]
pub struct PipelineCache {
#[allow(dead_code)]
key: u128,
pub raw: vk::PipelineCache,
}
impl PipelineCache {
pub(crate) unsafe fn destroy(self, device: &ash::Device) {
tracing::info!("destroying pipeline cache with key {:x}", self.key);
if let Ok(data) = self.export(device) {
tracing::info!(
"exported pipeline cache with key {:x} and size {} bytes",
self.key,
data.len()
);
_ = Self::write_to_disk(self.key, &data).inspect_err(|err| {
tracing::error!("failed to write pipeline cache to disk: {err}");
});
}
unsafe { device.destroy_pipeline_cache(self.raw, None) };
}
const MAGIC: [u8; 4] = *b"VYPC";
const KEY_VERSION: u32 = 1;
const PATH: &'static str = "pipeline_cache.bin";
fn calculate_key(adapter: &PhysicalDeviceInfo) -> u128 {
use md5::Digest;
let mut hasher = md5::Md5::new();
let props = &adapter.properties;
hasher.update(bytemuck::bytes_of(&[
props.core.vendor_id,
props.core.api_version,
props.core.device_id,
props.core.driver_version,
]));
u128::from_le_bytes(hasher.finalize().into())
}
fn load_from_disk(key: u128) -> Option<(u128, Vec<u8>)> {
use std::io::Read;
let file = std::fs::File::open(Self::PATH).ok()?;
let mut reader = std::io::BufReader::new(file);
let mut magic = [0; 4];
reader.read_exact(&mut magic).ok()?;
if magic != Self::MAGIC {
return None;
}
let mut version = 0;
reader
.read_exact(bytemuck::bytes_of_mut(&mut version))
.ok()?;
if version != Self::KEY_VERSION {
return None;
}
let mut disk_key = 0;
reader
.read_exact(bytemuck::bytes_of_mut(&mut disk_key))
.ok()?;
if disk_key != key {
return None;
}
let mut data = Vec::new();
reader.read_to_end(&mut data).ok()?;
Some((key, data))
}
fn write_to_disk(key: u128, data: &[u8]) -> std::io::Result<()> {
use std::io::Write;
let file = std::fs::File::create(Self::PATH)?;
let mut writer = std::io::BufWriter::new(file);
writer.write_all(&Self::MAGIC)?;
writer.write_all(bytemuck::bytes_of(&Self::KEY_VERSION))?;
writer.write_all(bytemuck::bytes_of(&key))?;
writer.write_all(data)?;
Ok(())
}
pub fn new(device: &Device, adapter: &PhysicalDeviceInfo) -> crate::Result<Self> {
let key = Self::calculate_key(adapter);
let data = Self::load_from_disk(key).map(|(key, data)| {
tracing::info!("loaded pipeline cache from disk with key {key:x}");
data
});
let info = vk::PipelineCacheCreateInfo::default()
// .flags(vk::PipelineCacheCreateFlags::EXTERNALLY_SYNCHRONIZED)
.initial_data(data.as_deref().unwrap_or_default());
let cache = unsafe { device.create_pipeline_cache(&info, None)? };
Ok(Self { key, raw: cache })
}
pub fn export(&self, device: &ash::Device) -> crate::Result<Vec<u8>> {
let data = unsafe { device.get_pipeline_cache_data(self.raw)? };
Ok(data)
}
}

View file

@ -1,4 +1,5 @@
use bitflags::bitflags; use bitflags::bitflags;
use core::fmt;
use parking_lot::Mutex; use parking_lot::Mutex;
use raw_window_handle::RawDisplayHandle; use raw_window_handle::RawDisplayHandle;
use std::{collections::HashMap, ops::Deref, sync::Arc}; use std::{collections::HashMap, ops::Deref, sync::Arc};
@ -36,35 +37,73 @@ impl Queue {
F: FnOnce(&Queue) -> R, F: FnOnce(&Queue) -> R,
{ {
let _lock = self.inner.lock.lock(); let _lock = self.inner.lock.lock();
f(&self) f(self)
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct QueueInner { pub struct QueueInner {
pub(crate) raw: vk::Queue, pub raw: vk::Queue,
pub(crate) family: QueueFamily, pub family: QueueFamily,
pub(crate) lock: Mutex<()>, pub lock: Mutex<()>,
} }
impl QueueInner { impl QueueInner {
pub fn raw(&self) -> vk::Queue { pub fn raw(&self) -> vk::Queue {
self.raw self.raw
} }
pub fn family_index(&self) -> u32 { pub fn family_index(&self) -> QueueFamilyIndex {
self.family.index self.family.index
} }
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct QueueFamily { pub struct QueueFamily {
pub index: u32, pub index: QueueFamilyIndex,
pub count: u32, pub count: u32,
pub flags: QueueFlags, pub flags: QueueFlags,
pub granularity: TransferGranuality, pub granularity: TransferGranuality,
pub timestamp_valid_bits: u32, pub timestamp_valid_bits: u32,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct QueueFamilyIndex(u32);
pub trait QueueFamilyIndices {
fn as_u32_slice(&self) -> &[u32];
}
impl QueueFamilyIndices for [QueueFamilyIndex] {
fn as_u32_slice(&self) -> &[u32] {
// SAFETY: QueueFamilyIndex is repr(transparent) over u32
unsafe { std::slice::from_raw_parts(self.as_ptr() as *const u32, self.len()) }
}
}
impl Default for QueueFamilyIndex {
fn default() -> Self {
Self::INVALID
}
}
impl fmt::Display for QueueFamilyIndex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "q{}", self.0)
}
}
impl QueueFamilyIndex {
const INVALID: QueueFamilyIndex = QueueFamilyIndex(u32::MAX);
pub fn new(index: u32) -> Self {
Self(index)
}
pub fn index(&self) -> u32 {
self.0
}
}
bitflags! { bitflags! {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct QueueFlags: u16 { pub struct QueueFlags: u16 {
@ -143,8 +182,11 @@ impl DeviceQueues {
&self.transfer &self.transfer
} }
pub fn family_indices(&self, flags: crate::device::QueueFlags) -> ArrayVec<[u32; 4]> { pub fn family_indices(
let mut indices = array_vec!([u32; 4]); &self,
flags: crate::device::QueueFlags,
) -> ArrayVec<[QueueFamilyIndex; 4]> {
let mut indices = array_vec!([QueueFamilyIndex; 4]);
use crate::device::QueueFlags as QF; use crate::device::QueueFlags as QF;
if flags.intersects(QF::GRAPHICS | QF::PRESENT) { if flags.intersects(QF::GRAPHICS | QF::PRESENT) {
indices.push(self.graphics.family.index); indices.push(self.graphics.family.index);
@ -160,7 +202,7 @@ impl DeviceQueues {
indices indices
} }
pub fn swapchain_family_indices(&self) -> &[u32] { pub fn swapchain_family_indices(&self) -> &[QueueFamilyIndex] {
core::slice::from_ref(&self.graphics.family.index) core::slice::from_ref(&self.graphics.family.index)
} }
@ -203,13 +245,13 @@ impl DeviceQueueInfos {
.into_iter() .into_iter()
.map(|(index, count)| { .map(|(index, count)| {
vk::DeviceQueueCreateInfo::default() vk::DeviceQueueCreateInfo::default()
.queue_family_index(index) .queue_family_index(index.index())
.queue_priorities(&Self::PRIORITIES[..count as usize]) .queue_priorities(&Self::PRIORITIES[..count as usize])
}) })
.collect() .collect()
} }
fn queue_family_indices(&self) -> HashMap<u32, u32> { fn queue_family_indices(&self) -> HashMap<QueueFamilyIndex, u32> {
let mut families = HashMap::new(); let mut families = HashMap::new();
families.insert(self.graphics.index, 1); families.insert(self.graphics.index, 1);
@ -231,12 +273,12 @@ impl DeviceQueueInfos {
.map(|(queue_family_index, count)| { .map(|(queue_family_index, count)| {
let queues = (0..count) let queues = (0..count)
.map(|queue_index| unsafe { .map(|queue_index| unsafe {
dev.get_device_queue(queue_family_index, queue_index) dev.get_device_queue(queue_family_index.index(), queue_index)
}) })
.collect(); .collect();
(queue_family_index, queues) (queue_family_index, queues)
}) })
.collect::<HashMap<u32, Vec<vk::Queue>>>(); .collect::<HashMap<QueueFamilyIndex, Vec<vk::Queue>>>();
let graphics = Queue::from_vk_queue( let graphics = Queue::from_vk_queue(
queues.get_mut(&self.graphics.index).unwrap().pop().unwrap(), queues.get_mut(&self.graphics.index).unwrap().pop().unwrap(),
@ -285,15 +327,14 @@ impl DeviceQueueInfos {
.enumerate() .enumerate()
.map(|(index, props)| { .map(|(index, props)| {
let mut flags = QueueFlags::from_vk_flags(props.queue_flags); let mut flags = QueueFlags::from_vk_flags(props.queue_flags);
if let Some(display_handle) = display_handle { if let Some(display_handle) = display_handle
if instance.query_presentation_support(pdev.pdev, index as u32, display_handle) && instance.query_presentation_support(pdev.pdev, index as u32, display_handle)
{ {
flags |= QueueFlags::PRESENT; flags |= QueueFlags::PRESENT;
} }
}
QueueFamily { QueueFamily {
index: index as u32, index: QueueFamilyIndex(index as u32),
count: props.queue_count, count: props.queue_count,
flags, flags,
granularity: TransferGranuality::from_vk_granularity( granularity: TransferGranuality::from_vk_granularity(
@ -323,10 +364,12 @@ impl DeviceQueueInfos {
}) })
.expect("No graphics queue family found"); .expect("No graphics queue family found");
if display_handle.is_some() {
assert!( assert!(
graphics.flags.contains(QueueFlags::PRESENT), graphics.flags.contains(QueueFlags::PRESENT),
"Selected graphics queue family does not support presentation" "Selected graphics queue family does not support presentation"
); );
}
let compute = queue_families let compute = queue_families
.iter() .iter()

View file

@ -6,11 +6,8 @@ use std::{
}; };
use crate::device::{ use crate::device::{
DevicePools, Pool, PoolObject, Pooled, Pool, PoolObject, Pooled, Recycle,
asdf::{ asdf::{DeviceObject, traits::ExternallyManagedObject as ExternallyManagedObjectTrait},
DeviceObject, InnerDeviceObject,
traits::ExternallyManagedObject as ExternallyManagedObjectTrait,
},
}; };
use crate::{Result, device::DeviceInner}; use crate::{Result, device::DeviceInner};
@ -62,7 +59,7 @@ impl SyncThreadpool {
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
match self match self
.num_threads .num_threads
.fetch_update(Ordering::Release, Ordering::Acquire, |i| { .try_update(Ordering::Release, Ordering::Acquire, |i| {
if i < self.max_threads { if i < self.max_threads {
Some(i + 1) Some(i + 1)
} else { } else {
@ -159,18 +156,14 @@ impl SyncThreadpool {
} }
pub enum Fence { pub enum Fence {
Dedicated { Dedicated { fence: DeviceObject<vk::Fence> },
fence: InnerDeviceObject<vk::Fence>, Pooled { fence: PoolObject<vk::Fence> },
},
Pooled {
fence: PoolObject<vk::Fence, Arc<Pool<vk::Fence>>>,
},
} }
impl Pooled for vk::Fence { impl Pooled for vk::Fence {
fn create_from_pool(pool: &Pool<Self>) -> Result<Self> { fn create_from_pool(pool: &Pool<Self>) -> Result<Self> {
let fence = unsafe { let fence = unsafe {
pool.device pool.device()
.raw .raw
.create_fence(&vk::FenceCreateInfo::default(), None)? .create_fence(&vk::FenceCreateInfo::default(), None)?
}; };
@ -179,17 +172,15 @@ impl Pooled for vk::Fence {
} }
} }
impl ExternallyManagedObjectTrait<Arc<Pool<vk::Fence>>> for vk::Fence { impl Recycle for vk::Fence {
unsafe fn destroy(self, pool: &Arc<Pool<vk::Fence>>) { fn recycle(self, device: &Device) {
pool.push(self); device.pools.fences.push(self);
} }
} }
impl<T: AsRef<DeviceInner>> ExternallyManagedObjectTrait<T> for vk::Fence { impl ExternallyManagedObjectTrait<Device> for vk::Fence {
unsafe fn destroy(self, device: &T) { unsafe fn destroy(self, device: &Device) {
unsafe { unsafe { device.raw.destroy_fence(self, None) };
device.as_ref().raw.destroy_fence(self, None);
}
} }
} }
@ -207,14 +198,14 @@ impl Fence {
.create_fence(&vk::FenceCreateInfo::default(), None)? .create_fence(&vk::FenceCreateInfo::default(), None)?
}; };
Ok(Self::Dedicated { Ok(Self::Dedicated {
fence: DeviceObject::new_debug_named(device.shared.clone(), fence, name), fence: DeviceObject::new_debug_named(device, fence, name),
}) })
} }
pub fn from_pool(pool: &Arc<Pool<vk::Fence>>, name: Option<&'static str>) -> Result<Fence> { pub fn from_pool(device: &Device, name: Option<&'static str>) -> Result<Fence> {
let fence = pool.get_debug_named(name)?; let fence = device.pools.fences.get_debug_named(name)?;
Ok(Self::Pooled { Ok(Self::Pooled {
fence: PoolObject::new(fence, pool.clone()), fence: PoolObject::new(fence, device.clone()),
}) })
} }
@ -225,10 +216,10 @@ impl Fence {
} }
} }
fn device(&self) -> &Arc<DeviceInner> { fn device(&self) -> &Device {
match self { match self {
Fence::Dedicated { fence } => &fence.device(), Fence::Dedicated { fence } => fence.device(),
Fence::Pooled { fence } => &fence.owner().device, Fence::Pooled { fence } => fence.device(),
} }
} }
@ -278,21 +269,23 @@ pub enum SemaphoreInner {
Timeline(vk::Semaphore), Timeline(vk::Semaphore),
} }
impl ExternallyManagedObjectTrait<Arc<DevicePools>> for SemaphoreInner { impl Recycle for SemaphoreInner {
unsafe fn destroy(self, owner: &Arc<DevicePools>) { fn recycle(self, owner: &Device) {
match self { match self {
SemaphoreInner::Binary(semaphore) => { SemaphoreInner::Binary(semaphore) => owner
owner.binary_semaphores.push(BinarySemaphore(semaphore)) .pools
} .binary_semaphores
SemaphoreInner::Timeline(semaphore) => { .push(BinarySemaphore(semaphore)),
owner.timeline_semaphores.push(TimelineSemaphore(semaphore)) SemaphoreInner::Timeline(semaphore) => owner
} .pools
.timeline_semaphores
.push(TimelineSemaphore(semaphore)),
} }
} }
} }
impl ExternallyManagedObjectTrait<Arc<DeviceInner>> for SemaphoreInner { impl ExternallyManagedObjectTrait<Device> for SemaphoreInner {
unsafe fn destroy(self, owner: &Arc<DeviceInner>) { unsafe fn destroy(self, owner: &Device) {
match self { match self {
SemaphoreInner::Binary(semaphore) | SemaphoreInner::Timeline(semaphore) => { SemaphoreInner::Binary(semaphore) | SemaphoreInner::Timeline(semaphore) => {
unsafe { owner.raw.destroy_semaphore(semaphore, None) }; unsafe { owner.raw.destroy_semaphore(semaphore, None) };
@ -336,18 +329,18 @@ impl From<TimelineSemaphore> for SemaphoreInner {
pub enum Semaphore { pub enum Semaphore {
Dedicated { Dedicated {
semaphore: InnerDeviceObject<SemaphoreInner>, semaphore: DeviceObject<SemaphoreInner>,
}, },
Pooled { Pooled {
#[allow(private_interfaces)] #[allow(private_interfaces)]
semaphore: PoolObject<SemaphoreInner, Arc<DevicePools>>, semaphore: PoolObject<SemaphoreInner>,
}, },
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub(crate) struct BinarySemaphore(pub(crate) vk::Semaphore); pub struct BinarySemaphore(pub vk::Semaphore);
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub(crate) struct TimelineSemaphore(pub(crate) vk::Semaphore); pub struct TimelineSemaphore(pub vk::Semaphore);
// This is just so that ash can name these semaphore newtypes // This is just so that ash can name these semaphore newtypes
impl vk::Handle for BinarySemaphore { impl vk::Handle for BinarySemaphore {
@ -379,7 +372,7 @@ impl Pooled for BinarySemaphore {
let mut type_info = let mut type_info =
vk::SemaphoreTypeCreateInfo::default().semaphore_type(vk::SemaphoreType::BINARY); vk::SemaphoreTypeCreateInfo::default().semaphore_type(vk::SemaphoreType::BINARY);
let create_info = vk::SemaphoreCreateInfo::default().push_next(&mut type_info); let create_info = vk::SemaphoreCreateInfo::default().push_next(&mut type_info);
let inner = unsafe { pool.device.raw.create_semaphore(&create_info, None)? }; let inner = unsafe { pool.device().raw.create_semaphore(&create_info, None)? };
Ok(Self(inner)) Ok(Self(inner))
} }
} }
@ -390,7 +383,7 @@ impl Pooled for TimelineSemaphore {
.semaphore_type(vk::SemaphoreType::TIMELINE) .semaphore_type(vk::SemaphoreType::TIMELINE)
.initial_value(0); .initial_value(0);
let create_info = vk::SemaphoreCreateInfo::default().push_next(&mut type_info); let create_info = vk::SemaphoreCreateInfo::default().push_next(&mut type_info);
let inner = unsafe { pool.device.raw.create_semaphore(&create_info, None)? }; let inner = unsafe { pool.device().raw.create_semaphore(&create_info, None)? };
Ok(Self(inner)) Ok(Self(inner))
} }
} }
@ -421,7 +414,7 @@ impl Semaphore {
}; };
Ok(Self::Dedicated { Ok(Self::Dedicated {
semaphore: DeviceObject::new_debug_named(device.shared.clone(), inner, name), semaphore: DeviceObject::new_debug_named(device, inner, name),
}) })
} }
@ -434,7 +427,7 @@ impl Semaphore {
SemaphoreType::Binary => { SemaphoreType::Binary => {
let semaphore: SemaphoreInner = let semaphore: SemaphoreInner =
device.pools.binary_semaphores.get_debug_named(name)?.into(); device.pools.binary_semaphores.get_debug_named(name)?.into();
PoolObject::new(semaphore, device.pools.clone()) PoolObject::new(semaphore, device)
} }
SemaphoreType::Timeline(value) => { SemaphoreType::Timeline(value) => {
let semaphore: SemaphoreInner = device let semaphore: SemaphoreInner = device
@ -450,7 +443,7 @@ impl Semaphore {
device.raw.signal_semaphore(&info)?; device.raw.signal_semaphore(&info)?;
} }
PoolObject::new(semaphore, device.pools.clone()) PoolObject::new(semaphore, device)
} }
}; };

View file

@ -0,0 +1,513 @@
//! Real Vulkan test devices with Khronos validation output captured for assertions.
//!
//! Run the device smoke tests with `cargo test -p renderer_device --features testing`.
use std::{
ffi::{CStr, c_void},
fmt,
sync::{Arc, Mutex},
};
use ash::{Entry, ext, khr, vk};
use crate::{
Device, Instance, PhysicalDeviceFeatures,
instance::{DebugUtils, InstanceDesc, VALIDATION_LAYER},
};
const TEST_LAYER_SETTINGS: &[vk::LayerSettingEXT<'static>] = &[
vk::LayerSettingEXT {
p_layer_name: VALIDATION_LAYER.as_ptr(),
p_setting_name: c"validate_sync".as_ptr(),
value_count: 1,
p_values: &[vk::TRUE] as *const u32 as _,
ty: vk::LayerSettingTypeEXT::BOOL32,
_marker: core::marker::PhantomData,
},
vk::LayerSettingEXT {
p_layer_name: VALIDATION_LAYER.as_ptr(),
p_setting_name: c"validate_best_practices".as_ptr(),
value_count: 1,
p_values: &[vk::TRUE] as *const u32 as _,
ty: vk::LayerSettingTypeEXT::BOOL32,
_marker: core::marker::PhantomData,
},
];
/// One validation callback message captured from a real Vulkan instance.
#[derive(Debug, Clone)]
pub struct CapturedMessage {
pub severity: vk::DebugUtilsMessageSeverityFlagsEXT,
pub types: vk::DebugUtilsMessageTypeFlagsEXT,
pub message: String,
}
impl fmt::Display for CapturedMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?} {:?}: {}", self.severity, self.types, self.message)
}
}
/// A Vulkan instance and logical device configured for validation-backed tests.
///
/// Instances are independent; tests may acquire their own device without sharing Vulkan state.
pub struct TestVulkan {
device: Device,
// Drop the callback messenger while its instance and user data are still alive.
_messenger: DebugUtils,
messages: Arc<Mutex<Vec<CapturedMessage>>>,
instance: Instance,
}
impl TestVulkan {
/// Attempt to create a Vulkan instance/device, printing a reason and returning `None` when
/// Vulkan, a physical device, or the Khronos validation layer is unavailable.
pub fn acquire() -> Option<Self> {
match Self::create() {
Ok(test) => {
eprintln!(
"renderer_device Vulkan test adapter: {}",
test.device
.properties()
.core
.device_name_as_c_str()
.map(CStr::to_string_lossy)
.unwrap_or_else(|_| "<invalid device name>".into())
);
Some(test)
}
Err(reason) => {
eprintln!("Skipping Vulkan test: {reason}");
None
}
}
}
/// Create a validation-backed Vulkan test device or panic. Useful for CI jobs where Vulkan is
/// a required test dependency.
pub fn expect() -> Self {
Self::acquire().expect("Vulkan test device is required")
}
fn create() -> Result<Self, String> {
// Do not rewrite loader environment variables here. The loader's default search paths and
// environment provided by the development shell are both supported.
let entry = unsafe { Entry::load() }.map_err(|error| format!("loading Vulkan: {error}"))?;
let layers = unsafe { entry.enumerate_instance_layer_properties() }
.map_err(|error| format!("enumerating instance layers: {error}"))?;
if !layers
.iter()
.any(|layer| layer.layer_name_as_c_str().ok() == Some(VALIDATION_LAYER))
{
return Err("VK_LAYER_KHRONOS_validation is not available".into());
}
let instance = Instance::new(&InstanceDesc {
app_name: Some("renderer_device validation tests"),
instance_extensions: &[crate::make_extension!(khr::surface)],
layer_settings: TEST_LAYER_SETTINGS,
layers: &[VALIDATION_LAYER],
display_handle: None,
..Default::default()
})
.map_err(|error| format!("creating Vulkan instance: {error}"))?;
let messages = Arc::new(Mutex::new(Vec::new()));
let debug_loader =
ext::debug_utils::Instance::new(&instance.inner.entry, &instance.inner.raw);
let create_info = vk::DebugUtilsMessengerCreateInfoEXT::default()
.message_severity(
vk::DebugUtilsMessageSeverityFlagsEXT::ERROR
| vk::DebugUtilsMessageSeverityFlagsEXT::WARNING,
)
.message_type(
vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
| vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION
| vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE,
)
.user_data(Arc::as_ptr(&messages) as *mut c_void)
.pfn_user_callback(Some(capture_callback));
let raw_messenger =
unsafe { debug_loader.create_debug_utils_messenger(&create_info, None) }
.map_err(|error| format!("creating validation message messenger: {error}"))?;
let messenger = DebugUtils {
instance: debug_loader,
messenger: raw_messenger,
};
let adapter = instance
.choose_adapter_default(None, &[], None)
.map_err(|error| format!("selecting physical device: {error}"))?;
let supported = &adapter.features;
let requested = PhysicalDeviceFeatures {
core: vk::PhysicalDeviceFeatures {
shader_sampled_image_array_dynamic_indexing: supported
.core
.shader_sampled_image_array_dynamic_indexing,
..Default::default()
},
core12: vk::PhysicalDeviceVulkan12Features {
descriptor_indexing: supported.core12.descriptor_indexing,
descriptor_binding_partially_bound: supported
.core12
.descriptor_binding_partially_bound,
descriptor_binding_update_unused_while_pending: supported
.core12
.descriptor_binding_update_unused_while_pending,
descriptor_binding_sampled_image_update_after_bind: supported
.core12
.descriptor_binding_sampled_image_update_after_bind,
descriptor_binding_storage_buffer_update_after_bind: supported
.core12
.descriptor_binding_storage_buffer_update_after_bind,
runtime_descriptor_array: supported.core12.runtime_descriptor_array,
timeline_semaphore: supported.core12.timeline_semaphore,
..Default::default()
},
core13: vk::PhysicalDeviceVulkan13Features {
synchronization2: supported.core13.synchronization2,
dynamic_rendering: supported.core13.dynamic_rendering,
maintenance4: supported.core13.maintenance4,
..Default::default()
},
..Default::default()
};
let device = adapter
.create_logical_device(&instance, &[], requested, None)
.map_err(|error| format!("creating logical device: {error}"))?;
Ok(Self {
device,
_messenger: messenger,
messages,
instance,
})
}
pub fn instance(&self) -> &Instance {
&self.instance
}
pub fn device(&self) -> &Device {
&self.device
}
pub fn physical_device(&self) -> &crate::PhysicalDeviceInfo {
self.device.physical_device()
}
/// Prints all warnings and fails only for validation errors.
pub fn assert_no_validation_errors(&self) {
let messages = self.take_messages();
for message in &messages {
if message
.severity
.contains(vk::DebugUtilsMessageSeverityFlagsEXT::WARNING)
{
eprintln!("Vulkan validation warning: {message}");
}
}
let errors = messages
.into_iter()
.filter(|message| {
message
.severity
.contains(vk::DebugUtilsMessageSeverityFlagsEXT::ERROR)
})
.collect::<Vec<_>>();
assert!(
errors.is_empty(),
"Vulkan validation errors:\n{}",
format_messages(&errors)
);
}
/// Fail for warnings as well as errors, for tests that explicitly require a clean log.
pub fn assert_no_warnings(&self) {
let messages = self.take_messages();
assert!(
messages.is_empty(),
"Vulkan validation messages:\n{}",
format_messages(&messages)
);
}
/// Drain captured messages; useful for tests that intentionally exercise a validation error.
pub fn take_messages(&self) -> Vec<CapturedMessage> {
std::mem::take(
&mut *self
.messages
.lock()
.expect("validation message lock poisoned"),
)
}
}
fn format_messages(messages: &[CapturedMessage]) -> String {
messages
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n")
}
unsafe extern "system" fn capture_callback(
severity: vk::DebugUtilsMessageSeverityFlagsEXT,
types: vk::DebugUtilsMessageTypeFlagsEXT,
callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>,
user_data: *mut c_void,
) -> vk::Bool32 {
if callback_data.is_null() || user_data.is_null() {
return vk::FALSE;
}
let data = unsafe { &*callback_data };
let message = if data.p_message.is_null() {
"<empty validation message>".into()
} else {
unsafe { CStr::from_ptr(data.p_message) }
.to_string_lossy()
.into_owned()
};
let messages = unsafe { &*(user_data as *const Mutex<Vec<CapturedMessage>>) };
if let Ok(mut messages) = messages.lock() {
messages.push(CapturedMessage {
severity,
types,
message,
});
}
vk::FALSE
}
#[cfg(test)]
mod tests {
use super::*;
use crate::device::{
Allocation, AllocationDesc, AllocationStrategy, DeviceObject, MemoryLocation,
};
#[test]
fn instance_and_device_creation() {
let Some(test) = TestVulkan::acquire() else {
return;
};
assert_ne!(test.instance().inner.raw.handle(), vk::Instance::null());
assert_ne!(test.device().dev().handle(), vk::Device::null());
test.device().wait_idle().expect("device wait idle");
test.assert_no_validation_errors();
}
#[test]
fn buffer_lifecycle_under_validation() {
let Some(test) = TestVulkan::acquire() else {
return;
};
let device = test.device();
let info = vk::BufferCreateInfo::default()
.size(256)
.usage(vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST)
.sharing_mode(vk::SharingMode::EXCLUSIVE);
let buffer = unsafe { device.dev().create_buffer(&info, None) }.expect("create buffer");
let buffer = DeviceObject::new(device.clone(), buffer);
let allocation = device
.allocate_for_buffer(
*buffer,
AllocationDesc {
name: "renderer_device validation smoke buffer",
location: MemoryLocation::CpuToGpu,
strategy: AllocationStrategy::AllocatorManaged,
},
)
.expect("allocate buffer memory");
let allocation = DeviceObject::new(device.clone(), allocation);
device
.bind_buffer_memory(&allocation, *buffer)
.expect("bind buffer memory");
drop(buffer);
drop(allocation);
test.assert_no_validation_errors();
}
#[test]
fn aliasable_buffer_allocation_binds_at_zero_under_validation() {
let Some(test) = TestVulkan::acquire() else {
return;
};
let device = test.device();
let create_info = vk::BufferCreateInfo::default()
.size(256)
.usage(vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST)
.sharing_mode(vk::SharingMode::EXCLUSIVE);
let buffer = unsafe { device.dev().create_buffer(&create_info, None) }
.expect("create aliasable buffer");
let buffer = DeviceObject::new(device.clone(), buffer);
let requirements = device
.buffer_memory_requirements(*buffer)
.expect("query buffer requirements");
let memory = device
.allocate_memory(&requirements, vk::MemoryPropertyFlags::empty())
.expect("allocate aliasable buffer memory");
let binding = device
.bind_buffer_at_offset(*buffer, &requirements, memory, 0)
.expect("bind aliasable buffer memory");
assert!(matches!(binding, Allocation::Shared(_)));
drop(buffer);
drop(binding);
test.assert_no_validation_errors();
}
#[test]
fn aliasable_image_allocation_binds_at_zero_under_validation() {
let Some(test) = TestVulkan::acquire() else {
return;
};
let device = test.device();
let create_info = vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(vk::Format::R8G8B8A8_UNORM)
.extent(vk::Extent3D {
width: 16,
height: 16,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::OPTIMAL)
.usage(vk::ImageUsageFlags::TRANSFER_DST)
.sharing_mode(vk::SharingMode::EXCLUSIVE)
.initial_layout(vk::ImageLayout::UNDEFINED);
let image = unsafe { device.dev().create_image(&create_info, None) }
.expect("create aliasable image");
let image = DeviceObject::new(device.clone(), image);
let requirements = device
.image_memory_requirements(*image)
.expect("query image requirements");
let memory = device
.allocate_memory(&requirements, vk::MemoryPropertyFlags::empty())
.expect("allocate aliasable image memory");
let binding = device
.bind_image_at_offset(*image, &requirements, memory, 0)
.expect("bind aliasable image memory");
assert!(matches!(binding, Allocation::Shared(_)));
drop(image);
drop(binding);
test.assert_no_validation_errors();
}
#[test]
fn aliasable_binding_rejects_out_of_bounds_and_unaligned_offsets() {
let Some(test) = TestVulkan::acquire() else {
return;
};
let device = test.device();
let create_info = vk::BufferCreateInfo::default()
.size(256)
.usage(vk::BufferUsageFlags::TRANSFER_DST)
.sharing_mode(vk::SharingMode::EXCLUSIVE);
let make_buffer = || {
let raw = unsafe { device.dev().create_buffer(&create_info, None) }
.expect("create offset validation buffer");
DeviceObject::new(device.clone(), raw)
};
let buffer = make_buffer();
let requirements = device
.buffer_memory_requirements(*buffer)
.expect("query offset validation requirements");
let memory = device
.allocate_memory(&requirements, vk::MemoryPropertyFlags::empty())
.expect("allocate offset validation memory");
assert!(matches!(
device.bind_buffer_at_offset(*buffer, &requirements, memory, requirements.size),
Err(crate::Error::InvalidAllocationOffset { .. })
));
drop(buffer);
let buffer = make_buffer();
let requirements = device
.buffer_memory_requirements(*buffer)
.expect("query unaligned validation requirements");
if requirements.alignment > 1 {
let memory = device
.allocate_memory(&requirements, vk::MemoryPropertyFlags::empty())
.expect("allocate unaligned validation memory");
assert!(matches!(
device.bind_buffer_at_offset(*buffer, &requirements, memory, 1),
Err(crate::Error::InvalidAllocationOffset { .. })
));
}
drop(buffer);
test.assert_no_validation_errors();
}
#[test]
fn validation_capture_works() {
let Some(test) = TestVulkan::acquire() else {
return;
};
let device = test.device();
let buffer_info = vk::BufferCreateInfo::default()
.size(16)
.usage(vk::BufferUsageFlags::TRANSFER_DST)
.sharing_mode(vk::SharingMode::EXCLUSIVE);
let buffer = unsafe { device.dev().create_buffer(&buffer_info, None) }
.expect("create validation probe buffer");
let buffer = DeviceObject::new(device.clone(), buffer);
let allocation = device
.allocate_for_buffer(
*buffer,
AllocationDesc {
name: "renderer_device validation probe buffer",
location: MemoryLocation::CpuToGpu,
strategy: AllocationStrategy::AllocatorManaged,
},
)
.expect("allocate validation probe buffer");
let allocation = DeviceObject::new(device.clone(), allocation);
device
.bind_buffer_memory(&allocation, *buffer)
.expect("bind validation probe buffer");
let pool_info = vk::CommandPoolCreateInfo::default()
.queue_family_index(device.queues.graphics().family.index.index());
let pool = unsafe { device.dev().create_command_pool(&pool_info, None) }
.expect("create command pool");
let allocate_info = vk::CommandBufferAllocateInfo::default()
.command_pool(pool)
.level(vk::CommandBufferLevel::PRIMARY)
.command_buffer_count(1);
let command_buffer = unsafe { device.dev().allocate_command_buffers(&allocate_info) }
.expect("allocate command buffer")[0];
unsafe {
device
.dev()
.begin_command_buffer(command_buffer, &vk::CommandBufferBeginInfo::default())
.expect("begin command buffer");
// The size must be a multiple of four. The invalid command is never submitted.
device
.dev()
.cmd_fill_buffer(command_buffer, *buffer, 0, 3, 0);
device
.dev()
.end_command_buffer(command_buffer)
.expect("end command buffer");
device.dev().destroy_command_pool(pool, None);
}
drop(buffer);
drop(allocation);
let messages = test.take_messages();
assert!(
messages.iter().any(|message| {
message
.severity
.contains(vk::DebugUtilsMessageSeverityFlagsEXT::ERROR)
&& message.message.contains("VUID-vkCmdFillBuffer-size")
}),
"expected a vkCmdFillBuffer size validation error, got:\n{}",
format_messages(&messages)
);
}
}

View file

@ -0,0 +1,22 @@
[package]
name = "renderer_rendergraph"
version = "0.1.0"
edition = "2024"
[dependencies]
ash.workspace = true
fixedbitset.workspace = true
tinyvec.workspace = true
renderer_device = { path = "../renderer_device" }
renderer_resources = { path = "../renderer_resources" }
ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" }
bitflags.workspace = true
[dev-dependencies]
criterion = { workspace = true }
renderer_resources = { path = "../renderer_resources", features = ["testing"] }
renderer_device = { path = "../renderer_device", features = ["testing"] }
[[bench]]
name = "compile"
harness = false

View file

@ -0,0 +1,381 @@
//! These graph shapes compare command-level batching with render-pass span
//! contraction and barrier hoisting. Each has two disjoint buffer access sets
//! joined by a shared-buffer dependency across the passes. The resolve benchmarks
//! include device-dependent placement, alias-barrier rebuilding, and pass hoisting
//! in the timed region.
use std::{hint::black_box, sync::Arc, time::Duration};
use ash::vk;
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
use renderer_device::device::{MemoryLocation, QueueFlags};
use renderer_device::testing::TestVulkan;
use renderer_rendergraph::{
Buffer, BufferRange, GraphBufferDesc, RenderGraph,
commands::{BeginRendering, Command, EndRendering},
recorder::{CommandIndex, CommandRecorder, RecordError, RecordedCommand, SideEffectMap},
resources::{ResourceState, SubresourceScope, SubresourceScopeInner},
};
use renderer_resources::{buffers::Buffer as DeviceBuffer, buffers::BufferDesc};
struct Use {
buffer: Buffer,
write: bool,
}
impl Command for Use {
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
map.insert(
command_index,
self.buffer.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::COMPUTE_SHADER,
access: if self.write {
vk::AccessFlags2::SHADER_WRITE
} else {
vk::AccessFlags2::SHADER_READ
},
subresource: SubresourceScopeInner::Buffer(BufferRange::full()),
},
);
}
}
impl RecordedCommand for Use {
fn allowed_in_render_pass(&self) -> bool {
true
}
fn apply(&mut self, _recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
Ok(())
}
}
struct UseWithBridge {
buffer: Buffer,
bridge: Buffer,
}
impl Command for UseWithBridge {
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
for buffer in [self.buffer, self.bridge] {
map.insert(
command_index,
buffer.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::COMPUTE_SHADER,
access: vk::AccessFlags2::SHADER_WRITE,
subresource: SubresourceScopeInner::Buffer(BufferRange::full()),
},
);
}
}
}
impl RecordedCommand for UseWithBridge {
fn allowed_in_render_pass(&self) -> bool {
true
}
fn apply(&mut self, _recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
Ok(())
}
}
fn build(n_groups: usize, per: usize, pass: bool) -> RenderGraph {
let mut graph = RenderGraph::new();
let a: Vec<Buffer> = (0..n_groups)
.map(|_| {
graph.create_buffer(GraphBufferDesc {
size: 4096,
..Default::default()
})
})
.collect();
let b: Vec<Buffer> = (0..n_groups)
.map(|_| {
graph.create_buffer(GraphBufferDesc {
size: 4096,
..Default::default()
})
})
.collect();
let shared = graph.create_buffer(GraphBufferDesc {
size: 4096,
..Default::default()
});
if pass {
graph.push(BeginRendering {
color: vec![],
depth: None,
});
}
for _ in 0..per {
for &buffer in &a {
graph.push(Use {
buffer,
write: true,
});
}
}
graph.push(Use {
buffer: shared,
write: true,
});
if pass {
graph.push(EndRendering);
graph.push(BeginRendering {
color: vec![],
depth: None,
});
}
for _ in 0..per {
for &buffer in &b {
graph.push(Use {
buffer,
write: true,
});
}
}
graph.push(Use {
buffer: shared,
write: false,
});
if pass {
graph.push(EndRendering);
}
graph
}
fn build_overlapping(n_groups: usize, per: usize) -> RenderGraph {
let mut graph = RenderGraph::new();
let buffers: Vec<Buffer> = (0..n_groups)
.map(|_| {
graph.create_buffer(GraphBufferDesc {
size: 4096,
..Default::default()
})
})
.collect();
let shared = graph.create_buffer(GraphBufferDesc {
size: 4096,
..Default::default()
});
graph.push(BeginRendering {
color: vec![],
depth: None,
});
for _ in 0..per {
for &buffer in &buffers {
graph.push(Use {
buffer,
write: true,
});
}
}
graph.push(Use {
buffer: shared,
write: true,
});
graph.push(EndRendering);
graph.push(BeginRendering {
color: vec![],
depth: None,
});
for _ in 0..per {
for &buffer in &buffers {
graph.push(Use {
buffer,
write: true,
});
}
}
graph.push(Use {
buffer: shared,
write: false,
});
graph.push(EndRendering);
graph
}
fn build_aliasing(
n_groups: usize,
per: usize,
bridge: &Arc<DeviceBuffer>,
) -> RenderGraph {
let mut graph = RenderGraph::new();
let a: Vec<Buffer> = (0..n_groups)
.map(|_| {
graph.create_buffer(GraphBufferDesc {
size: 4096,
..Default::default()
})
})
.collect();
let b: Vec<Buffer> = (0..n_groups)
.map(|_| {
graph.create_buffer(GraphBufferDesc {
size: 4096,
..Default::default()
})
})
.collect();
let bridge = graph
.import_buffer(bridge.clone(), ResourceState::default())
.unwrap();
// Keep the first set outside the pass; render-pass resources interfere with
// each other by design and otherwise prevent placement reuse.
for _ in 0..per {
for &buffer in &a {
graph.push(Use {
buffer,
write: true,
});
}
}
for &buffer in &a {
graph.push(UseWithBridge { buffer, bridge });
}
graph.push(BeginRendering {
color: vec![],
depth: None,
});
for &buffer in &b {
graph.push(UseWithBridge { buffer, bridge });
}
for _ in 0..per {
for &buffer in &b {
graph.push(Use {
buffer,
write: true,
});
}
}
graph.push(EndRendering);
graph
}
fn compile_benchmarks(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("compile");
group
.measurement_time(Duration::from_secs(4))
.sample_size(20);
for (pass_name, pass) in [("no_pass", false), ("pass", true)] {
for per in [20, 50, 100] {
group.bench_function(format!("two_pass/{pass_name}/per_{per}"), |bencher| {
bencher.iter_batched(
|| build(50, per, pass),
|graph| {
let compiled = graph.compile().unwrap();
black_box(compiled.barriers().len())
},
BatchSize::LargeInput,
);
});
}
}
group.finish();
}
fn resolve_benchmarks(criterion: &mut Criterion, device: &renderer_device::Device) {
let bridge = Arc::new(
DeviceBuffer::new(
device.clone(),
BufferDesc {
size: 4096,
usage: vk::BufferUsageFlags::STORAGE_BUFFER,
queue_families: QueueFlags::empty(),
mem_location: MemoryLocation::GpuOnly,
..Default::default()
},
)
.expect("create alias benchmark bridge buffer"),
);
let mut group = criterion.benchmark_group("compile_resolve");
group
.measurement_time(Duration::from_secs(4))
.sample_size(20);
for (name, overlapping) in [("alias", false), ("no_alias", true)] {
for per in [20, 50, 100] {
let graph = if overlapping {
build_overlapping(50, per)
} else {
build_aliasing(50, per, &bridge)
};
let mut probe = graph.compile().unwrap();
probe.resolve(device).unwrap();
assert_eq!(
probe
.resource_lifetimes()
.values()
.any(|lifetime| lifetime.placement.aliased),
!overlapping,
"unexpected placement aliasing in {name}/per_{per}"
);
let alias_barriers = probe
.barriers()
.iter()
.flat_map(|barrier| &barrier.resources)
.filter(|resource| {
probe
.resource_lifetimes()
.get(&resource.id)
.is_some_and(|lifetime| lifetime.placement.aliased)
&& resource.origin != renderer_rendergraph::BarrierOrigin::Initial
})
.count();
assert_eq!(
alias_barriers > 0,
!overlapping,
"unexpected alias barriers in {name}/per_{per}"
);
group.bench_function(format!("{name}/per_{per}"), |bencher| {
bencher.iter_batched(
|| {
if overlapping {
build_overlapping(50, per)
} else {
build_aliasing(50, per, &bridge)
}
},
|graph| {
let mut compiled = graph.compile().unwrap();
compiled.resolve(device).unwrap();
black_box(compiled.barriers().len())
},
BatchSize::LargeInput,
);
});
}
}
group.finish();
}
fn benchmarks(criterion: &mut Criterion) {
let test = TestVulkan::acquire();
compile_benchmarks(criterion);
if let Some(test) = &test {
resolve_benchmarks(criterion, test.device());
} else {
eprintln!("Skipping compile_resolve benchmarks: no Vulkan device is available");
}
}
criterion_group!(benches, benchmarks);
criterion_main!(benches);

View file

@ -0,0 +1,775 @@
use std::{cell::RefCell, sync::Arc};
use ash::vk;
use renderer_resources::images::{ImageView, ImageViewDesc};
use crate::{
recorder::{CommandIndex, CommandRecorder, RecordError, RecordedCommand, SideEffectMap},
resources::{
Buffer, BufferRange, DescriptorResourceAccess, GraphDescriptorSet, IndexBinding,
IndexFormat, IndirectBinding, PushConstantData, ResourceId, SubresourceScope,
SubresourceScopeInner, Texture, VertexBinding,
},
};
/// Classification used by the compiler to validate render-pass spans.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandKind {
Begin,
End,
Graphics,
Transfer,
Compute,
Other,
}
/// A view used by a render-pass attachment.
#[derive(Debug, Clone)]
pub enum AttachmentView {
/// A view the graph must create after resource resolution, described concretely.
Created(ImageViewDesc),
/// A caller-supplied view over an imported image.
Imported(Arc<ImageView>),
}
/// A color attachment used by dynamic rendering.
#[derive(Clone)]
pub struct ColorAttachment {
pub texture: Texture,
pub view: AttachmentView,
/// Layout used while rendering (must be a legal rendering layout; validated later).
pub layout: vk::ImageLayout,
pub load_op: vk::AttachmentLoadOp,
pub store_op: vk::AttachmentStoreOp,
pub clear_value: vk::ClearValue,
/// Concrete subresource range covered by the view. Created-view sentinels are preserved for
/// compile-time validation.
pub range: vk::ImageSubresourceRange,
}
impl std::fmt::Debug for ColorAttachment {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ColorAttachment")
.field("texture", &self.texture)
.field("view", &self.view)
.field("layout", &self.layout)
.field("load_op", &self.load_op)
.field("store_op", &self.store_op)
.field("clear_value", &"<Vulkan clear value>")
.field("range", &self.range)
.finish()
}
}
impl ColorAttachment {
/// Builds an attachment around a caller-supplied view of an imported image.
pub fn imported(
texture: Texture,
view: Arc<ImageView>,
layout: vk::ImageLayout,
load_op: vk::AttachmentLoadOp,
store_op: vk::AttachmentStoreOp,
clear_value: vk::ClearValue,
) -> Self {
let range = view.subresource_range();
Self {
texture,
view: AttachmentView::Imported(view),
layout,
load_op,
store_op,
clear_value,
range,
}
}
/// Builds an attachment whose view will be created after graph resolution.
pub fn created(
texture: Texture,
desc: ImageViewDesc,
layout: vk::ImageLayout,
load_op: vk::AttachmentLoadOp,
store_op: vk::AttachmentStoreOp,
clear_value: vk::ClearValue,
) -> Self {
let range = image_view_desc_range(&desc);
Self {
texture,
view: AttachmentView::Created(desc),
layout,
load_op,
store_op,
clear_value,
range,
}
}
}
/// A depth/stencil attachment used by dynamic rendering. The view's aspect mask
/// selects which depth/stencil attachment slots receive the view.
#[derive(Debug, Clone)]
pub struct DepthStencilAttachment {
pub texture: Texture,
pub view: AttachmentView,
pub layout: vk::ImageLayout,
pub depth_load_op: vk::AttachmentLoadOp,
pub depth_store_op: vk::AttachmentStoreOp,
pub stencil_load_op: vk::AttachmentLoadOp,
pub stencil_store_op: vk::AttachmentStoreOp,
pub clear_depth: f32,
pub clear_stencil: u32,
pub range: vk::ImageSubresourceRange,
}
impl DepthStencilAttachment {
/// Builds an attachment around a caller-supplied view of an imported image.
pub fn imported(
texture: Texture,
view: Arc<ImageView>,
layout: vk::ImageLayout,
depth_load_op: vk::AttachmentLoadOp,
depth_store_op: vk::AttachmentStoreOp,
stencil_load_op: vk::AttachmentLoadOp,
stencil_store_op: vk::AttachmentStoreOp,
clear_depth: f32,
clear_stencil: u32,
) -> Self {
let range = view.subresource_range();
Self {
texture,
view: AttachmentView::Imported(view),
layout,
depth_load_op,
depth_store_op,
stencil_load_op,
stencil_store_op,
clear_depth,
clear_stencil,
range,
}
}
/// Builds an attachment whose view will be created after graph resolution.
pub fn created(
texture: Texture,
desc: ImageViewDesc,
layout: vk::ImageLayout,
depth_load_op: vk::AttachmentLoadOp,
depth_store_op: vk::AttachmentStoreOp,
stencil_load_op: vk::AttachmentLoadOp,
stencil_store_op: vk::AttachmentStoreOp,
clear_depth: f32,
clear_stencil: u32,
) -> Self {
let range = image_view_desc_range(&desc);
Self {
texture,
view: AttachmentView::Created(desc),
layout,
depth_load_op,
depth_store_op,
stencil_load_op,
stencil_store_op,
clear_depth,
clear_stencil,
range,
}
}
}
fn image_view_desc_range(desc: &ImageViewDesc) -> vk::ImageSubresourceRange {
vk::ImageSubresourceRange::default()
.aspect_mask(desc.aspect)
.base_mip_level(desc.mip_range.start)
.level_count(if desc.mip_range.end == vk::REMAINING_MIP_LEVELS {
vk::REMAINING_MIP_LEVELS
} else {
desc.mip_range.end - desc.mip_range.start
})
.base_array_layer(desc.layer_range.start)
.layer_count(if desc.layer_range.end == vk::REMAINING_ARRAY_LAYERS {
vk::REMAINING_ARRAY_LAYERS
} else {
desc.layer_range.end - desc.layer_range.start
})
}
/// A borrowed summary of a render-pass attachment configuration.
#[derive(Debug, Clone, Copy)]
pub struct RenderPassDesc<'a> {
pub color: &'a [ColorAttachment],
pub depth: Option<&'a DepthStencilAttachment>,
}
/// Begins a dynamic-rendering instance.
#[derive(Debug, Clone)]
pub struct BeginRendering {
pub color: Vec<ColorAttachment>,
pub depth: Option<DepthStencilAttachment>,
}
/// Ends a dynamic-rendering instance.
#[derive(Debug, Clone, Copy, Default)]
pub struct EndRendering;
#[derive(Default)]
struct RenderingScratch {
color_attachments: Vec<vk::RenderingAttachmentInfo<'static>>,
}
thread_local! {
static RENDERING_SCRATCH: RefCell<RenderingScratch> = RefCell::new(RenderingScratch::default());
}
fn resolve_attachment_view(
texture: Texture,
view: &AttachmentView,
recorder: &CommandRecorder<'_>,
) -> Result<(vk::ImageView, vk::Extent2D), RecordError> {
recorder.image(texture.id())?;
let view = match view {
AttachmentView::Created(desc) => recorder.view(texture.id(), desc)?,
AttachmentView::Imported(view) => view.clone(),
};
let range = view.subresource_range();
let image_extent = view.image().extent_2d();
let mip = range.base_mip_level.min(u32::BITS - 1);
let extent = vk::Extent2D {
width: (image_extent.width >> mip).max(1),
height: (image_extent.height >> mip).max(1),
};
Ok((view.raw(), extent))
}
fn rendering_extent(current: Option<vk::Extent2D>, next: vk::Extent2D) -> vk::Extent2D {
current.map_or(next, |current| vk::Extent2D {
width: current.width.min(next.width),
height: current.height.min(next.height),
})
}
/// A recorded operation declares its accesses before compilation and applies
/// itself to a Vulkan command buffer after resource binding.
pub trait Command: RecordedCommand {
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap);
}
fn access(
stage: vk::PipelineStageFlags2,
access: vk::AccessFlags2,
subresource: SubresourceScopeInner,
) -> SubresourceScope {
SubresourceScope {
stage,
access,
subresource,
}
}
#[derive(Debug, Clone, Copy)]
pub struct CopyBuffer {
pub src: Buffer,
pub src_range: BufferRange,
pub dst: Buffer,
pub dst_range: BufferRange,
}
impl Command for CopyBuffer {
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
map.insert(
command_index,
self.src.id(),
access(
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_READ,
SubresourceScopeInner::Buffer(self.src_range),
),
);
map.insert(
command_index,
self.dst.id(),
access(
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_WRITE,
SubresourceScopeInner::Buffer(self.dst_range),
),
);
}
}
impl RecordedCommand for CopyBuffer {
fn kind(&self) -> CommandKind {
CommandKind::Transfer
}
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
let region = vk::BufferCopy2::default()
.src_offset(self.src_range.offset)
.dst_offset(self.dst_range.offset)
.size(self.src_range.size.min(self.dst_range.size));
let regions = [region];
let info = vk::CopyBufferInfo2::default()
.src_buffer(recorder.buffer(self.src.id())?)
.dst_buffer(recorder.buffer(self.dst.id())?)
.regions(&regions);
unsafe {
recorder
.device
.cmd_copy_buffer2(recorder.command_buffer, &info)
};
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub struct ClearTexture {
pub texture: Texture,
pub range: vk::ImageSubresourceRange,
pub layout: vk::ImageLayout,
pub color: [f32; 4],
}
impl Command for ClearTexture {
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
map.insert(
command_index,
self.texture.id(),
access(
vk::PipelineStageFlags2::CLEAR,
vk::AccessFlags2::TRANSFER_WRITE,
SubresourceScopeInner::Texture {
range: self.range,
layout: self.layout,
},
),
);
}
}
impl RecordedCommand for ClearTexture {
fn kind(&self) -> CommandKind {
CommandKind::Transfer
}
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
let value = vk::ClearColorValue {
float32: self.color,
};
unsafe {
recorder.device.cmd_clear_color_image(
recorder.command_buffer,
recorder.image(self.texture.id())?,
self.layout,
&value,
&[self.range],
);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub enum DrawArgs {
Direct {
vertex_count: u32,
instance_count: u32,
first_vertex: u32,
first_instance: u32,
},
Indexed {
index_count: u32,
instance_count: u32,
first_index: u32,
vertex_offset: i32,
first_instance: u32,
},
}
/// A self-contained graphics draw packet. Every graph resource consumed by
/// vertex/index/indirect input or descriptor sets is declared by this packet.
#[derive(Debug, Clone)]
pub struct Draw {
pub args: DrawArgs,
/// Layout compatible with the bound pipeline, descriptor sets, and push constants.
pub pipeline_layout: vk::PipelineLayout,
pub vertices: Vec<VertexBinding>,
pub index: Option<IndexBinding>,
pub indirect: Option<IndirectBinding>,
pub descriptor_sets: Vec<GraphDescriptorSet>,
pub push_constants: Vec<PushConstantData>,
}
impl Command for Draw {
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
for vertex in &self.vertices {
map.insert(
command_index,
vertex.slice.buffer.id(),
access(
vk::PipelineStageFlags2::VERTEX_INPUT,
vk::AccessFlags2::VERTEX_ATTRIBUTE_READ,
SubresourceScopeInner::Buffer(vertex.slice.range),
),
);
}
if let Some(index) = self.index {
map.insert(
command_index,
index.slice.buffer.id(),
access(
vk::PipelineStageFlags2::INDEX_INPUT,
vk::AccessFlags2::INDEX_READ,
SubresourceScopeInner::Buffer(index.slice.range),
),
);
}
if let Some(indirect) = self.indirect {
map.insert(
command_index,
indirect.slice.buffer.id(),
access(
vk::PipelineStageFlags2::DRAW_INDIRECT,
vk::AccessFlags2::INDIRECT_COMMAND_READ,
SubresourceScopeInner::Buffer(indirect.slice.range),
),
);
}
for set in &self.descriptor_sets {
for declared in &set.accesses {
let scope = match *declared {
DescriptorResourceAccess::Buffer {
range,
stage,
access: mask,
..
} => access(stage, mask, SubresourceScopeInner::Buffer(range)),
DescriptorResourceAccess::Texture {
range,
layout,
stage,
access: mask,
..
} => access(
stage,
mask,
SubresourceScopeInner::Texture { range, layout },
),
};
let resource = match *declared {
DescriptorResourceAccess::Buffer { buffer, .. } => buffer.id(),
DescriptorResourceAccess::Texture { texture, .. } => texture.id(),
};
map.insert(command_index, resource, scope);
}
}
}
}
impl RecordedCommand for Draw {
fn kind(&self) -> CommandKind {
CommandKind::Graphics
}
fn allowed_in_render_pass(&self) -> bool {
true
}
fn preflight(&self) -> Result<(), RecordError> {
validate_managed_descriptor_sets(&self.descriptor_sets).map(|_| ())
}
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
for vertex in &self.vertices {
let buffers = [recorder.buffer(vertex.slice.buffer.id())?];
let offsets = [vertex.slice.range.offset];
unsafe {
recorder.device.cmd_bind_vertex_buffers(
recorder.command_buffer,
vertex.binding,
&buffers,
&offsets,
);
}
}
if let Some(index) = self.index {
let index_type = match index.format {
IndexFormat::Uint8 => vk::IndexType::UINT8_KHR,
IndexFormat::Uint16 => vk::IndexType::UINT16,
IndexFormat::Uint32 => vk::IndexType::UINT32,
};
unsafe {
recorder.device.cmd_bind_index_buffer(
recorder.command_buffer,
recorder.buffer(index.slice.buffer.id())?,
index.slice.range.offset,
index_type,
);
}
}
for constants in &self.push_constants {
unsafe {
recorder.device.cmd_push_constants(
recorder.command_buffer,
self.pipeline_layout,
constants.stage_flags,
constants.offset,
&constants.data,
);
}
}
let descriptor_sets = validate_managed_descriptor_sets(&self.descriptor_sets)?;
let dynamic_offsets = self
.descriptor_sets
.iter()
.flat_map(|set| set.dynamic_offsets.iter().copied())
.collect::<Vec<_>>();
if !descriptor_sets.is_empty() {
unsafe {
recorder.device.cmd_bind_descriptor_sets(
recorder.command_buffer,
vk::PipelineBindPoint::GRAPHICS,
self.pipeline_layout,
0,
&descriptor_sets,
&dynamic_offsets,
);
}
}
unsafe {
if let Some(indirect) = self.indirect {
let buffer = recorder.buffer(indirect.slice.buffer.id())?;
if matches!(self.args, DrawArgs::Indexed { .. }) {
recorder.device.cmd_draw_indexed_indirect(
recorder.command_buffer,
buffer,
indirect.slice.range.offset,
indirect.draw_count,
indirect.stride,
);
} else {
recorder.device.cmd_draw_indirect(
recorder.command_buffer,
buffer,
indirect.slice.range.offset,
indirect.draw_count,
indirect.stride,
);
}
} else {
match self.args {
DrawArgs::Direct {
vertex_count,
instance_count,
first_vertex,
first_instance,
} => recorder.device.cmd_draw(
recorder.command_buffer,
vertex_count,
instance_count,
first_vertex,
first_instance,
),
DrawArgs::Indexed {
index_count,
instance_count,
first_index,
vertex_offset,
first_instance,
} => recorder.device.cmd_draw_indexed(
recorder.command_buffer,
index_count,
instance_count,
first_index,
vertex_offset,
first_instance,
),
}
}
}
Ok(())
}
}
impl Command for BeginRendering {
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
for attachment in &self.color {
let mut mask = vk::AccessFlags2::COLOR_ATTACHMENT_WRITE;
if attachment.load_op == vk::AttachmentLoadOp::LOAD {
mask |= vk::AccessFlags2::COLOR_ATTACHMENT_READ;
}
map.insert(
command_index,
attachment.texture.id(),
access(
vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT,
mask,
SubresourceScopeInner::Texture {
range: attachment.range,
layout: attachment.layout,
},
),
);
}
if let Some(attachment) = &self.depth {
let mut mask = vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_WRITE;
if attachment.depth_load_op == vk::AttachmentLoadOp::LOAD
|| attachment.stencil_load_op == vk::AttachmentLoadOp::LOAD
{
mask |= vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_READ;
}
map.insert(
command_index,
attachment.texture.id(),
access(
vk::PipelineStageFlags2::EARLY_FRAGMENT_TESTS
| vk::PipelineStageFlags2::LATE_FRAGMENT_TESTS,
mask,
SubresourceScopeInner::Texture {
range: attachment.range,
layout: attachment.layout,
},
),
);
}
}
}
impl RecordedCommand for BeginRendering {
fn kind(&self) -> CommandKind {
CommandKind::Begin
}
fn render_pass_desc(&self) -> Option<RenderPassDesc<'_>> {
Some(RenderPassDesc {
color: &self.color,
depth: self.depth.as_ref(),
})
}
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
RENDERING_SCRATCH.with(|scratch| {
let mut scratch = scratch.borrow_mut();
let attachments = &mut scratch.color_attachments;
attachments.clear();
let mut extent = None;
for attachment in &self.color {
let (view, attachment_extent) =
resolve_attachment_view(attachment.texture, &attachment.view, recorder)?;
extent = Some(rendering_extent(extent, attachment_extent));
attachments.push(
vk::RenderingAttachmentInfo::default()
.image_view(view)
.image_layout(attachment.layout)
.load_op(attachment.load_op)
.store_op(attachment.store_op)
.clear_value(attachment.clear_value),
);
}
let mut depth_info = None;
let mut stencil_info = None;
if let Some(attachment) = &self.depth {
let (view, attachment_extent) =
resolve_attachment_view(attachment.texture, &attachment.view, recorder)?;
extent = Some(rendering_extent(extent, attachment_extent));
let clear_value = vk::ClearValue {
depth_stencil: vk::ClearDepthStencilValue {
depth: attachment.clear_depth,
stencil: attachment.clear_stencil,
},
};
if attachment.range.aspect_mask.contains(vk::ImageAspectFlags::DEPTH) {
depth_info = Some(
vk::RenderingAttachmentInfo::default()
.image_view(view)
.image_layout(attachment.layout)
.load_op(attachment.depth_load_op)
.store_op(attachment.depth_store_op)
.clear_value(clear_value),
);
}
if attachment.range.aspect_mask.contains(vk::ImageAspectFlags::STENCIL) {
stencil_info = Some(
vk::RenderingAttachmentInfo::default()
.image_view(view)
.image_layout(attachment.layout)
.load_op(attachment.stencil_load_op)
.store_op(attachment.stencil_store_op)
.clear_value(clear_value),
);
}
}
let extent = extent.unwrap_or(vk::Extent2D {
width: 0,
height: 0,
});
let mut info = vk::RenderingInfo::default()
.render_area(vk::Rect2D {
offset: vk::Offset2D { x: 0, y: 0 },
extent,
})
.layer_count(1)
.color_attachments(attachments);
if let Some(depth) = depth_info.as_ref() {
info = info.depth_attachment(depth);
}
if let Some(stencil) = stencil_info.as_ref() {
info = info.stencil_attachment(stencil);
}
unsafe {
recorder
.device
.cmd_begin_rendering(recorder.command_buffer, &info)
};
Ok(())
})
}
}
impl Command for EndRendering {
fn side_effects(&self, _command_index: CommandIndex, _map: &mut SideEffectMap) {}
}
impl RecordedCommand for EndRendering {
fn kind(&self) -> CommandKind {
CommandKind::End
}
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
unsafe { recorder.device.cmd_end_rendering(recorder.command_buffer) };
Ok(())
}
}
fn validate_managed_descriptor_sets(
sets: &[GraphDescriptorSet],
) -> Result<Vec<vk::DescriptorSet>, RecordError> {
sets.iter()
.map(|set| {
if let Some(managed) = &set.managed {
let raw = managed.raw().map_err(|error| match error {
renderer_resources::descriptors::DescriptorSetError::StaleSet { .. } => {
RecordError::StaleDescriptorSet
}
error => RecordError::DescriptorSet(error.to_string()),
})?;
if raw != set.raw {
return Err(RecordError::DescriptorSet(
"managed descriptor set handle changed after import".into(),
));
}
Ok(raw)
} else {
Ok(set.raw)
}
})
.collect()
}
pub fn resource_id(buffer: Buffer) -> ResourceId {
buffer.id()
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,250 @@
use ash::vk;
use crate::{
CommandIndex, GraphError, SideEffectMap,
graph::{Kahn, RenderPassSpan},
recorder::BoxedCommand,
resources::{ResourceDesc, SubresourceScope, SubresourceScopeInner},
};
struct SpanBuilder {
spans: Vec<RenderPassSpan>,
current: Option<CommandIndex>,
}
impl SpanBuilder {
fn new() -> Self {
Self {
spans: Vec::new(),
current: None,
}
}
fn in_span(&self) -> bool {
self.current.is_some()
}
fn current_begin(&self) -> Option<CommandIndex> {
self.current
}
fn begin(&mut self, index: CommandIndex) -> Result<(), GraphError> {
if self.current.is_some() {
Err(GraphError::NestedRenderPass)
} else {
self.current = Some(index);
Ok(())
}
}
fn end(&mut self, end: CommandIndex) -> Result<(), GraphError> {
if let Some(begin) = self.current.take() {
self.spans.push(RenderPassSpan { begin, end });
Ok(())
} else {
Err(GraphError::UnbalancedRenderPass)
}
}
fn finish(self) -> Result<Vec<RenderPassSpan>, GraphError> {
if self.current.is_some() {
Err(GraphError::UnbalancedRenderPass)
} else {
Ok(self.spans)
}
}
}
pub(crate) struct Batch {
pub(crate) start: usize,
pub(crate) count: usize,
}
pub(crate) struct CommandOrderings {
pub(crate) execution_order: Vec<CommandIndex>,
pub(crate) batches: Vec<Batch>,
pub(crate) spans: Vec<RenderPassSpan>,
pub(crate) node_of_command: Vec<usize>,
pub(crate) node_representatives: Vec<CommandIndex>,
pub(crate) dependency_edges: Vec<Vec<usize>>,
}
pub(crate) fn build_command_orderings(
resources: &[ResourceDesc],
commands: &[BoxedCommand],
side_effects: &mut SideEffectMap,
) -> Result<CommandOrderings, GraphError> {
let per_command_resource_access = side_effects.command_accesses();
let mut kahn = Kahn::new(commands.len());
let mut access_by_resource: Vec<Vec<(CommandIndex, Vec<SubresourceScope>)>> =
vec![Vec::new(); resources.len()];
let mut span_builder = SpanBuilder::new();
let mut node_of_command = vec![usize::MAX; commands.len()];
let mut node_representatives = Vec::new();
for (index, (command, accesses)) in commands
.iter()
.zip(per_command_resource_access)
.enumerate()
.map(|(i, c)| (CommandIndex::from(i), c))
{
let rep = span_builder.current_begin().unwrap_or(index);
kahn.add_node(rep.index());
// Node ids follow first encounter of each representative in command order.
let node = if node_of_command[rep.index()] == usize::MAX {
let node = node_representatives.len();
node_representatives.push(rep);
node
} else {
node_of_command[rep.index()]
};
node_of_command[index.index()] = node;
for access in accesses {
let Some(resource_access) = access_by_resource.get_mut(access.resource.index()) else {
// `validate_resources` reports invalid ids after dependency analysis.
continue;
};
let priors = resource_access
.iter()
.take_while(|&&(c, _)| c < rep)
.filter(|(_, scopes)| scopes.iter().any(|scope| scope.conflicts(&access.scope)));
for &(prior, _) in priors {
kahn.add_edge(prior.index(), rep.index());
}
if let Some(&mut (last, ref mut scopes)) = resource_access.last_mut()
&& last == rep
{
scopes.push(access.scope);
} else {
resource_access.push((rep, vec![access.scope]))
}
}
match command.kind() {
crate::CommandKind::Begin => {
if let Some(desc) = command.render_pass_desc() {
crate::graph::validate_render_pass_attachments(resources, desc)?;
}
span_builder.begin(index)?;
}
crate::CommandKind::Graphics => {
if !command.allowed_in_render_pass() {
return Err(GraphError::CommandNotAllowedInRenderPass(index));
}
if !span_builder.in_span() {
return Err(GraphError::DrawOutsideRenderPass(index));
}
}
crate::CommandKind::Transfer
| crate::CommandKind::Compute
| crate::CommandKind::Other => {
if span_builder.in_span() && !command.allowed_in_render_pass() {
return Err(GraphError::CommandNotAllowedInRenderPass(index));
}
}
crate::CommandKind::End => {
span_builder.end(index)?;
}
}
}
// spans are ordered by beginning command
let spans = span_builder.finish()?;
for span in &spans {
// Snapshot accesses intentionally predate lifetime-only attachment injection:
// these accesses extend lifetimes but must not add hazard edges.
if let Some(desc) = commands[span.begin.index()].render_pass_desc() {
for attachment in desc.color {
side_effects.insert_lifetime_only(
span.end,
attachment.texture.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT,
access: vk::AccessFlags2::COLOR_ATTACHMENT_WRITE,
subresource: SubresourceScopeInner::Texture {
range: attachment.range,
layout: attachment.layout,
},
},
);
}
if let Some(attachment) = desc.depth {
side_effects.insert_lifetime_only(
span.end,
attachment.texture.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::EARLY_FRAGMENT_TESTS
| vk::PipelineStageFlags2::LATE_FRAGMENT_TESTS,
access: vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_WRITE,
subresource: SubresourceScopeInner::Texture {
range: attachment.range,
layout: attachment.layout,
},
},
);
}
}
}
let mut execution_order: Vec<CommandIndex> = Vec::with_capacity(commands.len());
let mut batches = Vec::<Batch>::new();
let mut span_iter = spans.iter().peekable();
while !kahn.is_complete() {
let first = execution_order.len();
for cmd in kahn.iter_batch().map(CommandIndex::from) {
use core::cmp::Ordering;
match span_iter.peek().map(|s| s.begin.cmp(&cmd)) {
Some(Ordering::Equal) => {
let span = span_iter.next().unwrap();
execution_order
.extend((span.begin.index()..=span.end.index()).map(CommandIndex::from));
}
_ => execution_order.push(cmd),
}
}
let count = execution_order.len() - first;
if count == 0 {
return Err(GraphError::DependencyCycle);
}
for command in &execution_order[first..] {
kahn.complete(command.index());
}
batches.push(Batch {
start: first,
count,
});
}
let mut dependency_edges = vec![Vec::new(); node_representatives.len()];
for (node, representative) in node_representatives.iter().enumerate() {
let representative = representative.index();
let node_edges = &mut dependency_edges[node];
for &edge in &kahn.edges[representative] {
node_edges.push(node_of_command[edge]);
}
}
Ok(CommandOrderings {
execution_order,
batches,
spans,
node_of_command,
node_representatives,
dependency_edges,
})
}

View file

@ -0,0 +1,24 @@
//! A small render-graph skeleton following the design in `rendergraph.org`.
#![allow(dead_code)]
pub mod commands;
pub mod graph;
mod graph_compiler;
pub mod recorder;
pub mod resources;
pub use commands::{
AttachmentView, BeginRendering, ColorAttachment, CommandKind, DepthStencilAttachment,
EndRendering, RenderPassDesc,
};
pub use graph::{
BarrierOrigin, CompiledGraph, GraphError, HappensBefore, MemoryBarrier, Placement, RenderGraph,
RenderPassSpan, ResourceFrontiers, ResourceInterference, ResourceLifetime, ResourceUse,
aliasing_permitted,
};
pub use recorder::{CommandIndex, CommandList, CommandRecorder, SideEffectMap};
pub use resources::{
Buffer, BufferRange, BufferSlice, DescriptorResourceAccess, DescriptorResourceIdentity,
GraphBufferDesc, GraphDescriptorSet, GraphTextureDesc, IndexBinding, IndexFormat,
IndirectBinding, PushConstantData, ResourceId, ResourceState, Texture, VertexBinding,
};

View file

@ -0,0 +1,587 @@
use std::{
cell::RefCell,
collections::{BTreeMap, BTreeSet, HashMap},
sync::Arc,
};
use ash::vk;
use renderer_resources::images::{ImageView, ImageViewDesc};
use crate::resources::{
BufferRange, ResourceId, Subresource, SubresourceScope, SubresourceScopeInner,
};
/// The position of a command in its command list.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CommandIndex(pub usize);
impl CommandIndex {
pub fn index(self) -> usize {
self.0
}
}
impl From<usize> for CommandIndex {
fn from(index: usize) -> Self {
Self(index)
}
}
impl From<CommandIndex> for usize {
fn from(index: CommandIndex) -> Self {
index.0
}
}
#[derive(Debug, Clone, Copy)]
pub struct ResourceAccess {
pub resource: ResourceId,
pub scope: SubresourceScope,
}
/// Stores every declared access together with the command that declared it.
#[derive(Debug, Default, Clone)]
pub struct SideEffectMap {
accesses: Vec<Vec<ResourceAccess>>,
lifetime_only: BTreeSet<(usize, ResourceId)>,
}
impl SideEffectMap {
pub fn begin_command(&mut self) -> CommandIndex {
let index = self.accesses.len();
self.accesses.push(Vec::new());
index.into()
}
/// Record an access scope for a command and resource.
///
/// Each scope must describe the command's complete use for that access,
/// not merely an enclosing bound, because `SideEffectMap::insert` may merge
/// contained scopes for the same command and resource.
pub fn insert(
&mut self,
command_index: CommandIndex,
resource: ResourceId,
scope: SubresourceScope,
) {
if self.accesses.len() <= command_index.0 {
self.accesses.resize_with(command_index.0 + 1, Vec::new);
}
let accesses = &mut self.accesses[command_index.0];
for existing in accesses.iter_mut() {
if existing.resource == resource {
if existing.scope.try_merge(&scope) {
return;
}
if scope.has_same_access_polarity(&existing.scope)
&& scope.contains_subresource(&existing.scope)
{
let mut merged = scope;
merged.stage |= existing.scope.stage;
merged.access |= existing.scope.access;
existing.scope = merged;
return;
}
}
}
accesses.push(ResourceAccess { resource, scope });
}
/// Record an access used only to extend a resource's lifetime through a command.
pub fn insert_lifetime_only(
&mut self,
command_index: CommandIndex,
resource: ResourceId,
scope: SubresourceScope,
) {
self.insert(command_index, resource, scope);
self.lifetime_only.insert((command_index.index(), resource));
}
/// Whether this command/resource access exists only for lifetime analysis.
pub fn is_lifetime_only(&self, command_index: CommandIndex, resource: ResourceId) -> bool {
self.lifetime_only
.contains(&(command_index.index(), resource))
}
pub fn accesses(&self, command_index: CommandIndex) -> &[ResourceAccess] {
self.accesses
.get(command_index.0)
.map_or(&[], Vec::as_slice)
}
pub fn command_accesses(&self) -> &[Vec<ResourceAccess>] {
&self.accesses
}
}
pub type BoxedCommand = Box<dyn RecordedCommand>;
/// A safe, type-erased temporary command buffer. Each command is owned until
/// compilation/recording, while its access declarations are stored separately.
pub struct CommandList {
pub(crate) commands: Vec<BoxedCommand>,
pub side_effects: SideEffectMap,
}
pub trait RecordedCommand {
/// Validate wrapper-backed state before any command in the graph is emitted.
fn preflight(&self) -> Result<(), RecordError> {
Ok(())
}
fn kind(&self) -> crate::commands::CommandKind {
crate::commands::CommandKind::Other
}
fn allowed_in_render_pass(&self) -> bool {
false
}
fn render_pass_desc(&self) -> Option<crate::commands::RenderPassDesc<'_>> {
None
}
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError>;
}
impl CommandList {
pub fn new() -> Self {
Self {
commands: Vec::new(),
side_effects: SideEffectMap::default(),
}
}
pub fn push<C: crate::commands::Command + 'static>(&mut self, command: C) -> CommandIndex {
let index = self.side_effects.begin_command();
command.side_effects(index, &mut self.side_effects);
self.commands.push(Box::new(command));
index
}
pub fn len(&self) -> usize {
self.commands.len()
}
pub fn is_empty(&self) -> bool {
self.commands.is_empty()
}
pub(crate) fn preflight(&self) -> Result<(), RecordError> {
for command in &self.commands {
command.preflight()?;
}
Ok(())
}
}
impl Default for CommandList {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RecordError {
MissingBuffer(ResourceId),
MissingImage(ResourceId),
MissingView(ResourceId),
UnresolvedCreatedBuffer(ResourceId),
UnresolvedCreatedImage(ResourceId),
DescriptorSet(String),
StaleDescriptorSet,
}
/// Vulkan recording context. Handle maps are populated by the compiled graph.
pub struct CommandRecorder<'a> {
pub device: &'a ash::Device,
pub command_buffer: vk::CommandBuffer,
pub buffers: BTreeMap<ResourceId, vk::Buffer>,
pub images: BTreeMap<ResourceId, vk::Image>,
pub views: HashMap<(ResourceId, ImageViewDesc), Arc<ImageView>>,
}
#[derive(Default)]
struct BarrierScratch {
memory: Vec<vk::MemoryBarrier2<'static>>,
buffers: Vec<vk::BufferMemoryBarrier2<'static>>,
images: Vec<vk::ImageMemoryBarrier2<'static>>,
}
thread_local! {
static BARRIER_SCRATCH: RefCell<BarrierScratch> = RefCell::new(BarrierScratch::default());
}
impl<'a> CommandRecorder<'a> {
pub fn new(device: &'a ash::Device, command_buffer: vk::CommandBuffer) -> Self {
Self {
device,
command_buffer,
buffers: BTreeMap::new(),
images: BTreeMap::new(),
views: HashMap::new(),
}
}
pub fn buffer(&self, id: ResourceId) -> Result<vk::Buffer, RecordError> {
self.buffers
.get(&id)
.copied()
.ok_or(RecordError::MissingBuffer(id))
}
pub fn image(&self, id: ResourceId) -> Result<vk::Image, RecordError> {
self.images
.get(&id)
.copied()
.ok_or(RecordError::MissingImage(id))
}
/// Resolve a cached image view by graph resource and concrete view description.
pub fn view(
&self,
id: ResourceId,
desc: &ImageViewDesc,
) -> Result<Arc<ImageView>, RecordError> {
self.views
.get(&(id, image_view_desc_without_name(desc)))
.cloned()
.ok_or(RecordError::MissingView(id))
}
pub fn pipeline_barrier(&self, barrier: &super::graph::PipelineBarrier) {
BARRIER_SCRATCH.with(|scratch| {
let mut scratch = scratch.borrow_mut();
scratch.memory.clear();
scratch
.memory
.extend(barrier.memory_barriers.iter().map(|resource| {
vk::MemoryBarrier2::default()
.src_stage_mask(resource.src_stage)
.src_access_mask(resource.src_access)
.dst_stage_mask(resource.dst_stage)
.dst_access_mask(resource.dst_access)
}));
scratch.buffers.clear();
scratch
.buffers
.extend(
barrier
.resources
.iter()
.filter_map(|resource| match resource.range {
Subresource::Buffer(BufferRange { offset, size }) => {
self.buffers.get(&resource.id).map(|&buffer| {
let (src, dst) =
if let Some(queue_transfer) = resource.queue_transfer {
(queue_transfer.src.index(), queue_transfer.dst.index())
} else {
(vk::QUEUE_FAMILY_IGNORED, vk::QUEUE_FAMILY_IGNORED)
};
vk::BufferMemoryBarrier2::default()
.src_stage_mask(resource.src_stage)
.src_access_mask(resource.src_access)
.dst_stage_mask(resource.dst_stage)
.dst_access_mask(resource.dst_access)
.src_queue_family_index(src)
.dst_queue_family_index(dst)
.buffer(buffer)
.offset(offset)
.size(size)
})
}
Subresource::Texture(_) => None,
}),
);
scratch.images.clear();
scratch
.images
.extend(
barrier
.resources
.iter()
.filter_map(|resource| match resource.range {
Subresource::Texture(range) => {
self.images.get(&resource.id).map(|&image| {
vk::ImageMemoryBarrier2::default()
.src_stage_mask(resource.src_stage)
.src_access_mask(resource.src_access)
.dst_stage_mask(resource.dst_stage)
.dst_access_mask(resource.dst_access)
.old_layout(
resource
.old_layout
.unwrap_or(vk::ImageLayout::UNDEFINED),
)
.new_layout(
resource.new_layout.unwrap_or(vk::ImageLayout::GENERAL),
)
.image(image)
.subresource_range(range)
})
}
Subresource::Buffer(_) => None,
}),
);
let dependency = vk::DependencyInfo::default()
.memory_barriers(&scratch.memory)
.buffer_memory_barriers(&scratch.buffers)
.image_memory_barriers(&scratch.images);
unsafe {
self.device
.cmd_pipeline_barrier2(self.command_buffer, &dependency);
}
});
}
}
fn image_view_desc_without_name(desc: &ImageViewDesc) -> ImageViewDesc {
ImageViewDesc {
name: None,
flags: desc.flags,
kind: desc.kind,
format: desc.format,
components: desc.components,
aspect: desc.aspect,
mip_range: desc.mip_range,
layer_range: desc.layer_range,
}
}
pub(crate) fn scope_range(scope: SubresourceScope) -> Subresource {
match scope.subresource {
SubresourceScopeInner::Buffer(range) => Subresource::Buffer(range),
SubresourceScopeInner::Texture { range, .. } => Subresource::Texture(range),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::{MemoryBarrier, PipelineBarrier};
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
static MEMORY_BARRIER_COUNT: AtomicU32 = AtomicU32::new(0);
static MEMORY_BARRIER_SRC_STAGE: AtomicU64 = AtomicU64::new(0);
unsafe extern "system" fn capture_pipeline_barrier(
_device: vk::Device,
_command_buffer: vk::CommandBuffer,
dependency: *const vk::DependencyInfo<'_>,
) {
let dependency = unsafe { &*dependency };
MEMORY_BARRIER_COUNT.store(dependency.memory_barrier_count, Ordering::SeqCst);
let memory = unsafe { &*dependency.p_memory_barriers };
MEMORY_BARRIER_SRC_STAGE.store(memory.src_stage_mask.as_raw(), Ordering::SeqCst);
}
#[test]
fn pipeline_barrier_emits_memory_barriers_in_dependency_info() {
let device = unsafe {
ash::Device::load_with(
|name| {
if name.to_bytes() == b"vkCmdPipelineBarrier2" {
capture_pipeline_barrier as *const () as *const std::ffi::c_void
} else {
std::ptr::null()
}
},
vk::Device::null(),
)
};
let barrier = PipelineBarrier {
before_command: CommandIndex(0),
memory_barriers: vec![MemoryBarrier {
src_stage: vk::PipelineStageFlags2::COMPUTE_SHADER,
src_access: vk::AccessFlags2::SHADER_STORAGE_WRITE,
dst_stage: vk::PipelineStageFlags2::FRAGMENT_SHADER,
dst_access: vk::AccessFlags2::SHADER_READ,
}],
resources: tinyvec::TinyVec::new(),
};
MEMORY_BARRIER_COUNT.store(0, Ordering::SeqCst);
MEMORY_BARRIER_SRC_STAGE.store(0, Ordering::SeqCst);
CommandRecorder::new(&device, vk::CommandBuffer::null()).pipeline_barrier(&barrier);
assert_eq!(MEMORY_BARRIER_COUNT.load(Ordering::SeqCst), 1);
assert_eq!(
MEMORY_BARRIER_SRC_STAGE.load(Ordering::SeqCst),
vk::PipelineStageFlags2::COMPUTE_SHADER.as_raw()
);
}
fn read_scope(
offset: u64,
size: u64,
stage: vk::PipelineStageFlags2,
access: vk::AccessFlags2,
) -> SubresourceScope {
SubresourceScope {
access,
stage,
subresource: SubresourceScopeInner::Buffer(BufferRange { offset, size }),
}
}
#[test]
fn side_effect_map_folds_same_command_scopes_in_both_containment_directions() {
let mut map = SideEffectMap::default();
let command = map.begin_command();
map.insert(
command,
ResourceId::from_u32(0),
read_scope(
0,
64,
vk::PipelineStageFlags2::VERTEX_SHADER,
vk::AccessFlags2::SHADER_READ,
),
);
map.insert(
command,
ResourceId::from_u32(0),
read_scope(
16,
16,
vk::PipelineStageFlags2::FRAGMENT_SHADER,
vk::AccessFlags2::UNIFORM_READ,
),
);
let accesses = map.accesses(command);
assert_eq!(accesses.len(), 1);
assert_eq!(
accesses[0].scope.stage,
vk::PipelineStageFlags2::VERTEX_SHADER | vk::PipelineStageFlags2::FRAGMENT_SHADER
);
assert_eq!(
accesses[0].scope.access,
vk::AccessFlags2::SHADER_READ | vk::AccessFlags2::UNIFORM_READ
);
assert!(matches!(
accesses[0].scope.subresource,
SubresourceScopeInner::Buffer(BufferRange {
offset: 0,
size: 64
})
));
let mut reverse = SideEffectMap::default();
let command = reverse.begin_command();
reverse.insert(
command,
ResourceId::from_u32(1),
read_scope(
16,
16,
vk::PipelineStageFlags2::VERTEX_SHADER,
vk::AccessFlags2::SHADER_READ,
),
);
reverse.insert(
command,
ResourceId::from_u32(1),
read_scope(
0,
64,
vk::PipelineStageFlags2::FRAGMENT_SHADER,
vk::AccessFlags2::UNIFORM_READ,
),
);
let accesses = reverse.accesses(command);
assert_eq!(accesses.len(), 1);
assert_eq!(
accesses[0].scope.stage,
vk::PipelineStageFlags2::VERTEX_SHADER | vk::PipelineStageFlags2::FRAGMENT_SHADER
);
assert_eq!(
accesses[0].scope.access,
vk::AccessFlags2::SHADER_READ | vk::AccessFlags2::UNIFORM_READ
);
assert!(matches!(
accesses[0].scope.subresource,
SubresourceScopeInner::Buffer(BufferRange {
offset: 0,
size: 64
})
));
}
#[test]
fn side_effect_map_keeps_overlapping_non_subset_scopes_separate() {
let mut map = SideEffectMap::default();
let command = map.begin_command();
map.insert(
command,
ResourceId::from_u32(0),
read_scope(
0,
16,
vk::PipelineStageFlags2::VERTEX_SHADER,
vk::AccessFlags2::SHADER_READ,
),
);
map.insert(
command,
ResourceId::from_u32(0),
read_scope(
8,
16,
vk::PipelineStageFlags2::FRAGMENT_SHADER,
vk::AccessFlags2::UNIFORM_READ,
),
);
assert_eq!(map.accesses(command).len(), 2);
}
#[test]
fn side_effect_map_does_not_fold_opposite_polarity_scopes() {
let mut map = SideEffectMap::default();
let command = map.begin_command();
map.insert(
command,
ResourceId::from_u32(0),
read_scope(
0,
64,
vk::PipelineStageFlags2::COMPUTE_SHADER,
vk::AccessFlags2::SHADER_READ,
),
);
map.insert(
command,
ResourceId::from_u32(0),
read_scope(
16,
16,
vk::PipelineStageFlags2::COMPUTE_SHADER,
vk::AccessFlags2::SHADER_WRITE,
),
);
let accesses = map.accesses(command);
assert_eq!(accesses.len(), 2);
assert!(matches!(
accesses[0].scope.subresource,
SubresourceScopeInner::Buffer(BufferRange {
offset: 0,
size: 64
})
));
assert!(matches!(
accesses[1].scope.subresource,
SubresourceScopeInner::Buffer(BufferRange {
offset: 16,
size: 16
})
));
}
}

View file

@ -0,0 +1,418 @@
use std::{borrow::Cow, sync::Arc};
use ash::vk;
use renderer_device::device::MemoryLocation;
use renderer_resources::{
buffers::Buffer as BufferResource,
images::{Image as ImageResource, ImageSubresourceExt},
pipeline::DescriptorSet,
};
/// Minimal graph-level buffer description. Buffer usage, queue sharing, and
/// allocation strategy are inferred from recorded command accesses at resolve.
#[derive(Debug, Clone)]
pub struct GraphBufferDesc {
pub size: u64,
pub name: Option<Cow<'static, str>>,
pub mem_location: MemoryLocation,
/// Additional Vulkan usages not visible in the graph's recorded commands.
pub extra_usage: vk::BufferUsageFlags,
}
impl Default for GraphBufferDesc {
fn default() -> Self {
Self {
size: 0,
name: None,
// Transient graph resources default to device-local memory.
mem_location: MemoryLocation::GpuOnly,
extra_usage: vk::BufferUsageFlags::empty(),
}
}
}
/// Minimal graph-level texture description. Usage, queue sharing, and
/// allocation strategy are inferred from recorded command accesses at resolve.
#[derive(Debug, Clone)]
pub struct GraphTextureDesc {
pub format: vk::Format,
pub kind: vk::ImageType,
pub extent: vk::Extent3D,
pub mip_levels: u32,
pub array_layers: u32,
pub samples: vk::SampleCountFlags,
pub tiling: vk::ImageTiling,
pub layout: vk::ImageLayout,
pub name: Option<Cow<'static, str>>,
pub mem_location: MemoryLocation,
/// Additional Vulkan usages not visible in the graph's recorded commands.
pub extra_usage: vk::ImageUsageFlags,
}
impl Default for GraphTextureDesc {
fn default() -> Self {
Self {
// Callers must provide a concrete format before resolving an image.
format: vk::Format::UNDEFINED,
kind: vk::ImageType::TYPE_2D,
extent: vk::Extent3D {
width: 1,
height: 1,
depth: 1,
},
mip_levels: 1,
array_layers: 1,
samples: vk::SampleCountFlags::TYPE_1,
// Optimal tiling is the usual choice for graph-managed GPU images.
tiling: vk::ImageTiling::OPTIMAL,
layout: vk::ImageLayout::UNDEFINED,
name: None,
mem_location: MemoryLocation::GpuOnly,
extra_usage: vk::ImageUsageFlags::empty(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct ResourceId(u32);
impl ResourceId {
pub fn from_u32(index: u32) -> Self {
Self(index)
}
pub fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Buffer(pub ResourceId);
impl Buffer {
pub fn id(self) -> ResourceId {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Texture(pub ResourceId);
impl Texture {
pub fn id(self) -> ResourceId {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BufferRange {
pub offset: u64,
pub size: u64,
}
impl BufferRange {
pub const fn full() -> Self {
Self {
offset: 0,
size: vk::WHOLE_SIZE,
}
}
pub fn end(self) -> u64 {
if self.size == vk::WHOLE_SIZE {
u64::MAX
} else {
self.offset.saturating_add(self.size)
}
}
pub fn overlaps(self, other: Self) -> bool {
self.offset < other.end() && other.offset < self.end()
}
pub fn contains(self, other: &Self) -> bool {
self.offset <= other.offset && self.end() >= other.end()
}
}
/// A graph buffer together with the byte range used by a command.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BufferSlice {
pub buffer: Buffer,
pub range: BufferRange,
}
/// A vertex-input binding. `binding` is the Vulkan vertex binding number.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VertexBinding {
pub binding: u32,
pub slice: BufferSlice,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IndexFormat {
Uint8,
Uint16,
Uint32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IndexBinding {
pub slice: BufferSlice,
pub format: IndexFormat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IndirectBinding {
pub slice: BufferSlice,
/// Number of draw commands in the indirect buffer.
pub draw_count: u32,
/// Stride in bytes between draw commands. Must satisfy Vulkan's alignment
/// and minimum-size requirements for the corresponding draw command.
pub stride: u32,
}
#[derive(Debug, Clone, Copy)]
pub enum DescriptorResourceAccess {
Buffer {
buffer: Buffer,
range: BufferRange,
stage: vk::PipelineStageFlags2,
access: vk::AccessFlags2,
},
Texture {
texture: Texture,
range: vk::ImageSubresourceRange,
layout: vk::ImageLayout,
stage: vk::PipelineStageFlags2,
access: vk::AccessFlags2,
},
}
/// A Vulkan descriptor set accompanied by graph-visible resource accesses.
/// Managed instances retain their descriptor wrapper and dynamic offsets;
/// raw instances created through `track_descriptor_set` are untracked and
/// require caller-managed resource lifetime and access declarations.
#[derive(Debug, Clone)]
pub struct GraphDescriptorSet {
pub(crate) raw: vk::DescriptorSet,
pub accesses: Vec<DescriptorResourceAccess>,
/// Retained managed set, when this packet was created by transitive import.
pub(crate) managed: Option<Arc<DescriptorSet>>,
/// Offsets ordered by the descriptor set's binding and array-element order.
pub(crate) dynamic_offsets: Vec<u32>,
}
impl GraphDescriptorSet {
pub fn dynamic_offsets(&self) -> &[u32] {
&self.dynamic_offsets
}
}
/// A resource identity used to supply explicit initial state during descriptor import.
#[derive(Debug, Clone)]
pub enum DescriptorResourceIdentity {
Buffer(Arc<BufferResource>),
Texture(Arc<ImageResource>),
}
#[derive(Debug, Clone)]
pub struct PushConstantData {
pub stage_flags: vk::ShaderStageFlags,
pub offset: u32,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Copy)]
pub enum Subresource {
Buffer(BufferRange),
Texture(vk::ImageSubresourceRange),
}
#[derive(Debug, Clone, Copy)]
pub enum SubresourceScopeInner {
Buffer(BufferRange),
Texture {
range: vk::ImageSubresourceRange,
layout: vk::ImageLayout,
},
}
#[derive(Debug, Clone, Copy)]
pub struct SubresourceScope {
pub access: vk::AccessFlags2,
pub stage: vk::PipelineStageFlags2,
pub subresource: SubresourceScopeInner,
}
impl SubresourceScope {
/// Whether this scope contains the other's subresource range.
/// Texture ranges are compatible only when their layouts match.
pub fn contains_subresource(&self, other: &Self) -> bool {
match (self.subresource, other.subresource) {
(SubresourceScopeInner::Buffer(a), SubresourceScopeInner::Buffer(b)) => a.contains(&b),
(
SubresourceScopeInner::Texture {
range: a,
layout: layout_a,
},
SubresourceScopeInner::Texture {
range: b,
layout: layout_b,
},
) => layout_a == layout_b && a.contains(&b),
_ => false,
}
}
/// Whether both scopes have the same read/write polarity.
pub fn has_same_access_polarity(&self, other: &Self) -> bool {
access_flag_is_read(self.access) == access_flag_is_read(other.access)
&& access_flag_is_write(self.access) == access_flag_is_write(other.access)
}
/// Merge sub-ranges with compatible access polarity into one scope.
pub fn try_merge(&mut self, other: &Self) -> bool {
if access_flag_is_read(self.access) != access_flag_is_read(other.access)
|| access_flag_is_write(self.access) != access_flag_is_write(other.access)
{
return false;
}
let can_merge = match (self.subresource, other.subresource) {
(SubresourceScopeInner::Buffer(a), SubresourceScopeInner::Buffer(b)) => a.contains(&b),
(
SubresourceScopeInner::Texture {
range: a,
layout: layout_a,
},
SubresourceScopeInner::Texture {
range: b,
layout: layout_b,
},
) => layout_a == layout_b && a.contains(&b),
_ => false,
};
if can_merge {
self.stage |= other.stage;
self.access |= other.access;
}
can_merge
}
/// Whether accesses overlap and require ordering (a write or layout change).
pub fn conflicts(&self, other: &Self) -> bool {
// Read-after-read accesses do not conflict, even if they overlap..
if access_flag_is_read(self.access)
&& !access_flag_is_write(self.access)
&& access_flag_is_read(other.access)
&& !access_flag_is_write(other.access)
{
// ..unless the layouts differ for overlapping texture subresources.
return match (self.subresource, other.subresource) {
(
SubresourceScopeInner::Texture {
layout: layout_a,
range: a,
},
SubresourceScopeInner::Texture {
layout: layout_b,
range: b,
},
) => layout_a != layout_b && a.intersects(&b),
_ => false,
};
}
// for accesses involving writes:
use SubresourceScopeInner::*;
match (self.subresource, other.subresource) {
(Buffer(a), Buffer(b)) => a.overlaps(b),
(Texture { range: a, .. }, Texture { range: b, .. }) => a.intersects(&b),
_ => false,
}
}
}
/// True when an access mask contains a Vulkan read access.
pub fn access_flag_is_read(access: vk::AccessFlags2) -> bool {
access.intersects(
vk::AccessFlags2::INDIRECT_COMMAND_READ
| vk::AccessFlags2::INDEX_READ
| vk::AccessFlags2::VERTEX_ATTRIBUTE_READ
| vk::AccessFlags2::UNIFORM_READ
| vk::AccessFlags2::INPUT_ATTACHMENT_READ
| vk::AccessFlags2::SHADER_READ
| vk::AccessFlags2::COLOR_ATTACHMENT_READ
| vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_READ
| vk::AccessFlags2::TRANSFER_READ
| vk::AccessFlags2::HOST_READ
| vk::AccessFlags2::MEMORY_READ
| vk::AccessFlags2::SHADER_SAMPLED_READ
| vk::AccessFlags2::SHADER_STORAGE_READ
| vk::AccessFlags2::ACCELERATION_STRUCTURE_READ_KHR,
)
}
/// True when an access mask contains a Vulkan write access.
pub fn access_flag_is_write(access: vk::AccessFlags2) -> bool {
access.intersects(
vk::AccessFlags2::SHADER_WRITE
| vk::AccessFlags2::COLOR_ATTACHMENT_WRITE
| vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_WRITE
| vk::AccessFlags2::TRANSFER_WRITE
| vk::AccessFlags2::HOST_WRITE
| vk::AccessFlags2::MEMORY_WRITE
| vk::AccessFlags2::SHADER_STORAGE_WRITE
| vk::AccessFlags2::ACCELERATION_STRUCTURE_WRITE_KHR,
)
}
#[derive(Debug, Clone, Copy)]
pub struct ResourceState {
pub stage: vk::PipelineStageFlags2,
pub access: vk::AccessFlags2,
pub layout: Option<vk::ImageLayout>,
}
impl Default for ResourceState {
fn default() -> Self {
Self {
stage: vk::PipelineStageFlags2::NONE,
access: vk::AccessFlags2::NONE,
layout: None,
}
}
}
#[derive(Debug, Clone)]
pub enum ResourceDesc {
CreatedBuffer(GraphBufferDesc),
CreatedTexture(GraphTextureDesc),
ImportedBuffer {
resource: Arc<BufferResource>,
initial: ResourceState,
},
ImportedTexture {
resource: Arc<ImageResource>,
initial: ResourceState,
},
}
impl ResourceDesc {
pub fn initial_state(&self) -> ResourceState {
match self {
Self::ImportedBuffer { initial, .. } | Self::ImportedTexture { initial, .. } => {
*initial
}
Self::CreatedBuffer(_) | Self::CreatedTexture(_) => ResourceState::default(),
}
}
pub fn is_created(&self) -> bool {
matches!(self, Self::CreatedBuffer(_) | Self::CreatedTexture(_))
}
}

View file

@ -0,0 +1,552 @@
use std::sync::Arc;
use ash::vk;
use renderer_device::{
device::{MemoryLocation, QueueFlags},
testing::TestVulkan,
};
use renderer_rendergraph::{
BeginRendering, ColorAttachment, EndRendering, RenderGraph,
commands::CopyBuffer,
commands::{ClearTexture, Command},
recorder::{CommandIndex, CommandRecorder, RecordError, RecordedCommand, SideEffectMap},
resources::{
BufferRange, GraphBufferDesc, GraphTextureDesc, ResourceId, ResourceState,
SubresourceScope, SubresourceScopeInner,
},
};
use renderer_resources::{
buffers::{Buffer, BufferDesc},
images::ImageViewDesc,
};
struct ScopedNoop(Vec<(ResourceId, SubresourceScope)>);
impl Command for ScopedNoop {
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
for &(resource, scope) in &self.0 {
map.insert(command_index, resource, scope);
}
}
}
impl RecordedCommand for ScopedNoop {
fn apply(&mut self, _recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
Ok(())
}
}
struct PassNoop(Vec<(ResourceId, SubresourceScope)>);
impl Command for PassNoop {
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
for &(resource, scope) in &self.0 {
map.insert(command_index, resource, scope);
}
}
}
impl RecordedCommand for PassNoop {
fn allowed_in_render_pass(&self) -> bool {
true
}
fn apply(&mut self, _recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
Ok(())
}
}
fn buffer_scope() -> SubresourceScope {
SubresourceScope {
stage: vk::PipelineStageFlags2::COPY,
access: vk::AccessFlags2::TRANSFER_WRITE,
subresource: SubresourceScopeInner::Buffer(BufferRange {
offset: 0,
size: 256,
}),
}
}
fn image_scope() -> SubresourceScope {
SubresourceScope {
stage: vk::PipelineStageFlags2::COMPUTE_SHADER,
access: vk::AccessFlags2::SHADER_STORAGE_WRITE,
subresource: SubresourceScopeInner::Texture {
range: vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
},
layout: vk::ImageLayout::GENERAL,
},
}
}
fn pass_buffer_read_scope() -> SubresourceScope {
SubresourceScope {
stage: vk::PipelineStageFlags2::VERTEX_SHADER,
access: vk::AccessFlags2::SHADER_READ,
subresource: SubresourceScopeInner::Buffer(BufferRange {
offset: 0,
size: 256,
}),
}
}
fn pass_texture_read_scope() -> SubresourceScope {
SubresourceScope {
stage: vk::PipelineStageFlags2::FRAGMENT_SHADER,
access: vk::AccessFlags2::SHADER_SAMPLED_READ,
subresource: SubresourceScopeInner::Texture {
range: vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
},
layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
},
}
}
fn supports_render_pass_test(test: &TestVulkan) -> bool {
let features = test.device().enabled_features().core13;
if features.synchronization2 != vk::TRUE {
eprintln!("Skipping graph render pass: synchronization2 is unsupported");
return false;
}
if features.dynamic_rendering != vk::TRUE {
eprintln!("Skipping graph render pass: dynamic_rendering is unsupported");
return false;
}
true
}
fn submit_graph(test: &TestVulkan, graph: &mut renderer_rendergraph::CompiledGraph) {
let device = test.device();
let pool_info = vk::CommandPoolCreateInfo::default()
.queue_family_index(device.queues.graphics().family.index.index());
let pool = unsafe { device.dev().create_command_pool(&pool_info, None) }
.expect("create alias test command pool");
let allocation_info = vk::CommandBufferAllocateInfo::default()
.command_pool(pool)
.level(vk::CommandBufferLevel::PRIMARY)
.command_buffer_count(1);
let command_buffer = unsafe { device.dev().allocate_command_buffers(&allocation_info) }
.expect("allocate alias test command buffer")[0];
unsafe {
device
.dev()
.begin_command_buffer(command_buffer, &vk::CommandBufferBeginInfo::default())
.expect("begin alias test command buffer");
}
let mut recorder = CommandRecorder::new(device.dev(), command_buffer);
graph
.record_commands(&mut recorder)
.expect("record alias graph");
unsafe {
device
.dev()
.end_command_buffer(command_buffer)
.expect("end alias test command buffer");
let command_buffers = [command_buffer];
let submit = vk::SubmitInfo::default().command_buffers(&command_buffers);
device
.dev()
.queue_submit(device.queues.graphics().raw, &[submit], vk::Fence::null())
.expect("submit alias graph");
}
device.wait_idle().expect("wait for alias graph idle");
unsafe { device.dev().destroy_command_pool(pool, None) };
}
#[test]
fn imported_buffer_copy_records_under_validation() {
let Some(test) = TestVulkan::acquire() else {
return;
};
let device = test.device();
if device.enabled_features().core13.synchronization2 != vk::TRUE {
eprintln!("Skipping graph recording: synchronization2 is unsupported");
return;
}
let make_buffer = || {
Arc::new(
Buffer::new(
device.clone(),
BufferDesc {
size: 256,
usage: vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST,
queue_families: QueueFlags::empty(),
mem_location: MemoryLocation::CpuToGpu,
..Default::default()
},
)
.expect("create graph test buffer"),
)
};
let source = make_buffer();
let destination = make_buffer();
let mut graph = RenderGraph::new();
let source_id = graph
.import_buffer(source, ResourceState::default())
.expect("import source buffer");
let destination_id = graph
.import_buffer(destination, ResourceState::default())
.expect("import destination buffer");
graph.push(CopyBuffer {
src: source_id,
src_range: BufferRange {
offset: 0,
size: 256,
},
dst: destination_id,
dst_range: BufferRange {
offset: 0,
size: 256,
},
});
let mut graph = graph.compile().expect("compile imported buffer graph");
let pool_info = vk::CommandPoolCreateInfo::default()
.queue_family_index(device.queues.graphics().family.index.index());
let pool = unsafe { device.dev().create_command_pool(&pool_info, None) }
.expect("create graph command pool");
let allocation_info = vk::CommandBufferAllocateInfo::default()
.command_pool(pool)
.level(vk::CommandBufferLevel::PRIMARY)
.command_buffer_count(1);
let command_buffer = unsafe { device.dev().allocate_command_buffers(&allocation_info) }
.expect("allocate graph command buffer")[0];
unsafe {
device
.dev()
.begin_command_buffer(command_buffer, &vk::CommandBufferBeginInfo::default())
.expect("begin graph command buffer");
}
let mut recorder = CommandRecorder::new(device.dev(), command_buffer);
graph
.record_commands(&mut recorder)
.expect("record imported buffer graph");
unsafe {
device
.dev()
.end_command_buffer(command_buffer)
.expect("end graph command buffer");
device.dev().destroy_command_pool(pool, None);
}
device.wait_idle().expect("wait for graph device idle");
test.assert_no_validation_errors();
}
#[test]
fn resolved_buffers_alias_and_submit_with_scoped_barrier() {
let Some(test) = TestVulkan::acquire() else {
return;
};
let device = test.device();
if device.enabled_features().core13.synchronization2 != vk::TRUE {
eprintln!("Skipping graph aliasing: synchronization2 is unsupported");
return;
}
let bridge = Arc::new(
Buffer::new(
device.clone(),
BufferDesc {
size: 256,
usage: vk::BufferUsageFlags::TRANSFER_DST,
mem_location: MemoryLocation::CpuToGpu,
..Default::default()
},
)
.expect("create dependency bridge buffer"),
);
let mut graph = RenderGraph::new();
let first = graph.create_buffer(GraphBufferDesc {
size: 256,
..Default::default()
});
let second = graph.create_buffer(GraphBufferDesc {
size: 256,
..Default::default()
});
let bridge = graph
.import_buffer(bridge, ResourceState::default())
.expect("import dependency bridge");
graph.push(ScopedNoop(vec![
(first.id(), buffer_scope()),
(bridge.id(), buffer_scope()),
]));
graph.push(ScopedNoop(vec![
(bridge.id(), buffer_scope()),
(second.id(), buffer_scope()),
]));
let mut compiled = graph.compile().expect("compile buffer alias graph");
compiled
.resolve(device)
.expect("resolve buffer alias graph");
compiled
.resolve(device)
.expect("resolving an already-resolved graph is idempotent");
let first_placement = compiled.resource_lifetimes()[&first.id()].placement;
let second_placement = compiled.resource_lifetimes()[&second.id()].placement;
assert_eq!(first_placement.pool, second_placement.pool);
assert_eq!(first_placement.offset, second_placement.offset);
assert!(first_placement.aliased && second_placement.aliased);
let alias_barrier = compiled
.barriers()
.iter()
.find(|barrier| barrier.before_command == CommandIndex(1))
.expect("incoming buffer's first use has an alias barrier");
assert!(alias_barrier.memory_barriers.is_empty());
let incoming = alias_barrier
.resources
.iter()
.find(|resource| resource.id == second.id())
.expect("alias barrier is scoped to the incoming buffer");
assert_eq!(incoming.src_stage, vk::PipelineStageFlags2::COPY);
assert_eq!(incoming.src_access, vk::AccessFlags2::TRANSFER_WRITE);
assert_eq!(incoming.dst_stage, vk::PipelineStageFlags2::COPY);
assert_eq!(incoming.dst_access, vk::AccessFlags2::TRANSFER_WRITE);
submit_graph(&test, &mut compiled);
test.assert_no_validation_errors();
drop(compiled);
device
.wait_idle()
.expect("wait before shared allocation teardown");
test.assert_no_validation_errors();
}
#[test]
fn resolved_images_alias_and_submit_with_undefined_transition() {
let Some(test) = TestVulkan::acquire() else {
return;
};
let device = test.device();
if device.enabled_features().core13.synchronization2 != vk::TRUE {
eprintln!("Skipping graph aliasing: synchronization2 is unsupported");
return;
}
let bridge = Arc::new(
Buffer::new(
device.clone(),
BufferDesc {
size: 256,
usage: vk::BufferUsageFlags::TRANSFER_DST,
mem_location: MemoryLocation::CpuToGpu,
..Default::default()
},
)
.expect("create dependency bridge buffer"),
);
let mut graph = RenderGraph::new();
let first = graph.create_texture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
extent: vk::Extent3D {
width: 16,
height: 16,
depth: 1,
},
extra_usage: vk::ImageUsageFlags::STORAGE,
..Default::default()
});
let second = graph.create_texture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
extent: vk::Extent3D {
width: 16,
height: 16,
depth: 1,
},
extra_usage: vk::ImageUsageFlags::STORAGE,
..Default::default()
});
let bridge = graph
.import_buffer(bridge, ResourceState::default())
.expect("import dependency bridge");
graph.push(ScopedNoop(vec![
(first.id(), image_scope()),
(bridge.id(), buffer_scope()),
]));
graph.push(ScopedNoop(vec![
(bridge.id(), buffer_scope()),
(second.id(), image_scope()),
]));
let mut compiled = graph.compile().expect("compile image alias graph");
compiled.resolve(device).expect("resolve image alias graph");
compiled
.resolve(device)
.expect("resolving an already-resolved graph is idempotent");
let first_placement = compiled.resource_lifetimes()[&first.id()].placement;
let second_placement = compiled.resource_lifetimes()[&second.id()].placement;
assert_eq!(first_placement.pool, second_placement.pool);
assert_eq!(first_placement.offset, second_placement.offset);
assert!(first_placement.aliased && second_placement.aliased);
assert!(compiled.barriers().iter().any(|barrier| {
barrier.before_command == CommandIndex(1)
&& barrier.resources.iter().any(|resource| {
resource.id == second.id()
&& resource.old_layout == Some(vk::ImageLayout::UNDEFINED)
&& resource.src_access == vk::AccessFlags2::SHADER_STORAGE_WRITE
})
}));
submit_graph(&test, &mut compiled);
test.assert_no_validation_errors();
drop(compiled);
device
.wait_idle()
.expect("wait before shared allocation teardown");
test.assert_no_validation_errors();
}
#[test]
fn created_render_pass_records_submits_and_validates() {
let Some(test) = TestVulkan::acquire() else {
return;
};
if !supports_render_pass_test(&test) {
return;
}
let device = test.device();
let make_buffer = || {
Arc::new(
Buffer::new(
device.clone(),
BufferDesc {
size: 256,
usage: vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST,
queue_families: QueueFlags::empty(),
mem_location: MemoryLocation::CpuToGpu,
..Default::default()
},
)
.expect("create render-pass test buffer"),
)
};
let producer_source = make_buffer();
let producer_destination = make_buffer();
let unrelated_destination = make_buffer();
let mut graph = RenderGraph::new();
let target = graph.create_texture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
extent: vk::Extent3D {
width: 16,
height: 16,
depth: 1,
},
..Default::default()
});
let producer_source = graph
.import_buffer(producer_source, ResourceState::default())
.expect("import producer source");
let producer_destination = graph
.import_buffer(producer_destination, ResourceState::default())
.expect("import producer destination");
let unrelated_destination = graph
.import_buffer(unrelated_destination, ResourceState::default())
.expect("import unrelated destination");
let buffer_range = BufferRange {
offset: 0,
size: 256,
};
let image_range = vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
};
graph.push(ClearTexture {
texture: target,
range: image_range,
layout: vk::ImageLayout::TRANSFER_DST_OPTIMAL,
color: [0.1, 0.2, 0.3, 1.0],
});
let producer = graph.push(CopyBuffer {
src: producer_source,
src_range: buffer_range,
dst: producer_destination,
dst_range: buffer_range,
});
let begin = graph.push(BeginRendering {
color: vec![ColorAttachment::created(
target,
ImageViewDesc::color_2d(),
vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
vk::AttachmentLoadOp::LOAD,
vk::AttachmentStoreOp::STORE,
vk::ClearValue::default(),
)],
depth: None,
});
let interior = graph.push(PassNoop(vec![(
producer_destination.id(),
pass_buffer_read_scope(),
)]));
let end = graph.push(EndRendering);
let post_end = graph.push(ScopedNoop(vec![(target.id(), pass_texture_read_scope())]));
let unrelated = graph.push(CopyBuffer {
src: producer_destination,
src_range: buffer_range,
dst: unrelated_destination,
dst_range: buffer_range,
});
let mut compiled = graph.compile().expect("compile render-pass graph");
let execution_order = compiled.execution_order();
let position = |command| {
execution_order
.iter()
.position(|&item| item == command)
.unwrap()
};
assert!(position(begin) < position(interior));
assert!(position(interior) < position(end));
assert!(position(unrelated) > position(end));
let begin_barrier = compiled
.barriers()
.iter()
.find(|barrier| barrier.before_command == begin)
.expect("incoming barriers are hoisted before BeginRendering");
assert!(begin_barrier.resources.iter().any(|resource| {
resource.id == target.id()
&& resource.old_layout == Some(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
&& resource.new_layout == Some(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
}));
assert!(begin_barrier.resources.iter().any(|resource| {
resource.id == producer_destination.id()
&& resource.origin == renderer_rendergraph::BarrierOrigin::Command(producer)
}));
assert!(compiled.barriers().iter().any(|barrier| {
barrier.before_command == post_end
&& barrier.resources.iter().any(|resource| {
resource.id == target.id()
&& resource.old_layout == Some(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
&& resource.new_layout == Some(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
})
}));
compiled
.resolve(device)
.expect("resolve created attachment view");
assert!(
compiled
.created_view(target.id(), &ImageViewDesc::color_2d())
.is_some()
);
submit_graph(&test, &mut compiled);
test.assert_no_validation_errors();
drop(compiled);
device
.wait_idle()
.expect("wait before render target teardown");
test.assert_no_validation_errors();
}

View file

@ -0,0 +1,85 @@
# renderer_resources
## Purpose
`renderer_resources` owns Vulkan buffer, image, view, descriptor, sampler, shader-module, and pipeline construction on the `renderer_device` layer. It handles resource memory and handle lifetimes, but contains no window, swapchain, or presentation orchestration; those remain in `renderer`. It must not depend on `renderer`.
## Module map
- `lib.rs`: re-exports `Device`, `Error`, `Result`, `device`, and `make_extension` from `renderer_device`; declares public buffer, image, memory, pipeline, utility, and descriptor modules.
- `buffers.rs`: `BufferDesc` and `Buffer`; allocated and unallocated Vulkan buffer creation, memory requirement query/binding, raw access, and host mapping.
- `images.rs`: `ImageDesc`, `Image`, `ImageViewDesc`, `ImageView`, subresource ranges, extents, offsets, and queue ownership. `ImageInner` distinguishes allocated/unallocated images from borrowed swapchain images.
- `memory.rs`: re-exports device `MemoryRequirements`; exposes memory validation errors, `MemoryMember`, and `MemoryGroup` for aligned shared buffer/image allocations.
- `pipeline.rs`: shader-stage and graphics/compute pipeline descriptions; `DescriptorPool`, `DescriptorSetLayout`, `PipelineLayout`, `Sampler`, `ShaderModule`, and `Pipeline`.
- `descriptors.rs`: managed descriptor binding/resource metadata, validation plans, errors, and pool-generation helpers; it remains independent of render-graph types.
- `util.rs`: `eq_f32`, `ord_f32`, `hash_f32`, `F32`, and the crate-private `weak_vec` module / `WeakVec` used by image view tracking.
## Key types and API
- These modules are public from `renderer_resources`; the crate root re-exports the core device types and `device` module from `renderer_device` for resource construction.
- `BufferDesc` configures Vulkan buffer flags, size, usage, queue families, `renderer_device::device::MemoryLocation`, allocation strategy, and optional debug name. `Buffer::new` requests a VMA allocation through `DeviceInner::allocate_for_buffer` and binds it through `bind_buffer_memory`; `Buffer::new_unallocated` creates only the Vulkan handle. Its `memory_requirements` includes dedicated-allocation hints, and `bind_allocation(Arc<DeviceObject<GpuAllocation>>, offset)` validates then binds shared memory. `map` and `map_mut` return mapped slices when available.
- `Buffer::raw` returns the Vulkan handle and `len` returns the configured size. Mapping is bounded to the descriptor size, not the VMA allocation block size. CPU-mapped locations select host-visible, host-coherent memory.
- `ImageDesc` configures image type, format, extent, layers/mips, samples, tiling, usage, initial layout, queue sharing, and allocation strategy/location. `Image::new` uses the device's VMA allocation and image-binding helpers.
- `Image::new` creates an allocated image; `Image::new_unallocated` creates an unbound image. `memory_requirements` includes dedicated-allocation hints, and `bind_allocation(Arc<DeviceObject<GpuAllocation>>, offset)` validates then binds shared memory. `Image::new_with_allocation` accepts an explicit `renderer_device::device::Allocation` and validates size and memory-type compatibility.
- `MemoryGroup::new(memory_type_index, property_flags, size, alignment)` fixes the shared pool's memory type and capacity. `add(MemoryMember::Buffer(&mut buffer))` / `add(MemoryMember::Image(&mut image))` queries requirements, checks that the selected type is allowed for every member, and packs each at an aligned offset; `allocate(&device)` creates aliasable memory, then consuming `bind_all()` transfers allocation references into the resources. The group does not retain its own allocation after binding.
- `Image::from_swapchain_raw(image, device, format, extent, usage)` builds an image wrapper for an image owned by a swapchain; renderer calls it from `Swapchain::acquire_image`.
- `ImageViewDesc` describes a view's type, format, component mapping, aspect, mip range, and layer range. `Image::create_view` validates compatibility and returns `ManuallyDrop<ImageView>`.
- `Image::create_view` resolves remaining mip/layer sentinels against the parent image, defaults an undefined view format to the image format, and checks image/view kind and format compatibility.
- `Extent`, `Offset`, and `MipRange` represent image dimensions, offsets, and mip/layer ranges. `QueueOwnership` carries source and destination family indices.
- `Image` exposes its raw handle, format, dimensions, and optional allocation; swapchain-backed images have no allocation.
- Pipeline configuration records include `ShaderStageDesc`, `GraphicsPipelineDesc`, `ComputePipelineDesc`, `PipelineLayoutDesc`, `DescriptorSetLayoutDesc`, `DescriptorSetLayoutBindingDesc`, `DescriptorPoolDesc`, `DescriptorSetAllocDesc`, `SamplerDesc`, and fixed-function state descriptions.
- Fixed-function state records are `VertexInputState`, `TessellationState`, `InputAssemblyState`, `ViewportState`, `DepthBiasState`, `RasterizationState`, `MultisampleState`, `DepthBounds`, `DepthState`, `StencilState`, `DepthStencilState`, `ColorBlendState`, `RenderingState`, and `DynamicState`.
- `Pipeline` wraps graphics or compute Vulkan pipelines and retains its `PipelineLayout` in an `Arc`. Creation uses `device.pools.pipeline_cache.raw`.
- `DescriptorPool` serializes allocation/reset with a mutex; descriptor set layouts and pipeline layouts own their Vulkan handles.
- `DescriptorPool::allocate_untracked` returns raw descriptor-set handles without retaining layouts or bound resources. `allocate_managed` returns a generation-checked `DescriptorSet` that retains its pool, layout facts, and written resource owners.
- A managed `DescriptorSet` retains its pool-allocation token, layout facts, and the `Arc<Buffer>`, `Arc<ImageView>`, `Arc<Sampler>`, or `Arc<BufferView>` owners supplied for its written descriptors. Image views in turn retain their parent images.
- `DescriptorSet::raw()` is fallible and checks the pool generation before exposing the Vulkan handle. Generation checks also apply to binding metadata and writer operations; they detect sets invalidated by reset but cannot revoke escaped raw handles or make concurrent reset/use safe.
- `DescriptorSet::writer` stages buffer, image, sampler, and texel-buffer updates. `DescriptorWriter` validates the descriptor type, slot bounds, ranges, resource facts, and required enabled features before Vulkan updates; retained owners are replaced only after the Vulkan writes succeed. Partially-bound bindings may keep unwritten slots; other bindings must be complete at commit.
- Managed writes support `UNIFORM_BUFFER`, `UNIFORM_BUFFER_DYNAMIC`, `STORAGE_BUFFER`, `STORAGE_BUFFER_DYNAMIC`, `SAMPLED_IMAGE`, `STORAGE_IMAGE`, `COMBINED_IMAGE_SAMPLER`, `SAMPLER`, `UNIFORM_TEXEL_BUFFER`, and `STORAGE_TEXEL_BUFFER`. Input attachments, acceleration structures, inline uniform blocks, and all other descriptor types are rejected until explicitly implemented.
- Managed image writes reject swapchain-backed images (including combined image samplers) before updating Vulkan descriptors, because retaining an `ImageView` does not retain its swapchain. Use the untracked route only when the caller separately guarantees the external image/swapchain lifetime.
- `DescriptorSetLayout::new` chains per-binding descriptor flags when the device API/extension supports the Vulkan structure and validates requested flags against enabled descriptor-indexing features.
- `PipelineLayoutDesc` references descriptor-set layouts and push-constant ranges; `PipelineLayout` owns the created Vulkan layout.
- `SamplerDesc` describes filtering, addressing, anisotropy, compare, LOD, and border settings; `Sampler::new` creates the Vulkan sampler.
- `ShaderModule::new_from_path` reads a shader file relative to the process working directory; file-open failures map to `Error::AssetMissing`, while read/Vulkan failures use their corresponding `Error` conversions. `new_from_memory` accepts SPIR-V words.
- `GraphicsPipelineDesc` supports render-pass/subpass inputs or dynamic `RenderingState`; optional fixed-function states are converted during pipeline creation.
- `SamplerDesc` uses `eq_f32` / `hash_f32` for float-valued fields. `F32` provides `Eq`/`Ord`/`Hash` semantics using the same helpers.
- `WeakVec<T>` stores `Weak<T>` references and prunes entries whose strong count is zero when growing; it is crate-private, not public API.
## Invariants and gotchas
- This crate consumes the `renderer_device` ownership types and re-exports; do not add a parallel device model or a dependency on `renderer`.
- `Image::from_swapchain_raw` stores the raw image in the `ImageInner::Swapchain` variant with its device. The swapchain owns that Vulkan image; never destroy it as an independently allocated image.
- `from_swapchain_raw` records a 2D, one-mip, one-layer, single-sample, optimal-tiled image with PRESENT queue sharing and UNDEFINED initial layout; the caller supplies format, extent, and usage.
- `from_swapchain_raw` retains a `Device` for views and device queries, but not ownership of the image handle or swapchain lifetime; the renderer's frame/swapchain token must keep the swapchain alive.
- Allocated images and buffers use `DeviceObject` for Vulkan handles and allocation ownership. Keep resource destruction ordered before freeing bound memory.
- Unallocated resources use `Allocation::Unmanaged` until bound. Shared bind APIs are one-shot; they validate offset alignment, allowed memory type, and allocation bounds before calling the device layer. Keep `DeviceObject<GpuAllocation>` on the resource after a successful bind so Vulkan handles are destroyed before their allocation reference.
- `MemoryGroup` only accepts members whose `memory_type_bits` include its selected type (the intersection across all members); it rejects dedicated-required resources. Group offsets use aligned bump packing, and allocation ownership transfers into each bound member at `bind_all`.
- `Buffer` equality compares the raw buffer handle; `Image` equality compares the raw image handle. Descriptor equality is separate from resource object identity.
- `Allocation::Owned` owns a device-managed allocation; `Shared` shares it through `Arc` and permits mutable access only when uniquely held; `Unmanaged` carries no allocation.
- `ImageView` retains `Arc<Image>` so the source image outlives the view. `Image::create_view` returns `ManuallyDrop<ImageView>`; callers must transfer/drop it deliberately.
- `Image` tracks views through `WeakVec<ImageView>` to avoid a strong-reference cycle. The weak list does not keep views alive.
- `ImageView`'s `DeviceObject` owns and destroys the Vulkan view; its parent `Arc<Image>` must remain alive through view destruction.
- `Image::create_view` returns `ManuallyDrop<ImageView>`; when a managed descriptor needs an owned view, deliberately transfer it with `ManuallyDrop::into_inner` and place it in an `Arc`.
- `ImageDesc` and `BufferDesc` omit `alloc_scheme` from both `Hash` and `Eq`; their `Hash` implementations omit `name`, and `Eq` also omits `name`. Do not treat these descriptors as complete resource identities without accounting for these fields.
- Descriptor hashing/equality covers resource shape, usage, sharing, and memory location but not every construction input. Update both implementations together if adding descriptor-keyed resource deduplication.
- `Pipeline` retains its layout with `Arc`; descriptor-pool operations are mutex-serialized. Keep these lifetime and synchronization guarantees when changing constructors or wrappers.
- Descriptor indexing validation uses `Device::enabled_features()` (the feature snapshot requested for logical-device creation), not `Device::features()` (adapter-supported features). Managed layouts validate binding-flag and descriptor-type-specific feature requirements, and managed writes validate descriptor requirements against the same enabled snapshot.
- `DescriptorPool::allocate_untracked` (and its backwards-compatible `allocate` alias) is a raw escape hatch: it retains no layout or binding owners, owns no managed-set token, and is not eligible for transitive managed import. Any raw handles are the caller's responsibility.
- Managed pool `reset` and `destroy` are unsafe caller-synchronized operations. Before reset/destruction, callers must ensure relevant GPU work has completed and exclude all managed-set use, including allocation, writes, graph recording/import, and command-buffer recording/binding. Final pool destruction/drop has the same synchronization requirement. Generation checks are defense-in-depth and do not close concurrent reset/use races; wrapper drops do not prove GPU retirement. Any raw handle returned by `raw()` escapes these checks and remains the caller's responsibility. Raw/untracked reset and use are likewise caller-synchronized.
- Descriptor-set layout binding flags are chained into layout creation when the device API version or descriptor-indexing extension supports them.
- Pipeline cache lifetime and teardown belong to `renderer_device`; use the device's existing cache rather than adding a resource-crate cache.
- Renderer shader assets remain under `crates/renderer/shaders`; this crate does not own those assets.
- `renderer` accesses these modules through crate-private aliases. Making a resource type available in `renderer`'s public API requires an explicit API/re-export decision there.
## Conventions
- `renderer_resources` has no crate-level nightly feature attributes; use APIs available under the workspace's pinned nightly toolchain and edition 2024.
- Depend on `renderer_device` paths re-exported at this crate root (`crate::Device`, `crate::device::...`, and `crate::make_extension`) rather than introducing a dependency on `renderer`.
- Do not commit changes. Verify with `cargo check -p renderer_resources` and `cargo check --workspace` from the workspace root.
## Change guidance
- Add buffer, image, view, descriptor, sampler, shader, and pipeline constructors in this crate, not in `renderer`'s orchestration layer.
- For each Vulkan handle, preserve the distinction between resource-owned handles and externally owned handles such as raw swapchain images.
- Keep image-view range/format/kind validation close to image/view construction. Cover `REMAINING_MIP_LEVELS` and `REMAINING_ARRAY_LAYERS` normalization when changing range handling.
- When modifying descriptor equality/hash behavior, update both together and account for `name` and `alloc_scheme` deliberately.
- Preserve `renderer_device::device::Allocation` ownership semantics when accepting shared or unmanaged memory; ensure bound resources outlive their allocation use.
- Keep `SamplerDesc` float equality and hashing consistent across every float field.
- `renderer_resources::util::weak_vec` is internal support for images; do not expose or move it back into `renderer` without reviewing view ownership cycles.
- Keep shader loading errors compatible with `renderer_device::Error`, and test path-based loading relative to the workspace runtime working directory.
- Keep this crate independent of swapchain/window wrappers. Integrate swapchain images through the raw image plus explicit device, format, extent, and usage, not renderer types.
- Keep pipeline descriptors, Vulkan state conversion, and handle ownership in this crate rather than moving resource ownership back into renderer orchestration.

View file

@ -0,0 +1,18 @@
[package]
name = "renderer_resources"
version = "0.1.0"
edition = "2024"
[features]
testing = []
[dependencies]
renderer_device = { path = "../renderer_device" }
ash = { workspace = true }
parking_lot = { workspace = true }
bytemuck = { workspace = true }
tracing = { workspace = true }
ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" }
[dev-dependencies]
renderer_device = { path = "../renderer_device", features = ["testing"] }

View file

@ -0,0 +1,640 @@
use std::{borrow::Cow, sync::Arc};
use ash::vk;
use renderer_device::queue::QueueFamilyIndices;
use crate::{
Device,
device::{
Allocation, AllocationDesc, AllocationStrategy, DeviceObject, MemoryLocation, QueueFlags,
},
};
#[derive(Clone)]
pub struct BufferDesc {
pub flags: vk::BufferCreateFlags,
pub name: Option<Cow<'static, str>>,
pub size: u64,
pub usage: vk::BufferUsageFlags,
pub queue_families: QueueFlags,
pub mem_location: MemoryLocation,
pub alloc_scheme: AllocationStrategy,
}
impl std::hash::Hash for BufferDesc {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.flags.hash(state);
self.size.hash(state);
self.usage.hash(state);
self.queue_families.hash(state);
self.mem_location.hash(state);
}
}
impl std::fmt::Debug for BufferDesc {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BufferDesc")
.field("flags", &self.flags)
.field("name", &self.name)
.field("size", &self.size)
.field("usage", &self.usage)
.field("queue_families", &self.queue_families)
.field("mem_location", &self.mem_location)
.field("alloc_scheme", &self.alloc_scheme)
.finish()
}
}
impl Eq for BufferDesc {}
impl PartialEq for BufferDesc {
fn eq(&self, other: &Self) -> bool {
self.flags == other.flags
// for hashmaps, `Eq` may be more strict than `Hash`
&& self.size == other.size
&& self.usage == other.usage
&& self.queue_families == other.queue_families
&& self.mem_location == other.mem_location
}
}
impl Default for BufferDesc {
fn default() -> Self {
Self {
flags: Default::default(),
name: Default::default(),
size: Default::default(),
usage: Default::default(),
queue_families: QueueFlags::empty(),
mem_location: MemoryLocation::Unknown,
alloc_scheme: AllocationStrategy::default(),
}
}
}
#[derive(Debug)]
pub struct Buffer {
buffer: BufferHandle,
pub desc: BufferDesc,
alloc: Allocation,
}
#[derive(Debug)]
enum BufferHandle {
Owned(DeviceObject<vk::Buffer>),
#[cfg(feature = "testing")]
Testing(vk::Buffer),
}
impl Eq for Buffer {}
impl PartialEq for Buffer {
fn eq(&self, other: &Self) -> bool {
self.raw() == other.raw()
}
}
impl Buffer {
fn owned(&self) -> Option<&DeviceObject<vk::Buffer>> {
match &self.buffer {
BufferHandle::Owned(buffer) => Some(buffer),
#[cfg(feature = "testing")]
BufferHandle::Testing(_) => None,
}
}
pub fn new(device: Device, desc: BufferDesc) -> crate::Result<Self> {
let buffer = Self::new_raw(device.clone(), &desc)?;
let alloc = device.allocate_for_buffer(
*buffer,
AllocationDesc {
name: desc.name.as_deref().unwrap_or_default(),
location: desc.mem_location,
strategy: desc.alloc_scheme,
},
)?;
let buffer = Self {
buffer: BufferHandle::Owned(buffer),
desc,
alloc: Allocation::Owned(DeviceObject::new(device.clone(), alloc)),
};
device.bind_buffer_memory(
buffer.alloc.allocation().expect("owned buffer allocation"),
buffer.raw(),
)?;
Ok(buffer)
}
/// Creates a Vulkan buffer without allocating or binding memory.
pub fn new_unallocated(device: Device, desc: BufferDesc) -> crate::Result<Self> {
let buffer = Self::new_raw(device, &desc)?;
Ok(Self {
buffer: BufferHandle::Owned(buffer),
desc,
alloc: Allocation::Unmanaged {
requirements: std::sync::OnceLock::new(),
},
})
}
/// Returns the Vulkan memory requirements, including dedicated-allocation hints.
pub fn memory_requirements(&self) -> crate::memory::Result<crate::memory::MemoryRequirements> {
let Some(buffer) = self.owned() else {
return Err(crate::memory::Error::RequirementQuery(
crate::Error::Unspecified,
));
};
let requirements_cache = match &self.alloc {
Allocation::Unmanaged { requirements } => Some(requirements),
Allocation::Owned(_) | Allocation::Shared(_) => None,
};
if let Some(requirements) = requirements_cache.and_then(std::sync::OnceLock::get) {
return Ok(*requirements);
}
let requirements = buffer
.device()
.buffer_memory_requirements(**buffer)
.map_err(crate::memory::Error::RequirementQuery)?;
if let Some(cache) = requirements_cache {
let _ = cache.set(requirements);
}
Ok(requirements)
}
/// Binds this buffer into an aliasable allocation at an allocation-local offset.
///
/// Requirements are queried once and cached in the resource. The Vulkan bind remains the
/// authoritative validation.
pub fn bind_allocation(
&mut self,
allocation: Arc<DeviceObject<crate::device::GpuAllocation>>,
offset: u64,
) -> crate::memory::Result<()> {
if !matches!(self.alloc, Allocation::Unmanaged { .. }) {
return Err(crate::memory::Error::AlreadyBound);
}
let requirements = self.memory_requirements()?;
crate::memory::validate_binding(&requirements, allocation.as_ref(), offset)?;
let device = self.owned().expect("owned buffer").device();
self.alloc = device
.bind_buffer_at_offset(self.raw(), &requirements, allocation, offset)
.map_err(crate::memory::Error::Bind)?;
Ok(())
}
pub(crate) fn validate_unallocated(&self) -> crate::memory::Result<()> {
if matches!(self.alloc, Allocation::Unmanaged { .. }) {
Ok(())
} else {
Err(crate::memory::Error::AlreadyBound)
}
}
pub(crate) fn validate_device(&self, device: &Device) -> crate::memory::Result<()> {
if self.owned().expect("unallocated buffer is owned").device() == device {
Ok(())
} else {
Err(crate::memory::Error::Bind(
crate::Error::AllocationDeviceMismatch,
))
}
}
pub(crate) fn set_shared_allocation(
&mut self,
allocation: Arc<DeviceObject<crate::device::GpuAllocation>>,
) {
debug_assert!(matches!(self.alloc, Allocation::Unmanaged { .. }));
self.alloc = Allocation::Shared(allocation);
}
fn new_raw(device: Device, desc: &BufferDesc) -> crate::Result<DeviceObject<vk::Buffer>> {
let queue_families = device.queues.family_indices(desc.queue_families);
let sharing_mode = if queue_families.len() > 1 {
vk::SharingMode::CONCURRENT
} else {
vk::SharingMode::EXCLUSIVE
};
let create_info = vk::BufferCreateInfo::default()
.size(desc.size)
.usage(desc.usage)
.queue_family_indices(queue_families.as_u32_slice())
.sharing_mode(sharing_mode);
let buffer = unsafe { device.dev().create_buffer(&create_info, None)? };
Ok(DeviceObject::new_debug_named(
device,
buffer,
desc.name.clone(),
))
}
pub fn map(&mut self) -> Option<&[u8]> {
if let Some(alloc) = self.alloc.allocation() {
alloc
.mapped_slice()
.map(|slice| &slice[..self.desc.size as usize])
} else {
None
}
}
pub fn map_mut(&mut self) -> Option<&mut [u8]> {
if let Some(alloc) = self.alloc.allocation_mut() {
alloc
.mapped_slice_mut()
.map(|slice| &mut slice[..self.desc.size as usize])
} else {
None
}
}
pub fn raw(&self) -> vk::Buffer {
match &self.buffer {
BufferHandle::Owned(buffer) => **buffer,
#[cfg(feature = "testing")]
BufferHandle::Testing(buffer) => *buffer,
}
}
pub fn len(&self) -> u64 {
self.desc.size
}
/// Creates a non-owning buffer fixture for device-free integration tests.
///
/// This constructor is only available with the crate's `testing` feature. The raw handle is
/// never destroyed and must not be used for Vulkan calls.
#[cfg(feature = "testing")]
#[doc(hidden)]
pub fn for_testing(raw: vk::Buffer, size: u64) -> Self {
Self {
buffer: BufferHandle::Testing(raw),
desc: BufferDesc {
size,
..Default::default()
},
alloc: Allocation::Unmanaged {
requirements: std::sync::OnceLock::new(),
},
}
}
}
#[cfg(test)]
mod allocation_validation_tests {
use ash::vk;
use renderer_device::testing::TestVulkan;
use super::{Buffer, BufferDesc};
use crate::{
device::MemoryLocation,
images::{Image, ImageDesc},
};
use renderer_device::device::AllocationStrategy;
#[test]
fn vma_mapping_and_dedicated_resources_under_validation() {
let test = TestVulkan::expect();
let device = test.device().clone();
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Buffer>();
assert_send_sync::<Image>();
let mut mapped_buffer = Buffer::new(
device.clone(),
BufferDesc {
size: 64,
usage: vk::BufferUsageFlags::TRANSFER_SRC,
mem_location: MemoryLocation::CpuToGpu,
..Default::default()
},
)
.expect("create CpuToGpu buffer");
let allocation = mapped_buffer
.alloc
.allocation()
.expect("CpuToGpu buffer allocation");
let memory_type = test
.physical_device()
.properties
.memory_properties
.memory_types[allocation.memory_type_index() as usize];
assert!(memory_type.property_flags.contains(
vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT
));
let expected = (0..64).map(|byte| byte as u8).collect::<Vec<_>>();
mapped_buffer
.map_mut()
.expect("CpuToGpu buffer is mapped")
.copy_from_slice(&expected);
assert_eq!(
mapped_buffer.map().expect("CpuToGpu buffer remains mapped"),
expected
);
let dedicated_buffer = Buffer::new(
device.clone(),
BufferDesc {
size: 256,
usage: vk::BufferUsageFlags::TRANSFER_DST,
alloc_scheme: AllocationStrategy::Dedicated,
..Default::default()
},
)
.expect("create dedicated buffer");
assert!(
dedicated_buffer
.alloc
.allocation()
.expect("dedicated buffer allocation")
.is_dedicated(),
"VMA should allocate the dedicated buffer from its own memory block"
);
let dedicated_image = Image::new(
device,
ImageDesc {
format: vk::Format::R8G8B8A8_UNORM,
extent: vk::Extent3D {
width: 16,
height: 16,
depth: 1,
},
usage: vk::ImageUsageFlags::SAMPLED,
alloc_scheme: AllocationStrategy::Dedicated,
..Default::default()
},
)
.expect("create dedicated image");
assert!(
dedicated_image
.allocation()
.expect("dedicated image allocation")
.allocation()
.expect("dedicated image VMA allocation")
.is_dedicated(),
"VMA should allocate the dedicated image from its own memory block"
);
drop(dedicated_image);
drop(dedicated_buffer);
drop(mapped_buffer);
test.device().wait_idle().expect("wait for device idle");
test.assert_no_validation_errors();
}
}
#[cfg(all(test, feature = "testing"))]
mod unallocated_memory_tests {
use ash::vk;
use renderer_device::testing::TestVulkan;
use super::{Buffer, BufferDesc};
use crate::{
device::MemoryLocation,
images::{Image, ImageDesc},
memory::{Error, MemoryGroup, MemoryMember},
};
fn buffer_desc() -> BufferDesc {
BufferDesc {
size: 256,
usage: vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST,
mem_location: MemoryLocation::GpuOnly,
..Default::default()
}
}
#[test]
fn unallocated_resources_query_requirements_and_bind() {
let test = TestVulkan::expect();
let device = test.device().clone();
let mut buffer = Buffer::new_unallocated(device.clone(), buffer_desc())
.expect("create unallocated buffer");
let buffer_requirements = buffer.memory_requirements().expect("buffer requirements");
assert!(buffer_requirements.size >= buffer.len());
assert!(matches!(
&buffer.alloc,
crate::device::Allocation::Unmanaged { .. }
));
let mut image = Image::new_unallocated(
device.clone(),
ImageDesc {
format: vk::Format::R8G8B8A8_UNORM,
extent: vk::Extent3D {
width: 16,
height: 16,
depth: 1,
},
usage: vk::ImageUsageFlags::TRANSFER_DST,
..Default::default()
},
)
.expect("create unallocated image");
let image_requirements = image.memory_requirements().expect("image requirements");
assert!(image_requirements.size > 0);
assert!(
image
.allocation()
.is_some_and(|allocation| allocation.allocation().is_none())
);
let buffer_memory = device
.allocate_memory(&buffer_requirements, vk::MemoryPropertyFlags::empty())
.expect("allocate buffer memory");
buffer
.bind_allocation(buffer_memory, 0)
.expect("bind buffer at offset zero");
assert!(matches!(
&buffer.alloc,
crate::device::Allocation::Shared(_)
));
let image_memory = device
.allocate_memory(&image_requirements, vk::MemoryPropertyFlags::empty())
.expect("allocate image memory");
image
.bind_allocation(image_memory, 0)
.expect("bind image at offset zero");
assert!(matches!(
image.allocation(),
Some(crate::device::Allocation::Shared(_))
));
drop(image);
drop(buffer);
test.device().wait_idle().expect("wait for device idle");
test.assert_no_validation_errors();
}
#[test]
fn bind_rejects_invalid_offsets_and_rebinding() {
let test = TestVulkan::expect();
let device = test.device().clone();
let mut buffer = Buffer::new_unallocated(device.clone(), buffer_desc())
.expect("create buffer for offset checks");
let requirements = buffer.memory_requirements().expect("buffer requirements");
let allocation = device
.allocate_memory(&requirements, vk::MemoryPropertyFlags::empty())
.expect("allocate offset-check memory");
if requirements.alignment > 1 {
assert!(matches!(
buffer.bind_allocation(allocation.clone(), 1),
Err(Error::MisalignedOffset { .. })
));
}
assert!(matches!(
buffer.bind_allocation(allocation.clone(), requirements.size),
Err(Error::OutOfBounds { .. })
));
buffer
.bind_allocation(allocation.clone(), 0)
.expect("first buffer bind");
assert!(matches!(
buffer.bind_allocation(allocation, 0),
Err(Error::AlreadyBound)
));
let allocated = device
.allocate_memory(&requirements, vk::MemoryPropertyFlags::empty())
.expect("allocate for synthetic type-bits validation");
let impossible = crate::memory::MemoryRequirements {
memory_type_bits: 0,
..requirements
};
assert!(matches!(
crate::memory::validate_binding(&impossible, allocated.as_ref(), 0),
Err(Error::IncompatibleMemoryType { .. })
));
drop(buffer);
test.device().wait_idle().expect("wait for device idle");
test.assert_no_validation_errors();
}
#[test]
fn memory_group_packs_and_binds_two_buffers() {
let test = TestVulkan::expect();
let device = test.device().clone();
let mut first =
Buffer::new_unallocated(device.clone(), buffer_desc()).expect("create first buffer");
let mut second =
Buffer::new_unallocated(device.clone(), buffer_desc()).expect("create second buffer");
let first_requirements = first.memory_requirements().expect("first requirements");
let second_requirements = second.memory_requirements().expect("second requirements");
let common_bits =
first_requirements.memory_type_bits & second_requirements.memory_type_bits;
assert_ne!(common_bits, 0, "the test adapter needs a common type");
let memory_type_index = common_bits.trailing_zeros();
let alignment = first_requirements
.alignment
.max(second_requirements.alignment);
let first_end = first_requirements.size;
let expected_second_offset = first_end
.checked_add(alignment - 1)
.expect("offset arithmetic")
/ alignment
* alignment;
let size = expected_second_offset + second_requirements.size;
let mut group = MemoryGroup::new(
memory_type_index,
vk::MemoryPropertyFlags::empty(),
size,
alignment,
)
.expect("create memory group");
let first_offset = group
.add(MemoryMember::Buffer(&mut first))
.expect("pack first buffer");
let second_offset = group
.add(MemoryMember::Buffer(&mut second))
.expect("pack second buffer");
assert_eq!(first_offset, 0);
assert_eq!(second_offset, expected_second_offset);
assert_eq!(second_offset % alignment, 0);
group.allocate(&device).expect("allocate shared memory");
group.bind_all().expect("bind group members");
assert!(matches!(&first.alloc, crate::device::Allocation::Shared(_)));
assert!(matches!(
&second.alloc,
crate::device::Allocation::Shared(_)
));
assert_eq!(
first
.alloc
.allocation()
.map(crate::device::GpuAllocation::memory),
second
.alloc
.allocation()
.map(crate::device::GpuAllocation::memory),
"both buffers retain the shared Vulkan memory block"
);
drop(first);
drop(second);
test.device().wait_idle().expect("wait for device idle");
test.assert_no_validation_errors();
}
}
/// Byte range selected by a buffer view.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BufferRange {
pub offset: u64,
pub size: u64,
}
/// An owned Vulkan buffer view, retaining the buffer it refers to.
#[derive(Debug)]
pub struct BufferView {
raw: vk::BufferView,
buffer: Arc<Buffer>,
range: BufferRange,
}
impl BufferView {
pub fn new(buffer: Arc<Buffer>, format: vk::Format, range: BufferRange) -> crate::Result<Self> {
let info = vk::BufferViewCreateInfo::default()
.buffer(buffer.raw())
.format(format)
.offset(range.offset)
.range(range.size);
let Some(buffer_handle) = buffer.owned() else {
return Err(crate::Error::Unspecified);
};
let raw = unsafe { buffer_handle.device().raw.create_buffer_view(&info, None)? };
Ok(Self { raw, buffer, range })
}
pub fn raw(&self) -> vk::BufferView {
self.raw
}
pub fn buffer(&self) -> &Arc<Buffer> {
&self.buffer
}
pub fn range(&self) -> BufferRange {
self.range
}
}
impl Drop for BufferView {
fn drop(&mut self) {
if let Some(buffer) = self.buffer.owned() {
unsafe { buffer.device().raw.destroy_buffer_view(self.raw, None) };
}
}
}

View file

@ -0,0 +1,987 @@
//! Graph-independent descriptor resource metadata and write validation.
use std::sync::Arc;
use ash::vk;
use renderer_device::PhysicalDeviceFeatures;
use crate::{
buffers::{Buffer, BufferView},
images::ImageView,
pipeline::Sampler,
};
/// An owned resource entry suitable for retention by a tracked descriptor set.
#[derive(Debug, Clone)]
pub enum DescriptorResourceBinding {
Buffer {
buffer: Arc<Buffer>,
offset: u64,
range: u64,
dynamic: bool,
},
Image {
view: Arc<ImageView>,
sampler: Option<Arc<Sampler>>,
range: vk::ImageSubresourceRange,
layout: vk::ImageLayout,
},
Sampler {
sampler: Arc<Sampler>,
},
TexelBuffer {
view: Arc<BufferView>,
range: crate::buffers::BufferRange,
},
}
/// Errors produced by managed descriptor-set operations.
#[derive(Debug)]
pub enum DescriptorSetError {
Vulkan(crate::Error),
Validation(DescriptorValidationError),
StaleSet {
allocated_generation: u64,
pool_generation: u64,
},
IncompleteBinding {
binding: u32,
element: u32,
},
PoolStillRetained,
PoolGenerationExhausted,
}
pub type DescriptorSetResult<T> = Result<T, DescriptorSetError>;
impl std::fmt::Display for DescriptorSetError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Vulkan(error) => error.fmt(f),
Self::Validation(error) => error.fmt(f),
Self::StaleSet {
allocated_generation,
pool_generation,
} => write!(
f,
"descriptor set is stale (allocated at generation {allocated_generation}, pool is at generation {pool_generation})"
),
Self::IncompleteBinding { binding, element } => write!(
f,
"descriptor binding {binding} has unwritten array element {element}"
),
Self::PoolStillRetained => {
f.write_str("managed descriptor sets still retain this descriptor pool")
}
Self::PoolGenerationExhausted => f.write_str("descriptor pool generation exhausted"),
}
}
}
impl std::error::Error for DescriptorSetError {}
impl From<crate::Error> for DescriptorSetError {
fn from(error: crate::Error) -> Self {
Self::Vulkan(error)
}
}
impl From<DescriptorValidationError> for DescriptorSetError {
fn from(error: DescriptorValidationError) -> Self {
Self::Validation(error)
}
}
pub(crate) fn check_pool_generation(
allocated_generation: u64,
pool_generation: u64,
) -> DescriptorSetResult<()> {
if allocated_generation == pool_generation {
Ok(())
} else {
Err(DescriptorSetError::StaleSet {
allocated_generation,
pool_generation,
})
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum PoolResetError<E> {
Reset(E),
GenerationExhausted,
}
pub(crate) fn pool_generation_after_reset<E>(
generation: u64,
reset: impl FnOnce() -> Result<(), E>,
) -> Result<u64, PoolResetError<E>> {
let next = generation
.checked_add(1)
.ok_or(PoolResetError::GenerationExhausted)?;
reset().map_err(PoolResetError::Reset)?;
Ok(next)
}
pub(crate) fn replace_binding_entries<T: Clone>(
current: &std::collections::BTreeMap<(u32, u32), T>,
writes: impl IntoIterator<Item = ((u32, u32), T)>,
) -> std::collections::BTreeMap<(u32, u32), T> {
let mut replacement = current.clone();
replacement.extend(writes);
replacement
}
pub(crate) fn clear_binding_entries<T>(
entries: &mut std::collections::BTreeMap<(u32, u32), T>,
binding: u32,
) {
entries.retain(|(entry_binding, _), _| *entry_binding != binding);
}
pub(crate) fn validate_binding_completeness<T>(
layout: &[crate::pipeline::DescriptorSetLayoutBindingDesc],
entries: &std::collections::BTreeMap<(u32, u32), T>,
) -> DescriptorSetResult<()> {
for binding in layout {
if binding
.flags
.unwrap_or_default()
.contains(vk::DescriptorBindingFlags::PARTIALLY_BOUND)
{
continue;
}
for element in 0..binding.count {
if !entries.contains_key(&(binding.binding, element)) {
return Err(DescriptorSetError::IncompleteBinding {
binding: binding.binding,
element,
});
}
}
}
Ok(())
}
/// An owned descriptor write. Texel-buffer kind is carried by `descriptor_type`.
#[derive(Debug, Clone)]
pub struct TrackedDescriptorWrite {
pub binding: u32,
pub array_element: u32,
pub descriptor_type: vk::DescriptorType,
pub stages: vk::ShaderStageFlags,
pub resource: DescriptorResourceBinding,
}
/// Resource facts attached to a raw Vulkan write, independent of Vulkan handles.
#[derive(Debug, Clone, Copy)]
pub enum RawDescriptorResourceFacts {
Buffer {
buffer_len: u64,
offset: u64,
range: u64,
dynamic: bool,
},
Image {
swapchain_backed: bool,
has_view: bool,
has_sampler: bool,
},
Sampler {
has_sampler: bool,
},
TexelBuffer {
buffer_len: u64,
offset: u64,
range: u64,
},
}
/// The descriptor-specific facts needed to validate a pending write.
#[derive(Debug, Clone, Copy)]
pub struct RawDescriptorWriteFacts {
pub binding: u32,
pub array_element: u32,
pub descriptor_type: vk::DescriptorType,
pub resource: RawDescriptorResourceFacts,
}
/// A validated write, with whole-size ranges normalized to concrete lengths.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ValidatedDescriptorWrite {
pub binding: u32,
pub array_element: u32,
pub descriptor_type: vk::DescriptorType,
pub normalized_range: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DescriptorValidationError {
UnknownBinding(u32),
ArrayElementOutOfBounds {
binding: u32,
element: u32,
count: u32,
},
TypeMismatch {
binding: u32,
expected: vk::DescriptorType,
actual: vk::DescriptorType,
},
UnsupportedDescriptorType(vk::DescriptorType),
ResourceTypeMismatch(vk::DescriptorType),
BufferRangeOutOfBounds,
InvalidDynamicAlignment,
MisalignedDynamicOffset,
MissingImageView,
MissingImageSampler,
MissingSampler,
SwapchainImage,
BindingNotPartiallyBound(u32),
FeatureNotEnabled {
binding: u32,
feature: &'static str,
},
}
impl std::fmt::Display for DescriptorValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownBinding(binding) => write!(
f,
"descriptor binding {binding} does not exist in the layout"
),
Self::ArrayElementOutOfBounds {
binding,
element,
count,
} => write!(
f,
"descriptor array element {element} is outside binding {binding}'s count {count}"
),
Self::TypeMismatch {
binding,
expected,
actual,
} => write!(
f,
"descriptor type {actual:?} does not match binding {binding}'s type {expected:?}"
),
Self::UnsupportedDescriptorType(kind) => write!(
f,
"descriptor type {kind:?} is unsupported for tracked writes"
),
Self::ResourceTypeMismatch(kind) => write!(
f,
"descriptor resource facts do not match descriptor type {kind:?}"
),
Self::BufferRangeOutOfBounds => {
f.write_str("descriptor buffer range exceeds the buffer length")
}
Self::InvalidDynamicAlignment => {
f.write_str("dynamic buffer descriptor has a zero alignment")
}
Self::MisalignedDynamicOffset => {
f.write_str("dynamic buffer descriptor offset is not aligned")
}
Self::MissingImageView => f.write_str("image descriptor requires an image view"),
Self::MissingImageSampler => {
f.write_str("combined image sampler descriptor requires a sampler")
}
Self::MissingSampler => f.write_str("sampler descriptor requires a sampler"),
Self::SwapchainImage => {
f.write_str("managed descriptor writes cannot reference swapchain-backed images")
}
Self::BindingNotPartiallyBound(binding) => write!(
f,
"descriptor binding {binding} cannot be cleared because it is not partially bound"
),
Self::FeatureNotEnabled { binding, feature } => write!(
f,
"binding {binding} requires an enabled descriptor-indexing feature: {feature}"
),
}
}
}
impl std::error::Error for DescriptorValidationError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DescriptorWritePlan {
pub writes: Vec<ValidatedDescriptorWrite>,
}
/// Checks all descriptor-indexing flags against the logical-device enabled-feature snapshot.
pub fn validate_descriptor_binding_features(
bindings: &[crate::pipeline::DescriptorSetLayoutBindingDesc],
enabled: &PhysicalDeviceFeatures,
) -> Result<(), DescriptorValidationError> {
for binding in bindings {
if !is_supported_descriptor_type(binding.kind) {
return Err(DescriptorValidationError::UnsupportedDescriptorType(
binding.kind,
));
}
let flags = binding.flags.unwrap_or_default();
let feature_check = |enabled: bool, name| {
if enabled {
Ok(())
} else {
Err(DescriptorValidationError::FeatureNotEnabled {
binding: binding.binding,
feature: name,
})
}
};
if flags.contains(vk::DescriptorBindingFlags::PARTIALLY_BOUND) {
feature_check(
enabled.core12.descriptor_binding_partially_bound != vk::FALSE,
"descriptorBindingPartiallyBound",
)?;
}
if flags.contains(vk::DescriptorBindingFlags::VARIABLE_DESCRIPTOR_COUNT) {
feature_check(
enabled.core12.descriptor_binding_variable_descriptor_count != vk::FALSE,
"descriptorBindingVariableDescriptorCount",
)?;
feature_check(
enabled.core12.runtime_descriptor_array != vk::FALSE,
"runtimeDescriptorArray",
)?;
}
if flags.contains(vk::DescriptorBindingFlags::UPDATE_UNUSED_WHILE_PENDING) {
feature_check(
enabled
.core12
.descriptor_binding_update_unused_while_pending
!= vk::FALSE,
"descriptorBindingUpdateUnusedWhilePending",
)?;
feature_check(
descriptor_type_update_after_bind_enabled(binding.kind, enabled),
descriptor_type_update_after_bind_feature(binding.kind),
)?;
}
if flags.contains(vk::DescriptorBindingFlags::UPDATE_AFTER_BIND) {
feature_check(
descriptor_type_update_after_bind_enabled(binding.kind, enabled),
descriptor_type_update_after_bind_feature(binding.kind),
)?;
}
}
Ok(())
}
/// Validates layout membership, array bounds, supported type, owner facts, and buffer ranges.
/// Dynamic alignment values are Vulkan's `min*BufferOffsetAlignment` limits.
pub fn validate_descriptor_writes(
layout: &[crate::pipeline::DescriptorSetLayoutBindingDesc],
writes: &[RawDescriptorWriteFacts],
enabled_features: &PhysicalDeviceFeatures,
min_uniform_buffer_offset_alignment: u64,
min_storage_buffer_offset_alignment: u64,
) -> Result<DescriptorWritePlan, DescriptorValidationError> {
validate_descriptor_binding_features(layout, enabled_features)?;
let mut validated = Vec::with_capacity(writes.len());
for write in writes {
let binding = layout
.iter()
.find(|binding| binding.binding == write.binding)
.ok_or(DescriptorValidationError::UnknownBinding(write.binding))?;
if write.array_element >= binding.count {
return Err(DescriptorValidationError::ArrayElementOutOfBounds {
binding: write.binding,
element: write.array_element,
count: binding.count,
});
}
if write.descriptor_type != binding.kind {
return Err(DescriptorValidationError::TypeMismatch {
binding: write.binding,
expected: binding.kind,
actual: write.descriptor_type,
});
}
if !is_supported_descriptor_type(write.descriptor_type) {
return Err(DescriptorValidationError::UnsupportedDescriptorType(
write.descriptor_type,
));
}
let normalized_range = match (write.descriptor_type, write.resource) {
(
vk::DescriptorType::UNIFORM_BUFFER
| vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC
| vk::DescriptorType::STORAGE_BUFFER
| vk::DescriptorType::STORAGE_BUFFER_DYNAMIC,
RawDescriptorResourceFacts::Buffer {
buffer_len,
offset,
range,
dynamic,
},
) => {
let range = normalize_buffer_range(buffer_len, offset, range)?;
if dynamic
!= (write.descriptor_type == vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC
|| write.descriptor_type == vk::DescriptorType::STORAGE_BUFFER_DYNAMIC)
{
return Err(DescriptorValidationError::ResourceTypeMismatch(
write.descriptor_type,
));
}
if dynamic {
let alignment =
if write.descriptor_type == vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC {
min_uniform_buffer_offset_alignment
} else {
min_storage_buffer_offset_alignment
};
if alignment == 0 {
return Err(DescriptorValidationError::InvalidDynamicAlignment);
}
if offset % alignment != 0 {
return Err(DescriptorValidationError::MisalignedDynamicOffset);
}
}
Some(range)
}
(
vk::DescriptorType::UNIFORM_TEXEL_BUFFER | vk::DescriptorType::STORAGE_TEXEL_BUFFER,
RawDescriptorResourceFacts::TexelBuffer {
buffer_len,
offset,
range,
},
) => Some(normalize_buffer_range(buffer_len, offset, range)?),
(
vk::DescriptorType::SAMPLED_IMAGE | vk::DescriptorType::STORAGE_IMAGE,
RawDescriptorResourceFacts::Image {
swapchain_backed,
has_view,
..
},
) => {
check_image_facts(swapchain_backed, has_view, false, false)?;
None
}
(
vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
RawDescriptorResourceFacts::Image {
swapchain_backed,
has_view,
has_sampler,
},
) => {
check_image_facts(swapchain_backed, has_view, has_sampler, true)?;
None
}
(vk::DescriptorType::SAMPLER, RawDescriptorResourceFacts::Sampler { has_sampler }) => {
if !has_sampler {
return Err(DescriptorValidationError::MissingSampler);
}
None
}
_ => {
return Err(DescriptorValidationError::ResourceTypeMismatch(
write.descriptor_type,
));
}
};
validated.push(ValidatedDescriptorWrite {
binding: write.binding,
array_element: write.array_element,
descriptor_type: write.descriptor_type,
normalized_range,
});
}
Ok(DescriptorWritePlan { writes: validated })
}
fn normalize_buffer_range(
buffer_len: u64,
offset: u64,
range: u64,
) -> Result<u64, DescriptorValidationError> {
if offset > buffer_len {
return Err(DescriptorValidationError::BufferRangeOutOfBounds);
}
let range = if range == vk::WHOLE_SIZE {
buffer_len - offset
} else {
range
};
if offset.checked_add(range).is_none_or(|end| end > buffer_len) {
return Err(DescriptorValidationError::BufferRangeOutOfBounds);
}
Ok(range)
}
fn check_image_facts(
swapchain_backed: bool,
has_view: bool,
has_sampler: bool,
sampler_required: bool,
) -> Result<(), DescriptorValidationError> {
if swapchain_backed {
return Err(DescriptorValidationError::SwapchainImage);
}
if !has_view {
return Err(DescriptorValidationError::MissingImageView);
}
if sampler_required && !has_sampler {
return Err(DescriptorValidationError::MissingImageSampler);
}
Ok(())
}
fn is_supported_descriptor_type(kind: vk::DescriptorType) -> bool {
matches!(
kind,
vk::DescriptorType::UNIFORM_BUFFER
| vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC
| vk::DescriptorType::STORAGE_BUFFER
| vk::DescriptorType::STORAGE_BUFFER_DYNAMIC
| vk::DescriptorType::SAMPLED_IMAGE
| vk::DescriptorType::STORAGE_IMAGE
| vk::DescriptorType::COMBINED_IMAGE_SAMPLER
| vk::DescriptorType::SAMPLER
| vk::DescriptorType::UNIFORM_TEXEL_BUFFER
| vk::DescriptorType::STORAGE_TEXEL_BUFFER
)
}
fn descriptor_type_update_after_bind_enabled(
kind: vk::DescriptorType,
features: &PhysicalDeviceFeatures,
) -> bool {
let core12 = &features.core12;
match kind {
vk::DescriptorType::UNIFORM_BUFFER | vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC => {
core12.descriptor_binding_uniform_buffer_update_after_bind != vk::FALSE
}
vk::DescriptorType::SAMPLED_IMAGE
| vk::DescriptorType::COMBINED_IMAGE_SAMPLER
| vk::DescriptorType::SAMPLER => {
core12.descriptor_binding_sampled_image_update_after_bind != vk::FALSE
}
vk::DescriptorType::STORAGE_IMAGE => {
core12.descriptor_binding_storage_image_update_after_bind != vk::FALSE
}
vk::DescriptorType::STORAGE_BUFFER | vk::DescriptorType::STORAGE_BUFFER_DYNAMIC => {
core12.descriptor_binding_storage_buffer_update_after_bind != vk::FALSE
}
vk::DescriptorType::UNIFORM_TEXEL_BUFFER => {
core12.descriptor_binding_uniform_texel_buffer_update_after_bind != vk::FALSE
}
vk::DescriptorType::STORAGE_TEXEL_BUFFER => {
core12.descriptor_binding_storage_texel_buffer_update_after_bind != vk::FALSE
}
_ => false,
}
}
fn descriptor_type_update_after_bind_feature(kind: vk::DescriptorType) -> &'static str {
match kind {
vk::DescriptorType::UNIFORM_BUFFER | vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC => {
"descriptorBindingUniformBufferUpdateAfterBind"
}
vk::DescriptorType::SAMPLED_IMAGE
| vk::DescriptorType::COMBINED_IMAGE_SAMPLER
| vk::DescriptorType::SAMPLER => "descriptorBindingSampledImageUpdateAfterBind",
vk::DescriptorType::STORAGE_IMAGE => "descriptorBindingStorageImageUpdateAfterBind",
vk::DescriptorType::STORAGE_BUFFER | vk::DescriptorType::STORAGE_BUFFER_DYNAMIC => {
"descriptorBindingStorageBufferUpdateAfterBind"
}
vk::DescriptorType::UNIFORM_TEXEL_BUFFER => {
"descriptorBindingUniformTexelBufferUpdateAfterBind"
}
vk::DescriptorType::STORAGE_TEXEL_BUFFER => {
"descriptorBindingStorageTexelBufferUpdateAfterBind"
}
_ => "descriptor type update-after-bind feature",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pipeline::DescriptorSetLayoutBindingDesc;
fn binding(
binding: u32,
kind: vk::DescriptorType,
count: u32,
) -> DescriptorSetLayoutBindingDesc {
DescriptorSetLayoutBindingDesc {
binding,
kind,
count,
stage: vk::ShaderStageFlags::FRAGMENT,
flags: None,
}
}
fn write(
binding: u32,
kind: vk::DescriptorType,
resource: RawDescriptorResourceFacts,
) -> RawDescriptorWriteFacts {
RawDescriptorWriteFacts {
binding,
array_element: 0,
descriptor_type: kind,
resource,
}
}
fn validate(
layout: &[DescriptorSetLayoutBindingDesc],
writes: &[RawDescriptorWriteFacts],
) -> Result<DescriptorWritePlan, DescriptorValidationError> {
validate_descriptor_writes(layout, writes, &PhysicalDeviceFeatures::default(), 16, 32)
}
#[test]
fn validates_supported_buffer_image_sampler_and_texel_writes() {
let layout = [
binding(0, vk::DescriptorType::UNIFORM_BUFFER, 1),
binding(1, vk::DescriptorType::SAMPLED_IMAGE, 1),
binding(2, vk::DescriptorType::SAMPLER, 1),
binding(3, vk::DescriptorType::UNIFORM_TEXEL_BUFFER, 1),
binding(4, vk::DescriptorType::COMBINED_IMAGE_SAMPLER, 1),
];
let writes = [
write(
0,
vk::DescriptorType::UNIFORM_BUFFER,
RawDescriptorResourceFacts::Buffer {
buffer_len: 128,
offset: 16,
range: vk::WHOLE_SIZE,
dynamic: false,
},
),
write(
1,
vk::DescriptorType::SAMPLED_IMAGE,
RawDescriptorResourceFacts::Image {
swapchain_backed: false,
has_view: true,
has_sampler: false,
},
),
write(
2,
vk::DescriptorType::SAMPLER,
RawDescriptorResourceFacts::Sampler { has_sampler: true },
),
write(
3,
vk::DescriptorType::UNIFORM_TEXEL_BUFFER,
RawDescriptorResourceFacts::TexelBuffer {
buffer_len: 64,
offset: 0,
range: 32,
},
),
write(
4,
vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
RawDescriptorResourceFacts::Image {
swapchain_backed: false,
has_view: true,
has_sampler: true,
},
),
];
let plan = validate(&layout, &writes).unwrap();
assert_eq!(plan.writes[0].normalized_range, Some(112));
assert_eq!(plan.writes[1].normalized_range, None);
assert_eq!(plan.writes[2].normalized_range, None);
assert_eq!(plan.writes[3].normalized_range, Some(32));
assert_eq!(plan.writes[4].normalized_range, None);
}
#[test]
fn reports_binding_element_type_and_buffer_range_errors() {
let layout = [binding(4, vk::DescriptorType::UNIFORM_BUFFER, 2)];
let facts = RawDescriptorResourceFacts::Buffer {
buffer_len: 32,
offset: 0,
range: 16,
dynamic: false,
};
assert_eq!(
validate(
&layout,
&[write(5, vk::DescriptorType::UNIFORM_BUFFER, facts)]
),
Err(DescriptorValidationError::UnknownBinding(5))
);
let mut out_of_bounds = write(4, vk::DescriptorType::UNIFORM_BUFFER, facts);
out_of_bounds.array_element = 2;
assert_eq!(
validate(&layout, &[out_of_bounds]),
Err(DescriptorValidationError::ArrayElementOutOfBounds {
binding: 4,
element: 2,
count: 2
})
);
assert!(matches!(
validate(
&layout,
&[write(4, vk::DescriptorType::STORAGE_BUFFER, facts)]
),
Err(DescriptorValidationError::TypeMismatch { .. })
));
let outside = RawDescriptorResourceFacts::Buffer {
buffer_len: 32,
offset: 24,
range: 16,
dynamic: false,
};
assert_eq!(
validate(
&layout,
&[write(4, vk::DescriptorType::UNIFORM_BUFFER, outside)]
),
Err(DescriptorValidationError::BufferRangeOutOfBounds)
);
}
#[test]
fn rejects_unsupported_and_swapchain_descriptors() {
let input_attachment = [binding(0, vk::DescriptorType::INPUT_ATTACHMENT, 1)];
assert_eq!(
validate(
&input_attachment,
&[write(
0,
vk::DescriptorType::INPUT_ATTACHMENT,
RawDescriptorResourceFacts::Sampler { has_sampler: false },
)]
),
Err(DescriptorValidationError::UnsupportedDescriptorType(
vk::DescriptorType::INPUT_ATTACHMENT
))
);
let image_layout = [binding(0, vk::DescriptorType::COMBINED_IMAGE_SAMPLER, 1)];
assert_eq!(
validate(
&image_layout,
&[write(
0,
vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
RawDescriptorResourceFacts::Image {
swapchain_backed: true,
has_view: true,
has_sampler: true,
},
)]
),
Err(DescriptorValidationError::SwapchainImage)
);
}
#[test]
fn checks_binding_flags_against_enabled_features_not_adapter_support() {
let layout = [DescriptorSetLayoutBindingDesc {
flags: Some(vk::DescriptorBindingFlags::PARTIALLY_BOUND),
..binding(7, vk::DescriptorType::SAMPLED_IMAGE, 4)
}];
let mut enabled = PhysicalDeviceFeatures::default();
assert_eq!(
validate_descriptor_binding_features(&layout, &enabled),
Err(DescriptorValidationError::FeatureNotEnabled {
binding: 7,
feature: "descriptorBindingPartiallyBound",
})
);
enabled.core12.descriptor_binding_partially_bound = vk::TRUE;
assert_eq!(
validate_descriptor_binding_features(&layout, &enabled),
Ok(())
);
}
#[test]
fn validates_dynamic_buffer_alignment_and_flags() {
let dynamic = [binding(0, vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC, 1)];
let misaligned = RawDescriptorResourceFacts::Buffer {
buffer_len: 128,
offset: 4,
range: 16,
dynamic: true,
};
assert_eq!(
validate(
&dynamic,
&[write(
0,
vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC,
misaligned
)]
),
Err(DescriptorValidationError::MisalignedDynamicOffset)
);
}
#[test]
fn generation_and_binding_map_helpers_track_invalidation_and_replacement() {
assert!(check_pool_generation(3, 3).is_ok());
assert!(matches!(
check_pool_generation(3, 4),
Err(DescriptorSetError::StaleSet {
allocated_generation: 3,
pool_generation: 4
})
));
let layout = [
DescriptorSetLayoutBindingDesc {
flags: Some(vk::DescriptorBindingFlags::PARTIALLY_BOUND),
..binding(0, vk::DescriptorType::SAMPLED_IMAGE, 3)
},
binding(1, vk::DescriptorType::SAMPLER, 1),
];
let mut entries = std::collections::BTreeMap::new();
entries.insert((0, 0), "old");
entries.insert((1, 0), "sampler");
assert!(validate_binding_completeness(&layout, &entries).is_ok());
let replaced = replace_binding_entries(&entries, [((0, 0), "new"), ((0, 2), "image")]);
assert_eq!(replaced.get(&(0, 0)), Some(&"new"));
assert_eq!(replaced.get(&(0, 1)), None);
assert_eq!(replaced.get(&(0, 2)), Some(&"image"));
assert_eq!(entries.get(&(0, 0)), Some(&"old"));
let incomplete = [binding(2, vk::DescriptorType::SAMPLER, 2)];
assert!(matches!(
validate_binding_completeness(&incomplete, &entries),
Err(DescriptorSetError::IncompleteBinding {
binding: 2,
element: 0
})
));
let unchanged = pool_generation_after_reset(4, || Err("reset failed"));
assert_eq!(unchanged, Err(PoolResetError::Reset("reset failed")));
assert_eq!(pool_generation_after_reset(4, || Ok::<_, ()>(())), Ok(5));
}
#[test]
fn clearing_binding_entries_releases_all_tracked_slots_only_for_that_binding() {
let retained = Arc::new(String::from("texture"));
let weak = Arc::downgrade(&retained);
let mut entries = std::collections::BTreeMap::from([
((0, 0), retained.clone()),
((0, 3), retained.clone()),
((1, 0), Arc::new(String::from("buffer"))),
]);
drop(retained);
clear_binding_entries(&mut entries, 0);
assert_eq!(entries.keys().copied().collect::<Vec<_>>(), [(1, 0)]);
assert_eq!(weak.strong_count(), 0);
}
#[test]
fn binding_map_replacement_retains_arc_owners() {
let owner = Arc::new(String::from("retained"));
let weak = Arc::downgrade(&owner);
let current = std::collections::BTreeMap::from([((0, 0), owner.clone())]);
drop(owner);
assert_eq!(weak.strong_count(), 1);
let replacement =
replace_binding_entries(&current, [((0, 1), Arc::new(String::from("second")))]);
drop(current);
assert_eq!(
weak.upgrade().as_deref().map(String::as_str),
Some("retained")
);
drop(replacement);
assert_eq!(weak.strong_count(), 0);
}
#[test]
fn update_after_bind_uses_feature_for_binding_descriptor_type() {
let layout = [DescriptorSetLayoutBindingDesc {
flags: Some(vk::DescriptorBindingFlags::UPDATE_AFTER_BIND),
..binding(2, vk::DescriptorType::STORAGE_BUFFER, 1)
}];
let mut enabled = PhysicalDeviceFeatures::default();
enabled
.core12
.descriptor_binding_uniform_buffer_update_after_bind = vk::TRUE;
assert_eq!(
validate_descriptor_binding_features(&layout, &enabled),
Err(DescriptorValidationError::FeatureNotEnabled {
binding: 2,
feature: "descriptorBindingStorageBufferUpdateAfterBind",
})
);
enabled
.core12
.descriptor_binding_storage_buffer_update_after_bind = vk::TRUE;
assert_eq!(
validate_descriptor_binding_features(&layout, &enabled),
Ok(())
);
}
#[test]
fn update_unused_while_pending_requires_type_specific_update_after_bind_feature() {
let layout = [DescriptorSetLayoutBindingDesc {
flags: Some(vk::DescriptorBindingFlags::UPDATE_UNUSED_WHILE_PENDING),
..binding(3, vk::DescriptorType::STORAGE_IMAGE, 1)
}];
let mut enabled = PhysicalDeviceFeatures::default();
enabled
.core12
.descriptor_binding_update_unused_while_pending = vk::TRUE;
enabled
.core12
.descriptor_binding_sampled_image_update_after_bind = vk::TRUE;
assert_eq!(
validate_descriptor_binding_features(&layout, &enabled),
Err(DescriptorValidationError::FeatureNotEnabled {
binding: 3,
feature: "descriptorBindingStorageImageUpdateAfterBind",
})
);
enabled
.core12
.descriptor_binding_storage_image_update_after_bind = vk::TRUE;
assert_eq!(
validate_descriptor_binding_features(&layout, &enabled),
Ok(())
);
}
#[test]
fn buffer_is_arc_compatible() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Buffer>();
}
}

View file

@ -0,0 +1,8 @@
pub use renderer_device::{Device, Error, Result, device, make_extension};
pub mod buffers;
pub mod descriptors;
pub mod images;
pub mod memory;
pub mod pipeline;
pub mod util;

View file

@ -0,0 +1,411 @@
//! Shared aliasable memory allocation helpers for buffers and images.
use std::sync::Arc;
use ash::vk;
use crate::{
Device,
buffers::Buffer,
device::{DeviceObject, GpuAllocation, MemoryBinding},
images::Image,
};
/// Vulkan memory requirements returned by the device layer.
pub use crate::device::MemoryRequirements;
/// Errors from resource requirement queries, validation, and shared binding.
#[derive(Debug)]
pub enum Error {
RequirementQuery(crate::Error),
Bind(crate::Error),
Allocation(crate::Error),
MisalignedOffset {
offset: u64,
alignment: u64,
},
IncompatibleMemoryType {
memory_type_index: u32,
memory_type_bits: u32,
},
OutOfBounds {
offset: u64,
end: u64,
allocation_size: u64,
},
AlreadyBound,
AlreadyAllocated,
RequiresDedicatedAllocation,
GroupMemoryTypeMismatch {
memory_type_index: u32,
},
GroupCapacityExceeded {
requested_end: u64,
capacity: u64,
},
EmptyGroup,
NotAllocated,
InvalidGroupRequirements,
OffsetOverflow,
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::RequirementQuery(error) => {
write!(f, "failed to query resource memory requirements: {error}")
}
Self::Bind(error) => write!(f, "failed to bind resource memory: {error}"),
Self::Allocation(error) => write!(f, "failed to allocate group memory: {error}"),
Self::MisalignedOffset { offset, alignment } => write!(
f,
"allocation offset {offset} is not aligned to {alignment}"
),
Self::IncompatibleMemoryType {
memory_type_index,
memory_type_bits,
} => write!(
f,
"allocation memory type {memory_type_index} is not allowed by mask {memory_type_bits:#010x}"
),
Self::OutOfBounds {
offset,
end,
allocation_size,
} => write!(
f,
"resource range [{offset}, {end}) does not fit allocation size {allocation_size}"
),
Self::AlreadyBound => f.write_str("resource is already bound to an allocation"),
Self::AlreadyAllocated => f.write_str("memory group has already been allocated"),
Self::RequiresDedicatedAllocation => f.write_str(
"resources requiring dedicated allocations cannot be added to a memory group",
),
Self::GroupMemoryTypeMismatch { memory_type_index } => write!(
f,
"resource memory requirements do not include memory type {memory_type_index}"
),
Self::GroupCapacityExceeded {
requested_end,
capacity,
} => write!(
f,
"memory group capacity exceeded: requested end {requested_end}, capacity {capacity}"
),
Self::EmptyGroup => f.write_str("memory group has no members"),
Self::NotAllocated => f.write_str("memory group has not been allocated"),
Self::InvalidGroupRequirements => {
f.write_str("memory group size or alignment is invalid")
}
Self::OffsetOverflow => f.write_str("memory group offset arithmetic overflowed"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::RequirementQuery(error) | Self::Bind(error) | Self::Allocation(error) => {
Some(error)
}
_ => None,
}
}
}
pub type Result<T> = core::result::Result<T, Error>;
pub(crate) fn validate_binding(
requirements: &MemoryRequirements,
allocation: &DeviceObject<GpuAllocation>,
offset: u64,
) -> Result<()> {
let alignment = requirements.alignment;
if alignment == 0 || !offset.is_multiple_of(alignment) {
return Err(Error::MisalignedOffset { offset, alignment });
}
let memory_type_index = allocation.memory_type_index();
if requirements.memory_type_bits & (1 << memory_type_index) == 0 {
return Err(Error::IncompatibleMemoryType {
memory_type_index,
memory_type_bits: requirements.memory_type_bits,
});
}
let end = offset
.checked_add(requirements.size)
.ok_or(Error::OutOfBounds {
offset,
end: u64::MAX,
allocation_size: allocation.size(),
})?;
if end > allocation.size() {
return Err(Error::OutOfBounds {
offset,
end,
allocation_size: allocation.size(),
});
}
Ok(())
}
enum Member<'a> {
Buffer {
resource: &'a mut Buffer,
offset: u64,
},
Image {
resource: &'a mut Image,
offset: u64,
},
}
/// Packs resources into a single aliasable allocation with a fixed memory type.
///
/// Members are exclusively borrowed until `bind_all` consumes the group. Their
/// allocation ownership is then transferred to each resource; the group retains
/// no allocation handle after binding.
pub struct MemoryGroup<'a> {
memory_type_index: u32,
property_flags: vk::MemoryPropertyFlags,
size: u64,
alignment: u64,
next_offset: u64,
members: Vec<Member<'a>>,
allocation: Option<Arc<DeviceObject<GpuAllocation>>>,
}
impl<'a> MemoryGroup<'a> {
/// Creates an empty group for a fixed memory type and allocation capacity.
pub fn new(
memory_type_index: u32,
property_flags: vk::MemoryPropertyFlags,
size: u64,
alignment: u64,
) -> Result<Self> {
if memory_type_index >= u32::BITS || size == 0 || alignment == 0 {
return Err(Error::InvalidGroupRequirements);
}
Ok(Self {
memory_type_index,
property_flags,
size,
alignment,
next_offset: 0,
members: Vec::new(),
allocation: None,
})
}
/// Adds a buffer, placing it at the next suitably aligned offset.
pub fn add_buffer(&mut self, resource: &'a mut Buffer) -> Result<u64> {
if self.allocation.is_some() {
return Err(Error::AlreadyAllocated);
}
let requirements = resource.memory_requirements()?;
self.add_member_type_check(&requirements)?;
let offset = self.next_member_offset(&requirements)?;
let next_offset = offset
.checked_add(requirements.size)
.ok_or(Error::OffsetOverflow)?;
self.members.push(Member::Buffer { resource, offset });
self.next_offset = next_offset;
Ok(offset)
}
/// Adds either resource kind through a uniform entry point.
pub fn add(&mut self, member: MemoryMember<'a>) -> Result<u64> {
match member {
MemoryMember::Buffer(resource) => self.add_buffer(resource),
MemoryMember::Image(resource) => self.add_image(resource),
}
}
/// Adds an image, placing it at the next suitably aligned offset.
pub fn add_image(&mut self, resource: &'a mut Image) -> Result<u64> {
if self.allocation.is_some() {
return Err(Error::AlreadyAllocated);
}
let requirements = resource.memory_requirements()?;
self.add_member_type_check(&requirements)?;
let offset = self.next_member_offset(&requirements)?;
let next_offset = offset
.checked_add(requirements.size)
.ok_or(Error::OffsetOverflow)?;
self.members.push(Member::Image { resource, offset });
self.next_offset = next_offset;
Ok(offset)
}
/// Allocates the group's shared backing memory.
pub fn allocate(&mut self, device: &Device) -> Result<()> {
if self.members.is_empty() {
return Err(Error::EmptyGroup);
}
if self.allocation.is_some() {
return Err(Error::AlreadyAllocated);
}
let requirements = MemoryRequirements {
size: self.size,
alignment: self.alignment,
memory_type_bits: 1 << self.memory_type_index,
requires_dedicated_allocation: false,
prefers_dedicated_allocation: false,
};
let allocation = device
.allocate_memory(&requirements, self.property_flags)
.map_err(Error::Allocation)?;
for member in &self.members {
let (requirements, offset) = match member {
Member::Buffer { resource, offset } => (resource.memory_requirements()?, *offset),
Member::Image { resource, offset } => (resource.memory_requirements()?, *offset),
};
validate_binding(&requirements, allocation.as_ref(), offset)?;
}
self.allocation = Some(allocation);
Ok(())
}
/// Binds every member and transfers the allocation reference into each resource.
pub fn bind_all(mut self) -> Result<()> {
let allocation = self.allocation.take().ok_or(Error::NotAllocated)?;
// Validate resources first to preserve the same resource-level error mapping as
// individual binding, then prepare the ordered device batch from cached requirements.
let mut bindings = Vec::with_capacity(self.members.len());
for member in &self.members {
match member {
Member::Buffer { resource, offset } => {
resource.validate_unallocated()?;
let requirements = resource.memory_requirements()?;
validate_binding(&requirements, allocation.as_ref(), *offset)?;
resource.validate_device(allocation.device())?;
bindings.push(MemoryBinding::Buffer {
buffer: resource.raw(),
offset: *offset,
requirements,
});
}
Member::Image { resource, offset } => {
resource.validate_unallocated()?;
let requirements = resource.memory_requirements()?;
validate_binding(&requirements, allocation.as_ref(), *offset)?;
resource.validate_device(allocation.device())?;
bindings.push(MemoryBinding::Image {
image: resource.raw(),
offset: *offset,
requirements,
});
}
}
}
if let Err(bind_error) = allocation
.device()
.bind_group_at_offset(&allocation, &bindings)
{
// A Vulkan bind failure leaves its earlier prefix bound; retain its allocation just
// as repeated `bind_allocation` calls would have done.
for (index, member) in self.members.into_iter().enumerate() {
if index >= bind_error.bound_count {
break;
}
match member {
Member::Buffer { resource, .. } => {
resource.set_shared_allocation(allocation.clone());
}
Member::Image { resource, .. } => {
resource.set_shared_allocation(allocation.clone());
}
}
}
return Err(Error::Bind(bind_error.error));
}
for member in self.members {
match member {
Member::Buffer { resource, .. } => {
resource.set_shared_allocation(allocation.clone());
}
Member::Image { resource, .. } => {
resource.set_shared_allocation(allocation.clone());
}
}
}
// `allocation` drops here. Only successfully bound resources retain it.
Ok(())
}
fn add_member_type_check(&self, requirements: &MemoryRequirements) -> Result<()> {
if requirements.requires_dedicated_allocation {
return Err(Error::RequiresDedicatedAllocation);
}
if requirements.memory_type_bits & (1 << self.memory_type_index) == 0 {
return Err(Error::GroupMemoryTypeMismatch {
memory_type_index: self.memory_type_index,
});
}
Ok(())
}
fn next_member_offset(&self, requirements: &MemoryRequirements) -> Result<u64> {
if requirements.alignment == 0 {
return Err(Error::InvalidGroupRequirements);
}
let alignment = checked_lcm(requirements.alignment, self.alignment)?;
let offset = (self
.next_offset
.checked_add(alignment - 1)
.ok_or(Error::OffsetOverflow)?
/ alignment)
.checked_mul(alignment)
.ok_or(Error::OffsetOverflow)?;
let end = offset
.checked_add(requirements.size)
.ok_or(Error::OffsetOverflow)?;
if end > self.size {
return Err(Error::GroupCapacityExceeded {
requested_end: end,
capacity: self.size,
});
}
Ok(offset)
}
}
fn checked_lcm(lhs: u64, rhs: u64) -> Result<u64> {
let gcd = {
let (mut lhs, mut rhs) = (lhs, rhs);
while rhs != 0 {
(lhs, rhs) = (rhs, lhs % rhs);
}
lhs
};
lhs.checked_div(gcd)
.and_then(|lhs| lhs.checked_mul(rhs))
.ok_or(Error::OffsetOverflow)
}
/// A resource accepted by [`MemoryGroup::add`].
pub enum MemoryMember<'a> {
Buffer(&'a mut Buffer),
Image(&'a mut Image),
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,170 @@
pub(crate) mod weak_vec {
#![allow(dead_code)]
//! Module containing the [`WeakVec`] API.
use std::{sync::Weak, vec::Vec};
/// An optimized container for `Weak` references of `T` that minimizes reallocations by
/// dropping older elements that no longer have strong references to them.
#[derive(Debug)]
pub(crate) struct WeakVec<T> {
inner: Vec<Weak<T>>,
}
impl<T> Default for WeakVec<T> {
fn default() -> Self {
Self {
inner: Default::default(),
}
}
}
impl<T> WeakVec<T> {
pub(crate) fn new() -> Self {
Self { inner: Vec::new() }
}
/// Pushes a new element to this collection.
///
/// If the inner Vec needs to be reallocated, we will first drop older elements that
/// no longer have strong references to them.
pub(crate) fn push(&mut self, value: Weak<T>) {
if self.inner.len() == self.inner.capacity() {
// Iterating backwards has the advantage that we don't do more work than we have to.
for i in (0..self.inner.len()).rev() {
if self.inner[i].strong_count() == 0 {
self.inner.swap_remove(i);
}
}
// Make sure our capacity is twice the number of live elements.
// Leaving some spare capacity ensures that we won't re-scan immediately.
self.inner.reserve_exact(self.inner.len());
}
self.inner.push(value);
}
}
pub(crate) struct WeakVecIter<T> {
inner: std::vec::IntoIter<Weak<T>>,
}
impl<T> Iterator for WeakVecIter<T> {
type Item = Weak<T>;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
}
impl<T> IntoIterator for WeakVec<T> {
type Item = Weak<T>;
type IntoIter = WeakVecIter<T>;
fn into_iter(self) -> Self::IntoIter {
WeakVecIter {
inner: self.inner.into_iter(),
}
}
}
}
pub(crate) use weak_vec::WeakVec;
pub fn eq_f32(lhs: f32, rhs: f32) -> bool {
use core::num::FpCategory::*;
match (lhs.classify(), rhs.classify()) {
(Zero, Zero) | (Nan, Nan) => true,
(Infinite, Infinite) => lhs.signum() == rhs.signum(),
_ => lhs == rhs,
}
}
pub fn ord_f32(lhs: f32, rhs: f32) -> core::cmp::Ordering {
use core::cmp::Ordering;
use core::num::FpCategory::*;
match (lhs.classify(), rhs.classify()) {
(Zero, Zero) | (Nan, Nan) => Ordering::Equal,
(Infinite, Infinite) => {
core::cmp::PartialOrd::partial_cmp(&lhs.signum(), &rhs.signum()).unwrap()
}
_ => core::cmp::PartialOrd::partial_cmp(&lhs, &rhs).unwrap(),
}
}
pub fn hash_f32<H: std::hash::Hasher>(state: &mut H, f: f32) {
use std::hash::Hash;
let classify = f.classify();
match classify {
std::num::FpCategory::Nan => (classify as u8).hash(state),
std::num::FpCategory::Infinite | std::num::FpCategory::Zero => {
(classify as u8, f.signum() as i8).hash(state)
}
std::num::FpCategory::Subnormal | std::num::FpCategory::Normal => f.to_bits().hash(state),
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy)]
pub struct F32(f32);
impl F32 {
#[inline]
pub fn from_bits(bits: u32) -> Self {
Self(f32::from_bits(bits))
}
#[inline]
pub fn into_bits(self) -> u32 {
self.0.to_bits()
}
}
impl From<f32> for F32 {
fn from(value: f32) -> Self {
Self(value)
}
}
impl From<F32> for f32 {
fn from(value: F32) -> Self {
value.0
}
}
impl std::ops::Deref for F32 {
type Target = f32;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for F32 {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl PartialEq for F32 {
fn eq(&self, other: &Self) -> bool {
eq_f32(self.0, other.0)
}
}
impl Eq for F32 {}
impl PartialOrd for F32 {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(ord_f32(self.0, other.0))
}
}
impl Ord for F32 {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
ord_f32(self.0, other.0)
}
}
impl core::hash::Hash for F32 {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
hash_f32(state, self.0)
}
}

View file

@ -0,0 +1,90 @@
use std::sync::Arc;
use ash::vk;
use renderer_device::{
device::{MemoryLocation, QueueFlags},
testing::TestVulkan,
};
use renderer_resources::{
buffers::{Buffer, BufferDesc},
pipeline::{
BufferDescriptor, DescriptorPool, DescriptorPoolDesc, DescriptorSetAllocDesc,
DescriptorSetLayout, DescriptorSetLayoutBindingDesc, DescriptorSetLayoutDesc,
},
};
#[test]
fn managed_set_write_and_pool_reset_under_validation() {
let Some(test) = TestVulkan::acquire() else {
return;
};
let device = test.device().clone();
let buffer = Arc::new(
Buffer::new(
device.clone(),
BufferDesc {
size: 256,
usage: vk::BufferUsageFlags::UNIFORM_BUFFER,
queue_families: QueueFlags::empty(),
mem_location: MemoryLocation::CpuToGpu,
..Default::default()
},
)
.expect("create descriptor buffer"),
);
let bindings = [DescriptorSetLayoutBindingDesc {
binding: 0,
count: 1,
kind: vk::DescriptorType::UNIFORM_BUFFER,
stage: vk::ShaderStageFlags::VERTEX,
flags: None,
}];
let layout = DescriptorSetLayout::new(
device.clone(),
DescriptorSetLayoutDesc {
bindings: &bindings,
..Default::default()
},
)
.expect("create descriptor set layout");
let sizes = [vk::DescriptorPoolSize::default()
.ty(vk::DescriptorType::UNIFORM_BUFFER)
.descriptor_count(1)];
let pool = DescriptorPool::new(
device,
DescriptorPoolDesc {
sizes: &sizes,
max_sets: 1,
..Default::default()
},
)
.expect("create descriptor pool");
let mut set = pool
.allocate_managed(DescriptorSetAllocDesc {
name: None,
layout: &layout,
})
.expect("allocate managed descriptor set");
let mut writer = set.writer().expect("create descriptor writer");
writer
.write_buffer(
0,
0,
&[BufferDescriptor {
buffer: &buffer,
offset: 0,
range: 256,
}],
)
.expect("stage buffer descriptor");
writer.commit().expect("commit buffer descriptor");
test.device().wait_idle().expect("wait for device idle");
drop(set);
// SAFETY: no command uses this set, the device is idle, and this test excludes concurrent use.
unsafe { pool.reset() }.expect("reset descriptor pool");
drop(pool);
drop(layout);
drop(buffer);
test.assert_no_validation_errors();
}

View file

@ -24,10 +24,8 @@ mod components {
pub struct Text(pub String); pub struct Text(pub String);
} }
const ROBOTO_BYTES: &[u8] = const ROBOTO_BYTES: &[u8] = include_bytes!("../../../assets/fonts/Roboto.ttf");
include_bytes!("../../../assets/fonts/Roboto/Roboto-VariableFont_wdth,wght.ttf"); const NOTO_SANS_HAN_BYTES: &[u8] = include_bytes!("../../../assets/fonts/NotoSansSC.ttf");
const NOTO_SANS_HAN_BYTES: &[u8] =
include_bytes!("../../../assets/fonts/Noto_Sans_SC/NotoSansSC-VariableFont_wght.ttf");
def_monotonic_id!(pub FontId); def_monotonic_id!(pub FontId);
@ -42,7 +40,7 @@ impl std::fmt::Debug for FontData {
} }
} }
#[derive(Debug, Default)] #[derive(Debug, Default, TypePath)]
struct FontLoader; struct FontLoader;
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]

View file

@ -0,0 +1,375 @@
# Descriptor Set Ownership & Tracking Plan
Status: approved direction, not yet implemented
Scope: `crates/renderer_device` (enabled-feature source), `crates/renderer_resources` (primary), `crates/renderer_rendergraph` (consumer), `crates/renderer` (egui migration, separate change)
## Problem
`renderer_resources::DescriptorPool::allocate` returns raw `vk::DescriptorSet` handles with no
record of what they bind (`pipeline.rs:233-287`). The render graph's draw packets therefore
require callers to manually declare every descriptor-bound resource with accurate
range/stage/access/layout (`GraphDescriptorSet` in `renderer_rendergraph/src/resources.rs:170-197`)
— manual, error-prone, and unverifiable. Bound resources can also be dropped while a set is
still in use; nothing detects this.
## Approved direction
1. **Managed descriptor sets retain owners of the resources and objects they bind** —
`Arc<Buffer>`, `Arc<ImageView>`, `Arc<Sampler>`, and texel-buffer views as appropriate.
`ImageView` retains its `Arc<Image>` (`images.rs:994-1008`), but this is not a swapchain
lifetime token (see below). This ownership protects CPU object lifetimes, not submitted GPU
work by itself.
2. **Importing a managed descriptor set into the graph transitively imports its resources.**
Identity uses `Arc::ptr_eq` first and raw-handle equality fallback. The graph deduplicates
resources and merges per-command draw-access scopes; descriptor use never changes an
imported resource's initial state.
3. **Future, out of scope:** graph-created descriptor sets binding graph-created transient
resources via a graph-owned writer built on a reusable validation/write-plan interface in
`renderer_resources`. It has no graph types and avoids forking descriptor validation.
4. The raw escape hatch stays, clearly named untracked, owning nothing and never eligible for
transitive import.
5. `renderer_resources` stays graph-agnostic: metadata is generic Vulkan facts; graph-side
mapping lives in `renderer_rendergraph`. Zero graph-shaped types in `renderer_resources`.
6. Descriptor owner replacement, managed-set/pool destruction, and reset are caller-synchronized
against GPU work for this skeleton. Dropping wrappers is not evidence that GPU use retired.
A later design may replace this contract with explicit retirement tokens/GPU-usage tracking.
Pool reset also invalidates every Vulkan set allocated from that pool, independently of GPU
retirement. Managed wrappers surviving a successful reset must fail validation before
wrapper-mediated import, update, or bind attempts; the generation check detects invalidation,
but cannot revoke raw handles already exposed by `raw()`. Reset must also be externally
synchronized against every managed-set use; generation validation does not make concurrent
reset/use safe.
7. Swapchain-backed images are rejected by managed image writes with an explicit error. A
managed `Arc<ImageView>` does not keep its swapchain alive (`renderer_resources/AGENTS.md:39-47`);
rejecting these images avoids representing a false ownership guarantee.
8. Documented invariant changes must be reflected in `crates/renderer_resources/AGENTS.md`
(it currently says descriptor sets are not wrapped in owning resource objects). Any
`renderer` re-export decisions are flagged separately.
## Design decisions
### Buffer Arc model
- Store `Arc<Buffer>` in managed sets; callers wrap buffers after construction.
Do NOT make `Buffer` internally Arc-backed.
- Rationale: least invasive; preserves value-based `Buffer::new`/`map`/`map_mut`
(`buffers.rs:75-89,145-160`) and raw-handle equality (`buffers.rs:81-86`).
- Same pattern as `ImageView` -> `Arc<Image>`.
### Managed set layout and supported descriptor types
- A managed set owns: its pool-allocation lifetime token, raw handle, retained layout facts
(binding number/type/count/stages/flags), and a binding map keyed by
`(binding, array_element)`.
- Supported managed descriptor types are `UNIFORM_BUFFER`, `UNIFORM_BUFFER_DYNAMIC`,
`STORAGE_BUFFER`, `STORAGE_BUFFER_DYNAMIC`, `SAMPLED_IMAGE`, `STORAGE_IMAGE`,
`COMBINED_IMAGE_SAMPLER`, `SAMPLER`, `UNIFORM_TEXEL_BUFFER`, and `STORAGE_TEXEL_BUFFER`.
Input attachments, acceleration structures, inline uniform blocks, and every other type
are explicitly rejected by managed allocation/write validation until separately designed.
- Binding entries retain the corresponding owners:
- uniform/storage buffer (including dynamic): `{ Arc<Buffer>, offset, range }`;
- sampled/storage image and combined image sampler: `{ Arc<ImageView>, optional Arc<Sampler>,
normalized subresource range, layout }` (combined requires a sampler);
- sampler-only: `{ Arc<Sampler> }` with no image/view;
- uniform/storage texel buffer: `{ Arc<BufferView>, texel range }` (or an equivalent
resource-crate-owned buffer-view wrapper that owns its Vulkan view and buffer).
- Swapchain-backed images are rejected for every managed image descriptor kind, including
combined image sampler. The error is returned before updating Vulkan descriptors; callers
needing swapchain images use the raw untracked route and must separately retain the frame /
swapchain token through completion.
- Slots may be empty only for `PARTIALLY_BOUND` bindings (egui needs sparse arrays +
update-after-bind). Writes replace retained entries only after the Vulkan update succeeds.
- Validate descriptor-indexing features for the actual binding flags and descriptor types
before creating a managed layout or set. Layout flags alone are not a capability check.
In particular validate each requested `PARTIALLY_BOUND`, `UPDATE_AFTER_BIND`,
`VARIABLE_DESCRIPTOR_COUNT`, and update-unused-while-pending behavior against enabled device
features and the specific descriptor type's feature bit; reject unsupported requests. This check
must use the enabled-feature snapshot exposed by `renderer_device::Device`, never the adapter's
queried-supported feature data (`DeviceInner::features()` currently returns
`adapter.features`; `renderer_device/src/device.rs:594-596`). `DeviceInner` currently retains
`adapter` and `enabled_extensions` but not enabled features (`device.rs:156-168`), although
`create_logical_device` receives the feature chain used for device creation (`device.rs:398-413`).
Add retained enabled-feature state and an accessor in `renderer_device`; layout creation and
managed write validation in `renderer_resources` must consult that accessor for binding flags and
type-specific requirements (including partially-bound, update-after-bind, runtime arrays, and
update-unused-while-pending). Keep this within the existing dependency direction:
`renderer_resources` consumes the data through its existing `renderer_device` re-exports; add
no dependency edge.
### Shared validation/write path
- Managed `DescriptorWriter::write_*` produces staged binding entries and raw Vulkan write
infos, then invokes a reusable `renderer_resources` validation/write-plan API. The API accepts
retained layout facts plus raw write infos/resource facts and returns a validated write plan
or structured errors. It checks descriptor type, binding/array bounds, descriptor ranges,
completeness/partial-binding rules, and type-specific owner facts before Vulkan is updated.
- Commit applies the validated Vulkan writes and only then swaps the retained owner entries.
The same graph-agnostic validation/write-plan interface is the future graph-owned writer's
route; it does not take graph types or rely on `write_raw_untracked`.
- `write_raw_untracked` remains a separate Vulkan-only escape hatch. It performs no managed
validation, retains no owners, and cannot be passed off as the shared managed write path.
### Untracked escape hatch and reset synchronization
- Renamed `allocate_untracked`: returns raw handles, retains no bindings, owns nothing, and
cannot be transitively imported.
- Managed-pool reset and destruction are explicitly externally synchronized operations. Before
reset/destruction, the caller must ensure every GPU submission that could access any set or
descriptor owner from the pool has completed, prevent new submissions and descriptor updates,
and exclude every concurrent managed-set use until reset/destruction is complete. This exclusion
covers allocation from the pool, writer updates/commit, graph recording and descriptor-set
import, and command-buffer recording/binding (as well as other operations that inspect or use a
managed set). This mirrors Vulkan's external-synchronization requirement for pool reset versus
descriptor-set use; it does not impose a stricter guarantee than Vulkan. This is an API safety
contract (prefer an `unsafe` reset entry point with this requirement in its safety docs);
wrapper-drop checks alone are insufficient.
- Every pool owns a monotonically increasing generation counter shared with its managed sets.
Allocation records the current generation in each wrapper. On a successful reset, while holding
the pool mutex and after Vulkan reset succeeds, increment the generation; failed reset leaves it
unchanged. Each managed-set operation that can expose or use the raw handle (`raw`, bindings/
writer access, write/commit, graph import, and command binding) must compare the recorded
generation with the pool's current generation and return a stale-set error on mismatch. Make
raw-handle access fallible (`raw() -> Result<vk::DescriptorSet>`) rather than exposing an
unchecked handle from a surviving wrapper. Recheck at graph import and command binding so a set
imported before reset is detected as stale afterward. These generation checks are defense-in-
depth for stale wrappers after reset, not a substitute for external synchronization: the pool
mutex is not held across command use, so a check immediately before binding cannot close the
check/use race if reset is concurrent. The generation mechanism detects invalidation of all
managed sets, including wrappers still strongly held by callers; reset does not need to find or
mutate those wrappers. `allocate_untracked` remains outside this managed-wrapper check and its
raw handles remain caller responsibility.
- A handle returned by `raw()` escapes the wrapper's control. Its generation check can reject a
wrapper already stale when `raw()` is called, but cannot eliminate the interval between the
check and use or protect an escaped handle if reset later occurs. Once a raw handle escapes,
the caller owns its validity and must uphold the same external-synchronization contract.
- The existing pool mutex only serializes CPU operations, not in-flight GPU work
(`pipeline.rs:253-285`). Dropping all managed wrappers does not prove GPU retirement. The same
caller synchronization applies to replacing/dropping retained binding owners: the caller must
not release or replace an owner while submitted GPU work may still use that descriptor
version. A future retirement-token/GPU-usage-tracking hook can automate this discipline.
- Raw/untracked reset remains caller-synchronized as well.
### Dynamic offsets
- Dynamic offsets are bind-time data, not owned resources. The graph packet carries offsets
per descriptor set, and the draw path constructs the single correctly ordered offset slice
required by `cmd_bind_descriptor_sets`: set order, then dynamic binding number and array
element order as specified by the bound pipeline layouts.
- Validate exact offset count, per-type Vulkan alignment, overflow, and effective range bounds
(`descriptor base offset + dynamic offset + range <= buffer size`) before accepting/recording
the draw. Keep base descriptor ranges for ownership and use effective ranges for draw scopes.
## Transitive import design
- `RenderGraph::import_descriptor_set(Arc<DescriptorSet>, initial_states, dynamic_offsets)
-> Result<GraphDescriptorSet, GraphError>`. `initial_states` explicitly supplies the initial
`ResourceState` for each unique resource referenced by the set (or a documented caller
default expanded to each resource); it is never inferred from descriptor layout/access.
The draw packet retains the `Arc<DescriptorSet>`, `raw()`, graph-derived draw accesses, and
per-set dynamic offsets.
- Descriptor-derived access/layout describes what a draw will USE. It contributes only
per-command draw-access scopes, which the command compiler already derives from
`GraphDescriptorSet.accesses` (`renderer_rendergraph/src/commands.rs:190-216`). The resource's
initial `ResourceState` describes state before graph execution and remains caller-supplied,
because initial barrier generation reads `ResourceDesc::initial_state`
(`renderer_rendergraph/src/graph.rs:352-358,422-447`).
- **Identity index / dedupe:** maintain a graph-local identity index for imported buffers and
textures. Look up by `Arc::ptr_eq` first and Vulkan raw-handle equality fallback. Both
`import_buffer` and `import_texture` use this index rather than unconditionally appending
(`graph.rs:226-245`); `import_descriptor_set` routes every retained buffer/image through those
same methods. A direct import followed by set import (or vice versa) yields one `ResourceId`.
- **Initial-state merge:** on a duplicate explicit direct import, validate the newly supplied
initial state against the resource's recorded initial state; if they differ in stage, access,
or layout, return a conflicting-initial-state graph error. Do not OR-merge initial states.
Set import requires caller input for each resource; if it encounters a resource already
imported, the explicit state must match exactly or import errors. This prevents a fabricated
initial state and ensures the initial barrier source is unambiguous.
- **Draw-access scopes:** descriptor access is derived per binding and per draw, never folded
into `ResourceState`:
- uniform buffer -> `UNIFORM_READ`, exact effective offset/range;
- storage buffer -> conservative `SHADER_STORAGE_READ | SHADER_STORAGE_WRITE`, effective
offset/range;
- sampled image -> shader sampled read + descriptor image layout and normalized view range;
- storage image -> storage read|write in `GENERAL` and normalized view range;
- combined image sampler -> sampled-image read scope (sampler itself has no memory resource
hazard);
- sampler-only -> no graph memory access;
- uniform/storage texel buffer -> corresponding shader read or conservative storage read|write
over the view's buffer range;
- unsupported descriptor kinds or missing required owners -> explicit import/write error,
never guessed access.
Shader stages map from retained layout shader stages to graph pipeline stages.
- **Draw-scope merge:** deduplicate scopes by imported resource identity within the command and
merge compatible scopes by OR'ing stage/access masks while preserving their covered ranges.
Buffer ranges may remain separate if they are not safely coalescible. Texture layout
conflicts apply to overlapping draw scopes and produce an import error; disjoint subresource
ranges may retain distinct layouts. No draw-scope merge alters the resource's initial state.
- `track_descriptor_set` remains an explicitly unsafe-by-contract/deprecated raw route: it
cannot own or verify resources, and must not be used as though managed imports were safe.
## API sketches
```rust
// renderer_resources
impl DescriptorPool {
pub fn allocate_untracked(&self, descs: &[DescriptorSetAllocDesc])
-> VkResult<Vec<vk::DescriptorSet>>;
pub fn allocate_managed(&self, desc: DescriptorSetAllocDesc) -> Result<DescriptorSet>;
}
pub struct DescriptorSet { /* pool token, raw handle, pool generation snapshot, layout facts, binding map */ }
pub enum DescriptorResourceBinding {
Buffer { buffer: Arc<Buffer>, offset: u64, range: u64, dynamic: bool },
Image { view: Arc<ImageView>, sampler: Option<Arc<Sampler>>,
range: vk::ImageSubresourceRange, layout: vk::ImageLayout },
Sampler { sampler: Arc<Sampler> },
TexelBuffer { view: Arc<BufferView>, range: BufferRange },
}
pub struct TrackedDescriptorWrite {
pub binding: u32, pub array_element: u32,
pub descriptor_type: vk::DescriptorType,
pub stages: vk::ShaderStageFlags,
pub resource: DescriptorResourceBinding,
}
pub struct DescriptorWritePlan { /* validated Vulkan writes + staged retained entries */ }
pub fn validate_descriptor_writes(
layout: &DescriptorLayoutFacts,
writes: &[RawDescriptorWriteFacts],
) -> Result<DescriptorWritePlan>;
impl DescriptorSet {
// Checks generation as defense-in-depth; caller must externally synchronize reset against
// every use. Once returned, raw-handle validity is the caller's responsibility.
pub fn raw(&self) -> Result<vk::DescriptorSet>;
pub fn bindings(&self) -> Result<impl Iterator<Item = &TrackedDescriptorWrite>>;
pub fn writer(&mut self) -> Result<DescriptorWriter<'_>>;
}
impl DescriptorWriter<'_> {
pub fn write_buffer(&mut self, binding: u32, first_element: u32,
values: &[BufferDescriptor<'_>]) -> Result<&mut Self>;
pub fn write_image(&mut self, binding: u32, first_element: u32,
values: &[ImageDescriptor<'_>]) -> Result<&mut Self>;
pub fn write_sampler(&mut self, binding: u32, first_element: u32,
values: &[Arc<Sampler>]) -> Result<&mut Self>;
pub fn write_texel_buffer(&mut self, binding: u32, first_element: u32,
values: &[Arc<BufferView>]) -> Result<&mut Self>;
pub fn commit(self, set: &mut DescriptorSet) -> Result<()>;
}
```
```rust
// renderer_rendergraph
impl RenderGraph {
pub fn import_descriptor_set(
&mut self,
set: Arc<DescriptorSet>,
initial_states: &[(DescriptorResourceIdentity, ResourceState)],
dynamic_offsets: &[u32],
) -> Result<GraphDescriptorSet, GraphError>;
}
pub struct GraphDescriptorSet {
/* private raw handle + retained Arc<DescriptorSet>; no unchecked public raw-handle field */
pub accesses: Vec<DescriptorResourceAccess>,
pub dynamic_offsets: Vec<u32>,
/* retained Arc<DescriptorSet> */
}
```
Exact signatures may be adjusted to existing project conventions during implementation.
## Staged tasks
### Task 1 — Enabled-feature source (`renderer_device`)
Retain the exact `PhysicalDeviceFeatures` core and versioned feature chain passed to
`create_logical_device` in `DeviceInner` as the enabled-feature snapshot, and expose it through a
clearly named accessor distinct from the existing adapter-supported `features()` accessor
(`device.rs:156-168,398-413,594-596`; feature-chain type and push logic in
`renderer_device/src/lib.rs:171-194`). Preserve the chain's enabled values after passing it to
`push_to_device_create_info`; do not substitute `adapter.features`, which represents queried
support. Keep the change in `renderer_device`, with `renderer_resources` consuming the accessor
through its existing re-exports and no new dependency edge.
Tests: device-level accessor reflects the values supplied for logical-device creation and remains
distinct from adapter support data.
Accept: `cargo test -p renderer_device`; `cargo check -p renderer_device`.
### Task 2 — Foundation (`renderer_resources`, no graph changes; depends on 1)
Retain layout binding descriptions; add normalized image-view range accessor; Arc-backed
binding entry types for the supported descriptor set; explicit unsupported-type and
swapchain-image errors; descriptor-indexing feature checks using Task 1's enabled-feature
accessor (both layout binding flags and type-specific descriptor features); and the reusable
graph-agnostic validation/write-plan interface.
Tests: binding lookup, range/type/count validation, partial binding, array element replacement,
dynamic binding metadata, sampler-only and texel-buffer owner retention, unsupported type/feature
rejection (including enabled-versus-supported feature cases), swapchain-backed image rejection,
and validation-plan errors.
Accept: `cargo test -p renderer_resources`; `cargo check -p renderer_resources`.
### Task 3 — Managed allocation / writer / reset (depends on 2)
Managed allocation, owned write updates using the shared plan, `allocate_untracked` naming, and
caller-synchronized managed-pool reset/destruction and owner replacement contract. Implement the
pool generation counter: capture it at managed allocation, increment only after successful reset,
and reject every operation on surviving stale wrappers, including raw access, writes, import, and
bind. Reset should be unsafe or clearly documented as requiring GPU completion and exclusion of
concurrent use; wrapper-drop alone does not unlock reset.
Tests: owners remain alive after caller drops Arcs; invalid writes leave old entries intact;
reset invalidates a still-live managed wrapper and subsequent raw access/write returns a stale-set
error; failed reset does not advance the generation; reset contract is tested at the API boundary;
tests verify generation checks reject already-stale wrappers but do not treat those checks as
concurrent-use protection; reset's external-synchronization requirement covers allocation, writer
updates, graph recording/import, and command-buffer recording/binding; no test treats wrapper drop
as proof of GPU retirement.
Accept: `cargo test -p renderer_resources`; `cargo check --workspace`.
### Task 4 — Graph transitive import (`renderer_rendergraph`, depends on 3)
Implement identity index and state-validation behavior in both `import_buffer` and
`import_texture` (replacing their current unconditional append behavior) and route descriptor
imports through those methods. Keep caller-supplied initial state separate from descriptor
draw-access scopes; define explicit per-resource initial state input and conflict errors.
Implement metadata-to-draw-scope mapping, layout conflict behavior, managed set retention,
dynamic-offset storage and ordered draw binding. Revalidate the managed-set generation at graph
import and immediately before command binding, returning an error for a wrapper already stale at
the check. This is defense-in-depth only: callers must externally synchronize reset against graph
recording/import and command-buffer recording/binding because revalidation cannot close a
concurrent reset check/use race.
`Draw::apply` must pass the combined offsets in
Vulkan-required order to `cmd_bind_descriptor_sets` rather than an empty slice.
Tests: direct+transitive dedupe in both import orders; duplicate direct imports with matching and
conflicting initial states; descriptor import initial-state conflicts; descriptor draw scopes do
not alter initial barrier sources; buffer/image access merge, incompatible overlapping texture
draw-layout error, sampled/storage mappings and ranges; an already-stale managed set is rejected at
import or bind after reset (not a claim that generation revalidation makes concurrent reset/use
safe); multiple dynamic bindings across
multiple sets, offset count/order, alignment, overflow/effective-range bounds; raw tracking
behavior.
Accept: `cargo test -p renderer_rendergraph`; `cargo check --workspace`.
### Task 5 — Documentation + egui migration (separate `renderer` change, depends on 3)
Revise resource invariants in `crates/renderer_resources/AGENTS.md` (reverse the current
raw-only statement). Decide public re-exports (egui can use managed set types internally without
public re-exports; `renderer::{buffers,images,pipeline}` are crate-private aliases,
`renderer/src/lib.rs:23`). Migrate egui's set creation/storage (`renderer/src/lib.rs:108-115,
155-210,337`), update and bind flow (`egui.rs:561-611,621-630,742-746`), preserving its sparse
update-after-bind array and uniform/storage buffer binding. Egui uses the legacy graph; its
manual texture-read declarations (`egui.rs:621-630`) remain necessary because Task 4 changes
only `renderer_rendergraph`, not the legacy graph. Its swapchain/external lifetime and submission
ordering must continue to satisfy the caller-synchronized contract.
Accept: `cargo check -p renderer`; `cargo check --workspace`.
## Risks / open questions
- `Image::create_view` returns `ManuallyDrop<ImageView>`; callers must deliberately transfer
it into an `Arc` (`images.rs:1001-1008`).
- Swapchain-backed images are deliberately rejected in managed image writes because `Image`
wrappers do not own swapchain image handles, and `ImageView` retains only `Arc<Image>`
(`renderer_resources/AGENTS.md:39-47`). The untracked path remains caller responsibility.
- GPU retirement is not mechanically tracked in this skeleton: managed owner replacement/drop,
set/pool destruction, and pool reset require caller synchronization with completed submissions.
Reset must additionally be externally synchronized against all managed-set use (allocation,
writer updates, graph recording/import, and command-buffer recording/binding), consistent with
Vulkan's own external-synchronization model. Dropping wrappers or taking the pool CPU mutex is
insufficient. Retirement tokens or a GPU-usage tracking hook are the explicit future path.
Separately, successful pool reset advances the pool generation, making every surviving managed
wrapper detectably stale; generation checks are defense-in-depth and do not replace the
GPU-completion/exclusion contract or close a concurrent check/use race. Escaped raw handles remain
the caller's validity responsibility.
- Supported managed descriptor types and rejected types are enumerated above; feature checks
must use the retained enabled-feature snapshot from `renderer_device` and cover
descriptor-indexing binding flags and type-specific requirements, not adapter support queries or
layout flags alone. Never silently omit bound resources from graph hazards.
- A raw Vulkan set or external `update_descriptor_sets` call bypasses tracking; the API must
distinguish "tracked through this wrapper" from merely possessing a raw set.
- Dynamic offsets are bound per draw, validated against binding order/alignment/effective
buffer bounds, and included in draw scopes; they are not implicit descriptor-set state.

View file

@ -0,0 +1,326 @@
# Migration Plan: gpu-allocator → VMA via vk-mem
Status: proposed — rev 2 (Luna review fixes applied)
Date: 2026-10-26
Scope: workspace-wide memory-allocator swap
## Goal
Replace the `gpu-allocator` crate with VMA (Vulkan Memory Allocator) via the
`vk-mem` bindings crate, preserving current allocation behavior (buffer/image
create/bind/destroy, mapping, dedicated allocations) and the existing real-Vulkan
tests.
## Architecture decision
### Requirements
- Hard: remove `gpu-allocator`; use `vk-mem`/VMA; preserve the dependency
direction `bevy_vulkan_render` → `renderer` → `renderer_resources` →
`renderer_device`; keep buffer/image create/bind/destroy and mapping behavior;
keep real-Vulkan tests working.
- Nice: dedicated allocations, allocation naming/stats, defragmentation (not
required now).
### Constraints
- Nightly toolchain, edition 2024.
- `renderer_resources` must not depend on `renderer`.
- `renderer_device` owns device/allocator state.
- `renderer_resources`/`renderer` should not expose `vk_mem` types.
- The `testing` feature gates real-device tests.
- No per-frame allocation is acceptable on current paths.
- `renderer_rendergraph` is also a workspace member and directly uses
`gpu_allocator::MemoryLocation`, so it must be included (it is not mentioned in
the workspace guide).
### Current architecture
- Deps: root `Cargo.toml:45` git `gpu-allocator` (line 44 has a commented
`# vk-mem = "0.5.0"`); `renderer_device/Cargo.toml:12`,
`renderer_resources/Cargo.toml:12`, `renderer/Cargo.toml:27`,
`renderer_rendergraph/Cargo.toml:8`. `Cargo.lock:3423-3435` is the git package;
`Cargo.lock:3409-3420` is the registry copy pulled by `wgpu-hal` via the Bevy
dev-dependency.
- Allocator state: `crates/renderer_device/src/device.rs:151`
`pub alloc2: Mutex<gpu_allocator::vulkan::Allocator>`, initialized at
`:441-456` (`AllocatorCreateDesc`, `AllocationSizes::new(8MB,64MB)` with 256MB
maxima). `GpuAllocation` is the private alias at `:111`; `AllocationStrategy`
at `:114-124`; `Allocation::{Owned,Shared,Unmanaged}` at `:126-147`. Free path
is `ExternallyManagedObject<Device> for GpuAllocation` at `:1074-1077`, which
calls `alloc2.lock().free(...)`.
- Errors: `crates/renderer_device/src/lib.rs:81`
`GpuAllocatorError(#[from] gpu_allocator::AllocationError)`.
- Buffer flow: `crates/renderer_resources/src/buffers.rs:4` imports
`AllocationScheme`; `:20,68` `MemoryLocation`; `:108-122` allocates from
`alloc2`; `:128-130` binds with raw `vkBindBufferMemory`; `:137` wraps in
`Allocation::Owned(DeviceObject<GpuAllocation>)`; `:164-181` maps via
`mapped_slice`/`mapped_slice_mut`.
- Image flow: `crates/renderer_resources/src/images.rs:16` imports VMA-named
`AllocationCreateDesc/Scheme`; `:35,126,289` `MemoryLocation`; `:191-205`
allocates and wraps; `:217-223` binds; `:244-260` validates size/memory-type
for `new_with_allocation`; `:418-428` exposes allocation.
- Dedicated logic: `buffers.rs:116-121` uses
`AllocationScheme::DedicatedBuffer(buffer)`; `images.rs:196-199` uses
`DedicatedImage(image)`; `renderer_rendergraph/src/graph.rs:1352-1357` chooses
dedicated at 64 MiB.
- Direct test allocations: `crates/renderer_device/src/testing.rs:312-319` and
`:351-357` allocate/free directly through `device.alloc2`.
- Other type consumers: `renderer/src/rendering/mod.rs:8,56,77,87,97`;
`renderer/src/egui.rs:8,74-75,140,157,510,531,538,545,555`;
`renderer/src/render_graph/graph_builder.rs:51,60,69,87,168-183`;
`renderer_rendergraph/src/resources.rs:16,27,46,69`; tests
`renderer_resources/tests/managed_set_validation.rs:26` and
`renderer_rendergraph/tests/graph_validation.rs:31`.
- No stats/reporting or defragmentation call sites exist today. `Buffer`/`Image`
drop field order: buffer/image handle first, then allocation
(`buffers.rs:74-78`, `images.rs:144-148`).
### Alternatives considered
1. **Swap allocator inside `renderer_device`, add a private wrapper and a
workspace-owned `MemoryLocation` (chosen)**: least invasive, upper crates stay
VMA-free.
2. Re-export `vk_mem` types as public resource API: least code, but leaks VMA
into `renderer_resources`/`renderer` and locks the workspace API to `vk-mem`.
3. Use `vk_mem::create_buffer/create_image` directly in `renderer_resources`:
fewer Vulkan calls and idiomatic VMA, but larger ownership refactor and
couples the resource crate to `vk_mem`.
4. Add a memory-backend trait: easy A/B testing, but unnecessary indirection and
lifetime complexity for a one-time swap.
### Decision rationale (feasibility verified)
- `vk-mem 0.5.0` bundles **VMA 3.3.0** and depends on **ash 0.38** (workspace ash
is `0.38.0+1.3.281`), so there is one ash and no duplicate-ash problem.
- `vk_mem::Allocator` is `Send + Sync` and internally synchronized unless
`EXTERNALLY_SYNCHRONIZED` is set; keep the `Mutex`.
- `vk_mem::Allocation` is `Copy, Send, Sync` and has no inherent
`size/memory/offset` accessors, so the wrapper must retain `AllocationInfo`.
## Target design
- `renderer_device::device::MemoryLocation { Unknown, GpuOnly, CpuToGpu,
GpuToCpu }` with `Default = Unknown`, `Copy/Eq/Hash`; use it as the public
`mem_location` type.
- `pub struct GpuAllocation { raw: vk_mem::Allocation, memory: vk::DeviceMemory,
offset: u64, size: u64, memory_type_index: u32, mapped: Option<SendSyncPtr> }`.
`SendSyncPtr` is a private `NonNull<u8>` wrapper with unsafe `Send/Sync`,
mirroring `gpu_allocator`'s pointer wrapper. Implement `Debug`,
`memory()/offset()/size()/memory_type_index()/mapped_slice()/mapped_slice_mut()`.
- `pub struct AllocationDesc<'a> { name: &'a str, location: MemoryLocation,
strategy: AllocationStrategy }` and
`DeviceInner::{allocate_for_buffer, allocate_for_image, bind_buffer_memory,
bind_image_memory}`. Note the call shapes: `allocate_memory_for_buffer/image`
live on the `vk_mem::Alloc` trait (import `vk_mem::Alloc as _`) and are
`unsafe` in `vk-mem 0.5.0`; `bind_buffer_memory`/`bind_image_memory` are
inherent (also `unsafe`) `Allocator` methods, not trait methods.
- `DeviceInner.alloc2: Mutex<vk_mem::Allocator>` remains the single allocator
owner. Keep `Allocation::{Owned(DeviceObject<GpuAllocation>),
Shared(Arc<...>), Unmanaged}` unchanged in shape.
- `renderer_resources`/`renderer`/`renderer_rendergraph` use only
`renderer_device::device::MemoryLocation`, `Allocation`, and the new `Device`
helpers; no `vk_mem` imports outside `renderer_device`.
Suggested interface:
```rust
impl DeviceInner {
pub fn allocate_for_buffer(&self, buffer: vk::Buffer, desc: AllocationDesc<'_>) -> Result<GpuAllocation>;
pub fn allocate_for_image(&self, image: vk::Image, desc: AllocationDesc<'_>) -> Result<GpuAllocation>;
pub fn bind_buffer_memory(&self, alloc: &GpuAllocation, buffer: vk::Buffer) -> Result<()>;
pub fn bind_image_memory(&self, alloc: &GpuAllocation, image: vk::Image) -> Result<()>;
}
```
## Dependency changes
- Root `Cargo.toml`: add `vk-mem = "0.5.0"` to `[workspace.dependencies]` (the
commented line at `:44` anticipates this); remove the git `gpu-allocator` entry
in phase 3.
- `renderer_device/Cargo.toml`: add `vk-mem = { workspace = true }`; remove
`gpu-allocator` in phase 2 (after the swap compiles).
- `renderer_resources/Cargo.toml`, `renderer/Cargo.toml`,
`renderer_rendergraph/Cargo.toml`: remove `gpu-allocator` in phase 3.
- Ash alignment: `vk-mem 0.5.0` pins ash 0.38; an ash upgrade requires a
matching `vk-mem` release or fork.
## API mapping
| gpu-allocator | VMA / `vk-mem` |
|---|---|
| `AllocatorCreateDesc` instance/device/physical_device | `AllocatorCreateInfo::new(&instance, &device, physical_device)` |
| `debug_settings`, allocation sizes | VMA compile-time debug macros; `preferred_large_heap_block_size` only (loss of min/max block tuning) |
| `buffer_device_address` | `AllocatorCreateFlags::BUFFER_DEVICE_ADDRESS` (off here) |
| `MemoryLocation::{Unknown,GpuOnly,CpuToGpu,GpuToCpu}` | `MemoryUsage::{Unknown,GpuOnly,CpuToGpu,GpuToCpu}` |
| `AllocationScheme::GpuAllocatorManaged` | default allocation (`AllocationCreateFlags::empty()`) |
| `DedicatedBuffer(buffer)` / `DedicatedImage(image)` | `DEDICATED_MEMORY` + `allocate_memory_for_buffer/image` (passes `VkMemoryDedicatedAllocateInfo` when API version ≥ 1.1) |
| `AllocationCreateDesc.linear` | implicit: use `allocate_memory_for_buffer/image`, not generic allocation |
| `Allocation.memory()/offset()/size()/memory_type_index()` | `AllocationInfo.device_memory/offset/size/memory_type` captured at allocation |
| `mapped_slice()/mapped_slice_mut()` | `AllocationInfo.mapped_data` from `MAPPED` allocation, or `Allocator::map_memory` |
| `Allocator::free(allocation)` | `Allocator::free_memory(&mut allocation)` |
| `bind_buffer_memory`/`bind_image_memory` | `Allocator::{bind_buffer_memory, bind_image_memory}` |
| stats report | `calculate_statistics()` / `get_heap_budgets()`; no allocation names in `vk-mem 0.5.0` |
| debug names | unchanged: `DeviceObject::new_debug_named` sets Vulkan object names; VMA allocation names are not exposed |
## Migration phases
### Phase 0 — baseline
Run `cargo check --workspace`, `cargo test -p renderer_device --features
testing`, inspect `git status`. No edits.
### Phase 1 — dependency/type scaffolding (non-breaking)
Add `vk-mem = "0.5.0"` to root `[workspace.dependencies]`; add
`vk-mem = { workspace = true }` to `renderer_device/Cargo.toml`; add
`MemoryLocation`, `GpuAllocation` skeleton, and `AllocationDesc` in
`crates/renderer_device/src/device.rs` while leaving the old allocator
untouched.
Verify: `cargo check -p renderer_device`.
### Phase 2 — atomic allocator swap (one compile unit)
In `crates/renderer_device/src/device.rs` replace `alloc2`/creation/
`GpuAllocation` alias/free impl with VMA; remove `GpuAllocatorError` from
`crates/renderer_device/src/lib.rs:81`; rewrite `testing.rs:312-319,351-357`;
in `crates/renderer_resources/src/buffers.rs` and `images.rs` replace
`alloc2.allocate` + raw bind with `allocate_for_buffer/image` + `bind_*`, adding
handle guards in the same pass (raw handle wrapped in `DeviceObject` immediately
after creation, allocation ownership established before binding — see
implementation notes), but temporarily keep the public fields as
`gpu_allocator::MemoryLocation` and convert at the call site.
Verify: `cargo check --workspace` and `cargo test -p renderer_device --features
testing`.
### Phase 3 — public type/dependency cleanup
Change `BufferDesc.mem_location`/`ImageDesc.mem_location` to
`renderer_device::device::MemoryLocation`; update `renderer/src/rendering/mod.rs`,
`renderer/src/egui.rs`, `renderer/src/render_graph/graph_builder.rs`,
`renderer_rendergraph/src/resources.rs`, and both tests
(`renderer_resources/tests/managed_set_validation.rs`,
`renderer_rendergraph/tests/graph_validation.rs`); remove `gpu-allocator` from
`renderer_resources`, `renderer`, `renderer_rendergraph`, and root manifests.
Verify: `cargo check --workspace`; `rg -n 'gpu_allocator|gpu-allocator' --glob
'!Cargo.lock'` should be empty.
### Phase 4 — hardening/docs
- Ensure `GpuAllocation` is `Send + Sync`.
- Add handle guards: wrap raw buffer/image handles in `DeviceObject` right after
creation and establish allocation ownership before binding, so allocation and
bind failures never leak (see implementation notes).
- Add a validation test for `CpuToGpu` mapping that asserts the allocated memory
type is `HOST_VISIBLE | HOST_COHERENT` and round-trips a CPU write through
`map_mut`, plus dedicated buffers/images.
- Update `crates/renderer_device/AGENTS.md`,
`crates/renderer_resources/AGENTS.md`, `crates/renderer/AGENTS.md`, and the
root workspace guide to say VMA/`vk-mem`.
- Optionally rename `alloc2` to `allocator`.
Verify: full checklist below.
## Implementation notes
- Allocator creation: `unsafe { vk_mem::Allocator::new(create_info) }?`; set
`preferred_large_heap_block_size` to ~64 MiB (VMA's default is 256 MiB).
`vulkan_api_version` must not exceed the instance's requested version or the
selected physical device's supported version: record the instance request
(add `pub api_version: u32` to `InstanceInner` when building `ApplicationInfo`
in `instance.rs:126-135`) and clamp it with the physical device's
`limits.api_version` at device selection, passing the effective minimum (a
safe floor is 1.1 when both sides support it, else 1.0). Dedicated-allocation
structs require API ≥ 1.1; on Vulkan 1.0 devices require
`VK_KHR_dedicated_allocation` (+ `VK_KHR_get_memory_requirements2`) at device
creation or skip `DEDICATED_MEMORY` there.
- Do not enable `EXTERNALLY_SYNCHRONIZED`; keep the `Mutex`.
- Allocation flags: `MAPPED` for `CpuToGpu`/`GpuToCpu`, plus `DEDICATED_MEMORY`
for `AllocationStrategy::Dedicated`.
- **Coherence policy (required)**: VMA's `CpuToGpu`/`GpuToCpu` usages guarantee
host-visible memory, not host-coherent memory, but the current mapping API
(`mapped_slice`/`mapped_slice_mut`) has no flush/invalidate step. Require
coherent memory: compute the memory-type bitmask of types with
`HOST_VISIBLE | HOST_COHERENT` from the physical device's memory properties
and set `AllocationCreateInfo.memory_type_bits` for CPU-mapped allocations;
fail the allocation if no such type exists (stricter than gpu-allocator,
which silently falls back to non-coherent types). If a non-coherent-only
device must be supported later, switch to explicit
`Allocator::flush_allocation`/`invalidate_allocation` around map windows and
change the API contract accordingly — do not ship a silent fallback.
- **Handle guards before allocation**: the buffer/image flows create raw Vulkan
handles before allocating (`buffers.rs:108-130`, `images.rs:191-223`); wrap
each raw handle in `DeviceObject` immediately after creation and establish
`Allocation` ownership (`DeviceObject<GpuAllocation>`) before binding, so
every failure path — allocation failure or bind failure — releases the handle
and the allocation in the correct order (destroy handle first, then
`free_memory`). No raw handle may escape a fallible span unowned.
- Free with `free_memory` after the buffer/image handle is destroyed; do not
also call `destroy_buffer/destroy_image` for the same allocation.
## Validation checklist
- `cargo check -p renderer_device`, `-p renderer_resources`, `-p renderer`,
`-p renderer_rendergraph`, then `cargo check --workspace`.
- `cargo test -p renderer_device` and `cargo test -p renderer_device --features
testing` (real Vulkan/validation; skips if unavailable).
- `cargo test -p renderer_resources` and `cargo test -p renderer_rendergraph`
(both exercise `TestVulkan` through their dev-dependency features).
- `cargo tree -d | rg '^ash'` should show one ash. The lockfile has two
`gpu-allocator 0.28.0` packages (registry + git), so `cargo tree -i
gpu-allocator` is ambiguous; use source-qualified package IDs:
`cargo tree -i 'registry+https://github.com/rust-lang/crates.io-index#gpu-allocator@0.28.0'`
should list only `wgpu-hal` via the Bevy dev-dependency, and
`cargo tree -i 'https://github.com/janis-bhm/gpu-allocator#gpu-allocator@0.28.0'`
should be empty after phase 3.
`rg -n 'gpu_allocator|gpu-allocator' --glob '!Cargo.lock'` should return
nothing.
- Re-run the baseline Vulkan smoke tests and, if a display is available,
`cargo build --examples`.
## Risks & gotchas
- **FFI/lifetimes**: `vk_mem::Allocator` is a raw-pointer owner; it must outlive
every `Allocation`. The existing `DeviceInner` field order (`alloc2` before
`_drop`) is correct; do not move the allocator after `_drop`. `GpuAllocation`
must not be `Copy` and must free exactly once through
`DeviceObject`/`ExternallyManagedObject`.
- **Thread safety**: `AllocationInfo.mapped_data` is `*mut c_void`, which is not
`Send/Sync`; storing it directly breaks `Arc<Buffer>` thread-safety. Use a
private `SendSyncPtr` or unsafe `Send/Sync` wrapper and add a compile-time
`assert_send_sync::<Buffer>()`/`Image` test.
- **Mapping differs**: `gpu-allocator` mapped host-visible blocks
opportunistically; VMA maps only with `MAPPED`. `CpuToGpu`/`GpuToCpu` keep
working; `GpuOnly` mapping on integrated GPUs may now return `None`.
**Coherence**: VMA only guarantees host-visible (not host-coherent) memory
for CPU usages; this plan requires coherent types via `memory_type_bits`
(see implementation notes) so the existing flush-free `map`/`map_mut` usage
stays correct. Non-coherent support would need explicit
`flush_allocation`/`invalidate_allocation`; current code does not
flush/invalidate either.
- **Dedicated allocation**: use `allocate_memory_for_buffer/image` with
`DEDICATED_MEMORY`; generic `allocate_memory` would create a separate block
without dedicated buffer/image info. `VkMemoryDedicatedAllocateInfo` is only
emitted when the runtime API version is ≥ 1.1 (or the KHR dedicated
extensions are enabled); pass the clamped effective version per the
implementation notes and require the extensions on Vulkan 1.0 devices.
- **OOM handling**: `gpu_allocator::AllocationError` variants disappear; VMA
returns `vk::Result` (`ERROR_OUT_OF_DEVICE_MEMORY`,
`ERROR_FEATURE_NOT_PRESENT`, etc.), already handled by `Error::Result`. No
call site pattern-matches the old variant.
- **Build environment**: `vk-mem 0.5.0` compiles VMA C++ (`wrapper.cpp`, C++17,
links stdc++ on Linux); ensure CI has a C++ compiler. It pins ash 0.38; an ash
upgrade requires a matching `vk-mem` release/fork. The crate is less actively
maintained than `gpu-allocator`.
- **Lost features**: no `vk_mem` allocation naming setter
(`vmaSetAllocationName`) and no `build_stats_string` in 0.5.0; VMA
stats/heap budgets remain available. Defragmentation is intentionally out of
scope; if enabled later, cached `memory/offset/mapped_data` in the wrapper
must be re-queried.
- **Pre-existing hazard**: `Image::new_with_allocation` still creates the raw
image before validation (`images.rs:235-260`); the handle-guard requirement
(implementation notes) covers this by owning the raw image in `DeviceObject`
from creation onward.

View file

@ -0,0 +1,575 @@
# Plan: Render-Pass Support (`BeginRendering` / `EndRendering`) in `renderer_rendergraph`
Status: proposed — rev 5 (rev-1 correctness, rev-2 performance, rev-3 adversarial, and rev-4 re-review applied; rev-4 verdict was REJECT, findings resolved here)
Date: 2026-10-27
Scope: `crates/renderer_rendergraph`
## Goal
Add dynamic-rendering render-pass support to the rendergraph: `BeginRendering`
and `EndRendering` commands that bracket a span of graph commands, with the
compiler guaranteeing that every command recorded between the two is emitted
inside the rendering instance — i.e. commands that must be in a rendergraph
pass are ordered by the begin/end rendering commands, and no foreign command
may be scheduled between them.
## Current state (verified against code)
- Commands implement `Command` (declares `side_effects` → `SubresourceScope`
accesses) + `RecordedCommand` (`preflight`/`apply`), and are pushed via
`RenderGraph::push` into `CommandList` (`commands.rs`, `recorder.rs`).
- `GraphCompiler::dependencies()` (`graph.rs:820-863`) builds hazard edges
purely from resource conflicts; `topo_batches_and_dependencies()`
(`graph.rs:870-889`) Kahn-sorts commands into `batches`;
`record_commands` (`graph.rs:2500-2554`) flattens batches, emitting barriers
(keyed `before_command` via `grouped_barrier_indices`) immediately before
each command.
- There is no notion of a render pass. Nothing prevents the flattener from
emitting an unrelated command between two commands the user recorded
adjacently: Kahn batching interleaves hazard-free commands freely.
- `CommandRecorder` (`recorder.rs:159-164`) maps `ResourceId → vk::Buffer` /
`vk::Image` only; there is no image-view plumbing. Descriptor imports read
`Arc<ImageView>` bindings at import time (`graph.rs:507-513`) but retain
only the underlying image (`StagedImportResource::Texture(view.image().clone())`,
`graph.rs:524-532`) — attachment views are new plumbing, not an existing
pattern to reuse.
- `CommandList` stores type-erased `Box<dyn RecordedCommand>` (`recorder.rs:97-109`);
the `Command` trait exposes only `side_effects` (`commands.rs:14-16`), so
there is currently no way to classify a command or introspect its render-pass
descriptor.
- The graph documents a single-queue submission contract (`graph.rs:8-10`);
ordinary barriers set `queue_transfer: None` (`graph.rs:972-982, 1001-1012`).
- Compile-time cost profile (recomputed independently): `dependencies()` is
`O(Σ_r u_r²)` pair checks (plus a per-pair scope cross-product factor);
`HappensBefore::from_dag` allocates one `FixedBitSet` row per node
(`graph.rs:213-217`, `O(N²/64)` memory) and unions per edge
(`O(E·N/64)` time). `ResourceInterference::from_resource_uses` **already**
computes terminal/initial frontier sets once per resource
(`graph.rs:1348-1356`); the remaining recomputations are in
`first_fit_placement` (`graph.rs:1642-1649`), the `overlapping` loop in
`plan_placements` (`graph.rs:2384-2399`), and `build_alias_barriers`
(`graph.rs:1088, 1109-1117`). Contraction does not remove those; see D8.
- The crate is not yet referenced by `renderer`/`bevy_vulkan_render`; the
current per-frame path is the legacy `renderer::render_graph`. If this
crate replaces it, `compile()` becomes per-frame, so compile cost matters.
- `dynamic_rendering` and `synchronization2` are requested/enabled
(`crates/renderer/src/lib.rs:352-357`, `crates/renderer_device/src/testing.rs:166-172`).
- **Pre-existing defect (reproduced):** `build_barriers` records only the last
*conflicting* producer's layout (`graph.rs:951-982`) and never chains
`old_layout` through earlier barriers. Two same-layout readers of one
producer yield two image barriers both claiming `oldLayout = producerLayout`;
the second is stale (Vulkan requires `oldLayout` be the current layout or
`UNDEFINED`). Fixing this generally (per-subresource current-layout map) is
part of Task 3 — the plan's own tests hit it.
- `Image::create_view` returns `Result<ManuallyDrop<ImageView>>`
(`renderer_resources/src/images.rs:558-621`); storing it directly leaks the
`vk::ImageView` and the `Arc<Image>` (the existing renderer calls `.raw()`
on the `ManuallyDrop`, `crates/renderer/src/rendering/mod.rs:332`, which
leaks today). New code must use `ManuallyDrop::into_inner`.
- `ImageView` holds `Arc<Image>` (`images.rs:1123-1127`), so a view cache
keeps its image alive; drop-order interaction with `CompiledGraph` is benign
provided the `ManuallyDrop` is unwrapped.
## Design decisions
### D1 — Dynamic rendering, not `VkRenderPass`
New commands `BeginRendering` / `EndRendering` wrapping `vkCmdBeginRendering`
/ `vkCmdEndRendering` (Vulkan 1.3 `dynamic_rendering`). Attachments are graph
`Texture` handles plus a view specification (see D9 for the created-texture
problem).
### D2 — The pass span is an atomic scheduling unit, contracted *before* hazard-edge construction
The compiler contracts the `[begin, end]` command span into a single DAG node
("pass node"). This is the mechanism that satisfies the ordering contract:
- No foreign command can ever be scheduled inside the span (Kahn batching
would otherwise interleave hazard-free commands into it).
- The whole span is ordered relative to everything else by the begin/end
hazard edges.
Contraction happens **before** `dependencies()` builds hazard edges. This
removes the `O(Σ u_r²)` interior-draw edge construction (edges that would be
discarded as self-edges anyway) and shrinks the `O(N²/64)` closure memory/time
by roughly `(N_c/N)²`.
The contraction mapping used by every consumer:
- **Type-safe node indexing:** use `NodeId(NonZeroU32)` newtypes and
`Option<NodeId>` rather than raw `u32` sentinels mixed into `CommandIndex`
spaces. To minimize churn, keep `batches: Vec<Vec<CommandIndex>>` with the
`BeginRendering` command index as the documented pass-node representative;
`spans: Vec<Span>` on `CompiledGraph` records `{ begin, end, interior }`
for expansion. Document the semantic change to the public `batches()` /
`dependency_edges()` accessors. Closure rows are indexed `0..N_c`.
- **HappensBefore is span-aware internally:** `HappensBefore` (public,
`lib.rs:10`) stores the command→node map and span slices and translates in
`contains`/`happens_before` (`graph.rs:244-251`), so command-space queries
keep working and interior indices never silently return `false` from a
node-space row lookup.
- **Intra-span record order is NOT a happens-before fact.** Interior commands
map to the pass node and same-node uses are treated as **unordered** by the
closure used for aliasing/interference. This is a memory-correctness
requirement, not a style choice: if intra-span record order were visible to
`resources_alias_in_order`, a created attachment `A` (single use at `Begin`)
and a created storage image `B` used only by an interior command would be
judged ordered, `first_fit_placement` could place `B` over `A`'s memory, and
`build_alias_barriers` would either emit no barrier or an illegal in-pass
`UNDEFINED → layout` transition — discarding a live render target mid-pass.
Intra-span record order is needed only by `build_barriers` (which iterates
execution order directly) and by recording; it must not leak into the
aliasing/interference closure.
- **Attachment liveness is modeled through `End`.** `EndRendering` carries
synthetic accesses (the attachment layout, `COLOR_ATTACHMENT_WRITE` /
`DEPTH_STENCIL_ATTACHMENT_WRITE` at the attachment stage) used **only** for
lifetime/alias analysis — excluded from barrier generation (the begin's
merged scope already covers synchronization) and from usage derivation.
This makes `terminal_uses(attachment) == End`, so an attachment is never
judged dead before the store.
- **Barrier analysis:** `build_barriers` walks scheduled command indices and
reads only `accesses[command_index]` (`graph.rs:930-934`). Barrier analysis
runs over the *uncontracted* access list in **execution order** (see D6),
with each interior command's scheduling position mapped to the pass node.
This preserves, e.g., the barrier for an outside producer consumed only by
an interior `Draw`.
- **Lifetimes:** `bind_lifetimes` indexes `batch_for_command` for every
original access (`graph.rs:1258-1275`); interior commands map to the pass
node's batch index so they are never left at `usize::MAX`. Placement uses
command-level interference, not batch intervals, so placement quality is
unaffected; external consumers of `resource_lifetimes()` get coarser
intervals for interior commands (documented).
- **Self-edges:** intra-span hazard pairs are skipped during edge
construction (they become intra-span execution-order constraints handled by
`build_barriers`, not DAG edges). Note `dependencies()` only adds
prior→current edges (`graph.rs:825-861`), so the DAG is acyclic regardless;
only a literal self-edge would trip `Kahn`, and skipping intra-span pairs
prevents exactly that.
### D3 — Access declaration: one merged scope per attachment; interior accesses preserved
- `BeginRendering` declares **one access per attachment** with
`access = COLOR_ATTACHMENT_WRITE | (LOAD ? COLOR_ATTACHMENT_READ : 0)` at
the same stage (`COLOR_ATTACHMENT_OUTPUT` / `EARLY|LATE_FRAGMENT_TESTS`),
subresource range from the view (concrete range rules in D9). Declaring
read and write as two separate accesses is a blocker:
`SideEffectMap::insert` deliberately keeps opposite-polarity scopes separate
(`recorder.rs:55-82`), and `build_barriers` then emits **two**
`ImageMemoryBarrier2` entries for the same image/range/layout in one
`vkCmdPipelineBarrier2` (reproduced). A single merged scope is handled
correctly by `SideEffectMap`, `conflicts`, `derive_image_usage`
(`graph.rs:2696-2709`), and the barrier builder.
- `EndRendering`'s synthetic accesses (D2) are tagged so barrier generation
and usage derivation skip them.
- Interior command accesses are *not* replaced by the begin declarations;
they remain in the access list (see D2 barrier analysis) so the compiler
can emit barriers for outside producers consumed inside the pass.
- Because the span is contracted, any later consumer is ordered after the
*entire pass node*, i.e. after `EndRendering` — matching store-visibility
semantics (verified: the consumer's barrier takes
`COLOR_ATTACHMENT_OUTPUT`/`COLOR_ATTACHMENT_WRITE` as src, and hoisting a
barrier before `BeginRendering` with those dst stages is sufficient
visibility for LOAD reads and in-pass attachment reads).
- **Intra-span hazard policy (narrowed):**
- **Allowed inside a span:** any barrier with `old_layout == new_layout` —
memory barriers, buffer barriers, *and* image barriers with no layout
change (a storage-image read/write hazard at `GENERAL` needs an
`ImageMemoryBarrier2`; buffer barriers do not synchronize image
accesses). These are legal inside a rendering instance.
- **Rejected at compile time** (`GraphError::UnsupportedIntraPassDependency`):
intra-span hazards requiring an image layout transition (`old != new`).
The rejection operates on the **generated barrier plan** (D5), not on an
independent declared-layout scan.
- **Feedback loops:** defined as any interior access (read *or* write)
whose `(aspect, mip, layer)` set intersects a `Begin` attachment
subresource, excluding the begin's own load operation. A storage-image
write to an attachment subresource is equally a feedback loop; an
interior read of a disjoint mip/layer is legal. Genuine feedback loops
require splitting.
- In-pass barrier emission is part of Task 4, not a follow-up: the recorder
emits the barrier between the two interior commands in execution order.
- **Aliasing vs. interior first uses is a placement constraint, not a
validation message.** For image resources, add an interference edge (or an
explicit `aliasing_allowed` predicate consulted by `plan_placements` /
`first_fit_placement`) whenever a required alias transition would land at a
command that maps to a pass node — covering interior *and* boundary
(`Begin`/`End`) positions. There is no "reject aliasing" hook after
`plan_placements` commits offsets, so this must be enforced before
placement. Attachments themselves are safe: with D2's `End` modeling,
their first use is `Begin` and their terminal use is `End`, both boundary
commands of the pass node itself.
### D4 — Barrier placement: hoist cross-boundary barriers to BeginRendering, with per-subresource layout handling
Barriers whose `before_command` targets an interior command but whose origin
lies outside the span are re-keyed to the span's `BeginRendering` command,
after batching when spans are known. Constraints:
- **Barrier origin must be explicit.** `BarrierResource` (`graph.rs:84-95`)
records only masks/layout — no source command and no initial-vs-command
origin — so a post-pass hoisting pass cannot distinguish an outside-producer
barrier from an initial-state transition. Add
`src_command: Option<CommandIndex>` or a
`BarrierOrigin::{Initial, Command(CommandIndex)}` to `BarrierResource`.
**Initial-state transitions for interior first uses are hoisted to
`Begin`** and are not covered by the intra-span rejection (a transient
storage image first used inside a pass is the common case, not an edge
case).
- **Coalesce hoisted image transitions per `(resource, exact range,
target_layout)` into one barrier** with a unioned `dst_stage/dst_access`
(safe: the dst scope is a wait target; unioning only makes all consumers
wait). Layout chaining is defined **per `(aspect, mip, layer)`**, not per
barrier: for overlapping-but-unequal ranges, transition only the
subresources still in the producer layout; for disjoint ranges, emit
independent `producer → layout` transitions. The earlier "chain different
target layouts" rule is removed: for same-pass overlapping ranges it is
unreachable (D3 rejects the required in-pass transition), and for disjoint
ranges chaining `producer → A → B` is simply wrong (the second range is
still in the producer layout).
- The general `build_barriers` fix (per-subresource current-layout map, not
"last conflicting access's layout") makes non-hoisted multi-reader cases
correct too; D4's rules then only govern the hoisted subset.
- After hoisting, rebuild `barrier_indices_by_command` and
`ordinary_barrier_count` (`O(B)`), and apply the same coalescing in
`rebuild_barriers` (`graph.rs:2092-2106`) for alias barriers.
**Sequencing:** hoisting runs before `build_alias_barriers`, and hoisted
barriers live strictly within the ordinary prefix so `rebuild_barriers`'
truncation to `ordinary_barrier_count` (`graph.rs:2098`) cannot drop or
duplicate them; `rebuild_barriers` re-runs hoisting after the alias rebuild.
- Consumers after the pass keep their own barrier keys; expansion emits them
after `EndRendering` naturally. No barrier targets `EndRendering` itself
(its synthetic accesses are excluded from barrier generation).
- In-pass barriers (per D3) are emitted between the two interior commands in
execution order; they are not hoisted.
### D5 — Compile-time validation on the generated barrier plan, all linear
New `GraphError` variants for:
- unbalanced begin/end; nested/overlapping spans (dynamic rendering cannot
nest); `EndRendering` before its `BeginRendering` in record order;
- graphics commands outside any span (`GraphError::DrawOutsideRenderPass`),
and **transfer/clear commands inside a span** (`CopyBuffer`,
`ClearTexture`, and any custom command not marked render-pass-legal —
`vkCmdCopyBuffer2`/`vkCmdClearColorImage` are illegal inside a rendering
instance). Classification uses a `fn kind(&self) -> CommandKind` method on
**`RecordedCommand`** (not `Command` — a method on `Command` is
unreachable after `Box<dyn RecordedCommand>` erasure), returning
`CommandKind::{Begin, End, Graphics, Transfer, Compute, Other}` plus a
`fn allowed_in_render_pass(&self) -> bool` (default false; `Draw` → true;
test/custom no-op commands may override so Task 5's in-pass barrier tests
can exist). **Note:** five existing unit tests compile graphs containing
`Draw` with no pass (pushes at `graph.rs:3563, 3749, 3815, 4329, 4469`)
and must be updated (wrap in begin/end or make the restriction opt-in) —
added to Task 5.
- unsupported intra-span dependencies (per D3). **The check operates on the
generated barrier plan after hoisting/chaining, not on an independent
declared-layout scan:** after hoisting, any barrier whose `before_command`
is interior and whose `old_layout != new_layout` is either hoistable
(initial/outside origin) or must be rejected. This is exact, `O(B)`, and
removes the fragile per-subresource validation machinery whose
`previous`-selection (`graph.rs:951-982`) can diverge from declared
layouts (e.g. producer `P` at layout `P0`, outside reader `R1` transitions
to `A`, interior reader `R2` reads at `A`: a declared-layout scan sees
`A → A` and accepts, while the un-fixed builder would emit `P0 → A` at
`R2` inside the pass).
- attachment consistency: each attachment view must refer to the same image
as its graph `Texture` — compare **raw handles** (`Image: PartialEq`
compares raw handles, `images.rs:484-488`; `Arc::ptr_eq` would
false-positive), consistent with import dedup by raw handle
(`graph.rs:389-399`). Applies to the imported case at compile time and to
the created case after view creation (D9). The recorder binds graph images
by `ResourceId` (`graph.rs:2513-2529`); this check closes that gap.
- attachment layout legality: the caller-specified layout must be a legal
rendering layout (`COLOR_ATTACHMENT_OPTIMAL`, `GENERAL`, etc.). The
previously proposed "layout must match compiler-produced state" rule is
vacuous — the barrier's `new_layout` is derived from the begin's own
declared access, so no independent state exists.
- duplicate attachment subresources: two attachments covering the same
subresource would be silently merged by `SideEffectMap::insert`
(`recorder.rs:55-82`), making them invisible from `command_accesses`;
reject via the introspection hook (below), using a per-subresource set
(`O(covered subresources)`, not pairwise `O(a²)`).
- queue ownership: passes cannot cross queues or require ownership
transfers; the single-queue submission contract (`graph.rs:8-10`) applies
unchanged, and any access requiring a queue transfer inside or across a
span is rejected until paired transfer/barrier support exists.
- **Introspection hook:** commands are type-erased, and `CommandKind` carries
no data, so Task 2's attachment validations (view/image identity, duplicate
subresources, layout legality) need a `fn render_pass_desc(&self) ->
Option<&RenderPassDesc>` method on `RecordedCommand` (or a validation hook
called from `compile()`, or an attachment summary stored in
`CommandList::push`, which is generic over `C: Command`). Without one,
these checks are not implementable as written.
- **Error-path safety:** `record_commands` returning mid-pass must not leave
an unclosed rendering instance; track an `in_render_pass` flag in the
recorder/compiled graph, end the rendering instance (or discard the command
buffer) on the error path, and reset the flag (`graph.rs:2540-2553`).
### D6 — Execution order is precomputed at compile time
Compile an `execution_order: Vec<CommandIndex>` once: expanded contracted
batches — begin → interiors in **original record order** → end. Barrier
analysis and recording both iterate it; `batches` is retained (begin-index
representative) for future parallel recording. This avoids per-frame
expansion branches and keeps `recording_steps_cached` (`graph.rs:2595-2608`)
allocation-free. Interior commands are emitted in record order, not topo
order: ordering within the pass is the user's responsibility, matching the
"ordered by begin/end" contract.
**Batch-index assertion fix:** `build_barriers_with_initial_states` asserts
`batch_indices[previous] < batch_indices[current]` (`graph.rs:962-966`).
With interior commands mapped to the pass node's batch index, an allowed
intra-span hazard gives equal batch indices → debug panic. Use
**execution-order positions** for that assertion, not batch indices.
### D7 — Recording is allocation-free per frame
- `BeginRendering::apply` precomputes `vk::RenderingAttachmentInfo`s in the
command (or uses `ArrayVec`/thread-local scratch, mirroring
`pipeline_barrier`'s `BARRIER_SCRATCH` pattern, `recorder.rs:217-292`) so
no `Vec` is built per frame. Resolving attachment images per frame is
`O(a log R)` `BTreeMap` lookups — negligible.
- **View plumbing into `apply`:** `RecordedCommand::apply` receives only
`CommandRecorder` (`recorder.rs:108`), which cannot see `CompiledGraph`'s
view cache. Extend `CommandRecorder` with a view map
(`ResourceId → Arc<ImageView>`, or `ResourceId → Arc<Image>` plus
command-local view cache populated via `&mut self`) populated by
`record_commands`. `BeginRendering::apply` looks up the view; a missing or
unresolved view is a new `RecordError::MissingView`. Views are created
with `ManuallyDrop::into_inner(create_view(...))` — never store the
`ManuallyDrop` directly (leak).
- **Resolve idempotence:** views are created only after a fully successful
placement pass, atomically; a failed view creation must not leave a
partially-populated cache that later makes `Arc::get_mut`
(`graph.rs:2472-2480`) fail on a retried placement. Repeated `resolve()`
calls are idempotent; `create_view` failures map to `GraphError` variants.
### D8 — Frontier reuse for placement and alias barriers (performance amendment, corrected)
`ResourceInterference::from_resource_uses` already caches frontier sets per
resource (`graph.rs:1348-1356`); the plan does not claim to change it. The
actual recomputation hot spots contraction does not touch are
`first_fit_placement` (`graph.rs:1642-1649`), the `overlapping` loop in
`plan_placements` (`graph.rs:2384-2399`), and `build_alias_barriers`
(`terminal_uses` per old resource per new resource at `graph.rs:1109-1117`;
`initial_uses` per new at `graph.rs:1088`). Compute terminal/initial
frontiers **once per resource** and reuse them across all three sites, so
the performance rationale is honest: contraction removes the dependency-edge
and closure quadratics; frontier reuse removes the placement/alias ones.
### D9 — Attachment API for created textures
`RenderGraph::create_texture` returns only a `Texture` id (`graph.rs:352-354`);
the `Arc<Image>` is created privately in `plan_placements`/`resolve`
(`graph.rs:2129-2495`) and `CompiledGraph` exposes no accessor, so a caller
can never supply an `Arc<ImageView>` for a transient render target — the
main use case. Resolution:
- `ColorAttachment.view` is an enum:
`AttachmentView::Created(ImageViewDesc) | Imported(Arc<ImageView>)`.
- **`Created(ImageViewDesc)` must carry concrete aspect/mip/layer ranges**
(`REMAINING_*` sentinels are rejected at validation): `side_effects` runs
at `push` time (`recorder.rs:119-124`), before `resolve()` creates any
image, `image_view_subresource_range` is private (`images.rs:1183`), and
`create_view` normalizes/mutates the desc (`set_max_end`, format fill-in,
`images.rs:566-589`; `MipRange` addition panics on overflow in debug for
`base > 0` with a `REMAINING` count, `images.rs:1319-1333`). The declared
access range therefore comes from the desc, and is validated against the
image's mip/layer counts at compile time (from `GraphTextureDesc` /
imported `ImageDesc`).
- The crate creates and caches views after `resolve()` (`Image::create_view`
needs `&Arc<Image>`, `images.rs:558`), keyed by `(Texture, ImageViewDesc)`;
`BeginRendering::apply` consumes the cached view via the D7 recorder view
map.
- The D5 view/image consistency check applies to the imported case at
compile time and to the created case after view creation.
## Work items (all in `crates/renderer_rendergraph`)
### Task 1 — New commands in `src/commands.rs`
- `BeginRendering { color: Vec<ColorAttachment>, depth: Option<DepthStencilAttachment>, ... }`
with `ColorAttachment { texture: Texture, view: AttachmentView (D9),
layout: vk::ImageLayout, load_op, store_op, clear_value }` (depth/stencil
analogous). `EndRendering` (unit struct).
- `Command::side_effects`: `BeginRendering` inserts **one merged scope per
attachment** (write + conditional LOAD read, same stage, concrete range
from the desc/view — D3, D9). `EndRendering` inserts synthetic
lifetime-only accesses (D2), tagged to be skipped by barrier generation
and usage derivation.
- Add `fn kind(&self) -> CommandKind` and
`fn allowed_in_render_pass(&self) -> bool` and
`fn render_pass_desc(&self) -> Option<&RenderPassDesc>` to
`RecordedCommand` (D5): `Draw` → `Graphics`/allowed; begin/end →
`Begin`/`End`; `CopyBuffer`/`ClearTexture` → `Transfer`/not allowed;
default `Other`/not allowed.
- `RecordedCommand::apply`: `BeginRendering` builds `vk::RenderingInfo` +
precomputed `vk::RenderingAttachmentInfo`s (resolving `vk::Image` via
`recorder.image(...)`, using the cached/created view via the D7 view map)
and calls `cmd_begin_rendering`; `EndRendering` calls `cmd_end_rendering`.
Allocation-free per D7.
- Export both from `lib.rs`.
### Task 2 — Span tracking & validation in `src/graph.rs`
- Derive spans in `compile()` by scanning command record order using
`CommandKind`; build `node_of_command` and `spans`.
- Validation pass implementing the D5 rules, including the raw-handle
view/image consistency check (imported case), layout legality, duplicate
attachment subresources (via the introspection hook), and the
classification-based inside/outside-pass checks. Linear per D5.
- Update the five existing unit tests that compile `Draw` without a pass
(pushes at `graph.rs:3563, 3749, 3815, 4329, 4469`).
### Task 3 — Span contraction in the compiler
- Contract spans **before** `dependencies()` per D2: aggregate access scopes
per pass node, skip intra-span pairs during edge construction, begin-index
node representation, contracted `dependency_edges`; `HappensBefore` stores
the command→node map and translates in queries.
- Fix the pre-existing stale-`old_layout` defect in `build_barriers`
(`graph.rs:951-982`) with a per-subresource current-layout map (D4).
- Add `BarrierOrigin` to `BarrierResource` (D4).
- Barrier analysis runs over the uncontracted access list in execution order
with interior positions mapped to the pass node (D2); switch the
`batch_indices` debug assertion to execution-order positions (D6).
- `bind_lifetimes`, `HappensBefore::from_dag`, `uses_by_resource`, and
`build_alias_barriers` thread the span map through per D2; all closure
consumers route through the span-aware query; same-node uses are unordered
for aliasing/interference (D2 blocker fix).
- Precompute `execution_order` per D6.
- Frontier reuse per D8 (`first_fit_placement`, `plan_placements` overlapping
loop, `build_alias_barriers`).
### Task 4 — Barrier hoisting, in-pass barriers & recording
- Hoist cross-boundary barriers per D4, including initial-state transitions
for interior first uses (`BarrierOrigin::Initial`); coalesce per
`(resource, exact range, target_layout)` with unioned dst masks;
per-`(aspect, mip, layer)` layout handling for overlapping/disjoint ranges.
- Enforce the aliasing placement constraint per D3 (interference edge /
`aliasing_allowed` predicate) before placement commits offsets.
- Emit in-pass barriers (any `old_layout == new_layout`) between interior
commands in execution order for allowed intra-span hazards (D3).
- Hoisting sequenced before `build_alias_barriers`, hoisted barriers within
the ordinary prefix, `rebuild_barriers` re-runs hoisting (D4).
- `recording_steps_cached` iterates `execution_order`, expanding pass nodes:
barriers (hoisted) → `BeginRendering` → interior commands in record order
(with in-pass barriers interleaved) → `EndRendering`.
- `record_commands`: expanded steps, D7 view map population, and the D5
error-path safety (`in_render_pass` flag, no unclosed rendering instance
on mid-pass `RecordError`).
### Task 5 — Tests
- Unit tests in `graph.rs`:
- span validation errors (unbalanced, nested, draw-outside-pass,
transfer-inside-pass, view/image mismatch, layout legality, duplicate
attachment subresources, feedback-loop rejection);
- contraction correctness (foreign command never appears between begin and
end in `execution_order`; interior accesses still produce barriers;
lifetimes resolved for interior commands);
- barrier hoisting (barrier for an outside producer consumed by an interior
command is keyed to begin; **two interior consumers of one
outside-produced image coalesce into one barrier with unioned dst masks
and correct per-subresource `old_layout`** — regression test for the
reproduced blocker; also cover the non-hoisted two-reader case fixed by
the general `build_barriers` change);
- initial-state hoisting (transient storage image first used inside a pass
gets its `UNDEFINED → GENERAL` transition before `BeginRendering`);
- in-pass barrier emission (interior storage-buffer *and* storage-image
same-layout hazards produce barriers between the interior commands, no
pass split; layout-transition hazard is rejected);
- **aliasing safety (regression test for the rev-4 blocker):** created
attachment `A` + interior-only storage image `B` must **not** share
memory, and no barrier may target an interior command for `A`/`B`;
attachment `terminal_uses == End`;
- ordering of both incoming and outgoing dependencies around the whole
span;
- span-aware closure queries (`happens_before` with interior endpoints;
same-node uses unordered for aliasing).
- Integration tests in `tests/graph_validation.rs` (following the existing
`TestVulkan` pattern):
- skip when `core13.dynamic_rendering != TRUE` in addition to the existing
`synchronization2` check (`tests/graph_validation.rs:108-111`);
- the previously proposed `Draw`-based graph is **not constructible**
(`CopyBuffer` accepts only buffers, `commands.rs:30-79`; `Draw` has no
pipeline handle and never binds one, `commands.rs:143-153, 226-335`).
Use a custom no-op command (like `ScopedNoop`) with a texture read
access as the post-`End` consumer:
`ClearTexture → BeginRendering(color=target) → allowed no-op (texture
read) → EndRendering → no-op (texture read)` compiles, records,
submits, and validates. Adding a real `BindPipeline` command (with a
documented sequence edge to the following `Draw`, since pipeline binding
declares no resources) is an optional separate work item;
- a case proving a hazard-free foreign command is emitted *after*
`EndRendering`, not inside the pass;
- an outside-producer → interior-consumer case asserting the hoisted
barrier appears before `BeginRendering`.
### Task 6 — Existing test/API churn
- `batches()` assertions and helpers that assume command-space batches:
`graph.rs:3774, 4285, 4388-4393, 4674, 5237, 5245, 5257, 5337, 5343, 5397,
5409, 5414` (including `recording_steps(compiled.batches(), …)` at 5343).
- `GraphCompiler::new` / `build_barriers` signature changes (execution order +
span map) break direct unit-test call sites at `graph.rs:3177-3178, 4125,
4242, 4716-4718, 4792-4800, 4846-4854, 4893-4901, 4962-4976, 5051-5052,
5102-5104, 5142-5145, 5171-5173`; `HappensBefore::from_dag` is called
directly by a test (`graph.rs:4140-4155`) and its signature changes.
- Wrapping a `Draw` in a pass shifts barrier indices in tests such as
`sampled_image_import_sets_usage_and_layout_barrier` (asserts
`compiled.barriers()[0]`, `graph.rs:3837-3845`).
- Provide compatibility constructors where cheap, or update all listed call
sites; `cargo test -p renderer_rendergraph` must pass at the end.
## Sequencing
Tasks 1–2 and 3–4 share types, so execute strictly in order: **1 → 2 → 3 →
4 → 5 → 6**, with `cargo check -p renderer_rendergraph` after each and
`cargo test -p renderer_rendergraph` at the end. No changes to other crates;
`renderer`'s legacy `render_graph` module is out of scope.
## Performance summary (from rev-2 review, corrected in revs 3–5)
- **Contraction before `dependencies()`** removes the interior-draw
`O(Σ u_r²)` edge construction and shrinks the `O(N²/64)` closure
memory/time by roughly `(N_c/N)²` (a pass spanning 90% of commands →
~100× less closure work).
- **Contraction does not touch** the placement/alias frontiers; D8's
frontier reuse addresses `first_fit_placement`, `plan_placements`'
overlapping loop, and `build_alias_barriers` (interference frontier
caching already exists).
- **Barrier hoisting is cost-neutral to slightly positive** provided
coalescing follows D4 (per-target-layout, per-subresource handling,
unioned dst masks only for identical ranges/layouts).
- **In-pass barriers for same-layout hazards** avoid the significant
tiled-GPU regression of forced pass splits (attachment store/load
round-trips); only layout transitions and feedback loops force splits.
- **Validation is linear** with the barrier-plan-based intra-span check
(`O(B)`) and per-subresource-set duplicate detection; the existing
layout-conflict precheck grows by `O(a²)` pair iterations per pass
(28 for 8 attachments with the merged single access) — negligible.
- **Recording stays allocation-free** via precomputed attachment infos,
thread-local scratch, precomputed `execution_order`, and begin-index node
representation.
## Risks / open points
- **API shape:** raw `push(BeginRendering)` / `push(EndRendering)` vs. a
scoped `render_pass(desc, |graph| ...)` builder that pushes begin, runs the
closure, and pushes end while recording span command indices. The scoped
builder makes unbalanced spans unrepresentable; raw push is more flexible.
Decide before Task 1.
- **Created-texture views (D9):** view creation after `resolve()` adds a
cache keyed by `(Texture, ImageViewDesc)`; `ImageView` holds `Arc<Image>`
so lifetime is safe; resolve idempotence and atomic creation per D7.
- **Public API semantics:** `batches()` / `dependency_edges()` keep
command-space indices with the begin index as pass-node representative;
document the semantics and consider adding `execution_order()` as the
primary accessor.
- **`BindPipeline`:** out of scope for this plan; without it, submitted
tests cannot include real draws. If added later, it needs a sequence edge
to the following `Draw` (pipeline binding declares no resources, so the
hazard graph will not order it).

View file

@ -0,0 +1,202 @@
# Plan: Correct Transient-Resource Aliasing in `renderer_rendergraph`
Status: proposed — rev 3 (code-verified; rev-2 review + follow-up comments applied)
Date: 2026-10-26
Scope: `crates/renderer_rendergraph`, `crates/renderer_resources`, `crates/renderer_device`
## Goal
Fix the broken transient-resource aliasing in the rendergraph: lifetimes must be
derived from the graph's real ordering (dependency DAG + batches, not command
indices), alias reuse must be synchronized with barriers, and the resources
crate must gain the infrastructure to compute correct memory requirements so
correctly sized pools can be allocated and bound to graph-created resources.
## Diagnosis (verified against code)
| # | Defect | Evidence |
|---|--------|----------|
| 1 | Lifetimes are command-index intervals (`first_command`/`last_command`), but commands within a `topo_batches` batch are conceptually concurrent — two resources used by different commands of the same batch are wrongly treated as non-overlapping | `graph.rs:66-79, 862-877` |
| 2 | Batch index is **not** a happens-before proof: the DAG only contains edges for conflicting same-resource accesses and is discarded after `topo_batches`; e.g. test `independent_commands_share_a_batch...` produces batches `[0,2],[1]` with no edge `2→1` | `graph.rs:643-657, 683-697, 699-714` |
| 3 | Queue transfers unmodeled: `QueueTransfer` exists but every barrier sets `None`; no command→queue assignment exists anywhere (`derive_queue_capabilities` is a resource-wide stage union, not a per-command queue) | `graph.rs:41-58, 792, 822, 1461-1541` |
| 4 | No alias-safety barriers: `build_barriers` only tracks prior uses of the *same* resource; nothing orders last use of alias A before first use of alias B sharing memory | `graph.rs:728-835` |
| 5 | Packing uses `texture_size_estimate`/`desc.size`, ignoring real `VkMemoryRequirements`, alignment, memory-type bits, `bufferImageGranularity`, and dedicated-allocation requirements; offsets are never bound to resources | `graph.rs:888-919, 1543-1588` |
| 6 | No offset-aware bind path: `bind_buffer_memory`/`bind_image_memory` bind whole VMA allocations; `Allocation::Shared(Arc<DeviceObject<GpuAllocation>>)` exists but is unused for pooling | `device.rs:209-214, 766-778` |
## Design decisions (post-review)
### D1 — Happens-before, not batch numbers
Retain the dependency adjacency from `GraphCompiler::dependencies()` in
`CompiledGraph` and add an explicit `happens_before(a, b)` reachability query.
Alias A→B is permitted only if *every* terminal use of A happens-before *every*
initial use of B. Batch intervals are computed (for reporting/limits) but never
used as the safety criterion. Same-batch uses always interfere.
`happens_before` is computed as a **transitive closure at compile time**: one
bitset per command (`Vec<FixedBitSet>`-style reachability rows, or bitset
columns if that packs better), built once in `compile()` from the Kahn
adjacency. Queries are then O(1) and deterministic. Rationale: the command
count is bounded by the recorded command list (thousands at most), closure is
O(V·E/64) once, and per-query BFS would be re-run per alias-candidate pair
during placement for no benefit. Note the closure's semantics: edges encode
only conflicting same-resource accesses, so reachability means exactly
"provably ordered via a resource-hazard chain" — precisely the aliasing-safety
criterion. Commands mutually unreachable (including same-batch pairs) interfere.
### D2 — Phase split
`compile()` stays device-free: dependency order, batch lifetimes, interference.
A new device-dependent `plan_placements(device)` (called from `resolve`) does
requirements queries, pool placement, alias-barrier construction, and binding.
Tests that call `bind_lifetimes()` directly are updated.
### D3 — Conservative queue policy
There is no command→queue assignment today and all commands record into one
command buffer. Aliasing is restricted to the single-queue case; any pair whose
accesses can't be proven ordered on one queue interferes. Cross-queue aliasing
(with ownership transfers/semaphores) is explicitly out of scope and documented
as a follow-up — no fake `QueueTransfer` support.
### D4 — Alias synchronization done correctly
- For aliased **buffers**: emit a `VkMemoryBarrier2` (new `MemoryBarrier` +
`PipelineBarrier.memory_barriers`) before the new occupant's *first-use
command* (a concrete command, not "batch start"), with `src` = old occupant's
aggregated terminal stage/access (writes; execution-only if read-only),
`dst` = new occupant's aggregated first-use scope.
- For aliased **images**: fold the old occupant's terminal scope into the new
image's first-use image barrier (`srcStageMask`/`srcAccessMask` = old scope,
`oldLayout = UNDEFINED`) — a bare memory barrier does not chain with a
following `srcStageMask=NONE` layout transition.
- Aggregate multi-scope accesses per command (OR all stage/access for the
resource at that command).
- Placement is range-based: a new resource's `[offset, offset+size)` is tested
against **all** occupied ranges; barriers include every old occupant
intersecting the range.
### D5 — Vulkan aliasing correctness details
- `bufferImageGranularity`: **separate pools per class** (linear-only and
optimal-only) is the primary strategy — first-fit stays simple and no
padding logic is needed. Intra-pool offsets are rounded only to each
resource's `alignment`. (Intra-pool interleaving with granularity-driven
offset rounding is a deliberate non-goal; revisit only if pool-class split
waste ever shows up in practice.)
- Set `VK_IMAGE_CREATE_ALIAS_BIT` on pooled images.
- Exclude from pools: `requiresDedicatedAllocation` resources, explicit
`AllocationStrategy::Dedicated` (incl. the 64 MiB heuristic), and
host-visible (`CpuToGpu`/`GpuToCpu`) resources — `Buffer::map()` returns
slices from the allocation base and would silently map wrong bytes unless
suballocation offsets are tracked.
- Created-but-unused resources: skip creation entirely (or allocate outside the
pool) — decided as "skip unused transients", with a test.
- Pooled images must have `GraphTextureDesc.layout == UNDEFINED`:
`plan_placements` rejects non-UNDEFINED declared initial layouts on pooled
images with a graph error (a created image's declared initial layout cannot
be honored by reused memory, whose contents are always undefined at first
use). Non-pooled images keep today's behavior.
### D6 — Device/resources infra (new APIs, not existing ones)
- `renderer_device`: `allocate_memory(requirements, …)` via
`vk_mem::Alloc::allocate_memory` with `CAN_ALIAS`; `find_memory_type_index`;
`bind_buffer_at_offset`/`bind_image_at_offset` via `bind_*_memory2` with
`allocation_local_offset`; require API ≥1.1 or enable the full
`get_memory_requirements2`/`bind_memory2`/`dedicated_allocation` extension
set with a clear error otherwise (same guard pattern as the existing
dedicated-allocation check in `device.rs`); define shared-allocation
ownership/cleanup: at bind time the group **transfers** the
`Arc<DeviceObject<GpuAllocation>>` into each bound resource's
`Allocation::Shared` and drops its own reference — the underlying memory is
freed when the last bound resource drops (via `DeviceObject` drop), matching
the existing `Owned`/`Shared` semantics. The group never retains a post-bind
handle.
- `renderer_resources`: symmetric `Buffer::new_unallocated` /
`Image::new_unallocated`, `memory_requirements()` (incl.
`VkMemoryDedicatedRequirements`), `bind_allocation(alloc, offset)` with
alignment/type-bits/bounds validation; a `memory` module with
`MemoryRequirements` + `MemoryGroup` (bind → `Allocation::Shared`). Pool
groups are keyed by a memory type that is in **every member's**
`memoryTypeBits` (intersection), not by any single member's individually
best type; a member whose bits exclude the group's type moves to another
group or its own allocation. New `renderer_resources::Error` variants for
failed requirement queries / bind-time validation (alignment, type-bits,
bounds); `renderer_rendergraph` maps these into new `GraphError` variants
(e.g. `PlacementFailed(ResourceId)`, `BindFailed(ResourceId)`) — bind/placement
failures surface through `resolve`/`plan_placements`'s existing
`renderer_resources::Result` boundary and map at the call site. No graph
types leak into this crate.
## Task breakdown (ordered, dependency-explicit)
1. **`renderer_device`**: memory-type query, aliasable shared allocation,
offset-aware bind, API/extension guards, error variants, ownership docs.
Tests with `TestVulkan` (`--features testing`).
2. **`renderer_resources`**: unbound create + requirements + bind APIs for
buffer *and* image; `memory` module (`MemoryRequirements`, `MemoryGroup`);
update crate AGENTS.md. Existing `Buffer::new`/`Image::new` unchanged.
3. **`renderer_rendergraph` (analysis)**: retain dependency DAG in
`CompiledGraph`; `happens_before` query; batch-mapped
`ResourceLifetime {first_batch, last_batch}`; replace `memory_offset: u64`
with a typed `Placement {offset, size, alignment, pool, aliased}`; imported
resources tracked but excluded from packing; queue-conservatism rule per D3.
4. **`renderer_rendergraph` (synchronization)**: `MemoryBarrier` +
`PipelineBarrier.memory_barriers`; recorder emits `vk::MemoryBarrier2` in
`DependencyInfo` (anchored via the existing `recording_steps`
`before_command` mechanism, like today's image/barrier steps); alias-barrier
construction per D4 (buffer and image variants, aggregation,
first-use-command anchoring); new `GraphError` variants for placement/bind
failures per D6, mapped from `renderer_resources::Error` at the
`resolve` boundary.
5. **`renderer_rendergraph` (placement)**: `plan_placements(device)` **replaces
`resolve()`'s current eager create+allocate loop** (graph.rs:1218-1276) for
pooled resources — create unbound, query requirements, group by a memory
type common to every member's `memoryTypeBits`, granularity-aware first-fit
over ranges (D4/D5, pool-per-class), allocate groups, bind, store
placements in `CompiledGraph`. Non-poolable resources (dedicated,
host-visible, imports) keep an allocate-and-bind-whole path equivalent to
today's. "Skip unused transients" lands here: created resources absent from
`lifetimes` are never created (no behavior change needed in
`record_commands`, which already skips ids absent from `lifetimes` at
graph.rs:1290). Transactional and idempotent; remove
`texture_size_estimate` from allocation decisions.
6. **Docs/tests**: update stale TODOs (`graph.rs:882`, `1315-1319`), document
the single-queue submission contract and the future multi-queue invariant
(alias transitions become semaphore edges).
## Verification
- Device-free unit tests: same-batch interference; unordered-but-distinct-batch
interference; proven-ordered reuse succeeds; granularity boundary (buffer
next to optimal image); range-overlap first-fit; dedicated/host-visible
exclusion; unused-transient skip; pooled-image non-UNDEFINED initial layout
rejected; alias-barrier scope correctness incl. read-only old occupant and
image `UNDEFINED` transition chaining.
- GPU-gated (`tests/graph_validation.rs`): buffer-buffer and image-image alias
reuse through the real barrier path, submit, `device_wait_idle`,
`assert_no_validation_errors`; teardown of shared allocation after all
resources drop.
- Commands: `cargo check --workspace`, `cargo test -p renderer_rendergraph`,
`cargo test -p renderer_resources --features testing`,
`cargo test -p renderer_device --features testing` (from workspace root,
pinned toolchain).
## Review history
- Drafted independently by deepseek/planner and luna/planner; cross-reviewed by
deepseek/reviewer and luna/reviewer (both verdicts: CHANGES REQUIRED).
- Blockers folded into this revision: batch-index ≠ happens-before (D1),
compile/resolve phase split (D2), `bufferImageGranularity` handling (D5),
alias-barrier/layout-transition chaining (D4), offset-aware bind APIs are new
work not existing capability (D6), queue model restricted to single-queue
(D3).
- Rev 3: diagnosis re-verified against code and `vk-mem 0.5.0` sources
(all six rows and the D6 API names confirmed). Follow-up comments folded in:
`happens_before` = compile-time transitive closure (D1), pool-per-class
granularity strategy picked (D5), pooled-image UNDEFINED initial-layout
rule (D5), pool groups keyed by intersecting `memoryTypeBits` (D6),
`MemoryGroup` transfers the `Arc` at bind time instead of retaining it (D6),
placement/bind error surfacing through `GraphError` (D6/task 4),
`plan_placements` explicitly replaces `resolve`'s eager loop (task 5).