Compare commits

..

11 commits

Author SHA1 Message Date
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
38 changed files with 2190 additions and 1688 deletions

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

@ -3,18 +3,16 @@ name = "vidya"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[patch.crates-io]
bevy = {path = "../bevy"}
[workspace] [workspace]
resolver = "2" 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",
] ]
[dependencies] [dependencies]
@ -36,34 +34,35 @@ 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"
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" } 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 = "1.8"
indexmap = "2" indexmap = "2"
petgraph = "0.7" petgraph = "0.7"
itertools = "0.14.0" # 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"
@ -71,22 +70,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.20.0-dev" } bevy = { path = "/var/code/bevy-engine/bevy" }
bevy_app = { version = "0.20.0-dev" } bevy_app = { path = "/var/code/bevy-engine/bevy/crates/bevy_app" }
bevy_asset = { version = "0.20.0-dev" } bevy_asset = { path = "/var/code/bevy-engine/bevy/crates/bevy_asset" }
bevy_derive = { version = "0.20.0-dev" } bevy_derive = { path = "/var/code/bevy-engine/bevy/crates/bevy_derive" }
bevy_ecs = { version = "0.20.0-dev" } bevy_ecs = { path = "/var/code/bevy-engine/bevy/crates/bevy_ecs" }
bevy_hierarchy = { version = "0.20.0-dev" } bevy_log = { path = "/var/code/bevy-engine/bevy/crates/bevy_log" }
bevy_log = { version = "0.20.0-dev" } # bevy_math = { path = "/var/code/bevy-engine/bevy/crates/bevy_math" }
bevy_math = { version = "0.20.0-dev" } bevy_platform = { path = "/var/code/bevy-engine/bevy/crates/bevy_platform" }
bevy_platform = { version = "0.20.0-dev" } bevy_reflect = { path = "/var/code/bevy-engine/bevy/crates/bevy_reflect" }
bevy_reflect = { version = "0.20.0-dev" } bevy_tasks = { path = "/var/code/bevy-engine/bevy/crates/bevy_tasks" }
bevy_tasks = { version = "0.20.0-dev" } bevy_time = { path = "/var/code/bevy-engine/bevy/crates/bevy_time" }
bevy_time = { version = "0.20.0-dev" } # bevy_transform = { path = "/var/code/bevy-engine/bevy/crates/bevy_transform" }
bevy_transform = { version = "0.20.0-dev" } bevy_utils = { path = "/var/code/bevy-engine/bevy/crates/bevy_utils" }
bevy_utils = { version = "0.20.0-dev" } bevy_window = { path = "/var/code/bevy-engine/bevy/crates/bevy_window" }
bevy_window = { version = "0.20.0-dev" } # bevy_winit = { path = "/var/code/bevy-engine/bevy/crates/bevy_winit" }
bevy_winit = { version = "0.20.0-dev" }
[[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);

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, memory location, queue-sharing flags, and allocation strategy; `Buffer::map`/`map_mut` expose mapped memory only when the allocator returns a mapped allocation.
- 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 allocation memory with its `DeviceObject`; `Allocation::Shared` permits shared ownership and only exposes mutable access when uniquely held; `Unmanaged` carries no allocator 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 via `DeviceInner.alloc2`; device ownership and final 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` wraps `u64`.
- 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,42 @@ 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 } 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"] }

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,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 struct SingleUseCommand {
pub SingleUseCommand(vk::CommandBuffer) { 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()]);
}
} }
} }

View file

@ -2,40 +2,36 @@
negative_impls, negative_impls,
map_try_insert, map_try_insert,
debug_closure_helpers, 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::{device, instance, queue, sync};
pub use renderer_device::{
compatible_extension_properties, extension_intersection,
get_physical_device_features, get_physical_device_properties, make_extension,
DebugUtils, DebugUtilsCreateInfo, Device, DeviceOwned, DeviceRef, Error,
ExtendsDeviceFeatures2Debug, ExtendsDeviceProperties2Debug, Extension, Instance,
PhysicalDeviceFeatures, PhysicalDeviceInfo, PhysicalDeviceProperties, Result,
SurfaceCapabilities,
};
pub use renderer_macros::DeviceRef;
mod texture { mod texture {
use std::{collections::BTreeMap, sync::Arc}; use std::{collections::BTreeMap, sync::Arc};
@ -79,581 +75,7 @@ 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,
@ -922,7 +344,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 +459,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;
@ -1051,8 +475,7 @@ impl Renderer2 {
pub use vk::Extent2D; pub use vk::Extent2D;
use crate::{ use crate::{
device::Extension, instance::InstanceDesc,
instance::{InstanceDesc, InstanceInner},
}; };
pub mod utils { pub mod utils {

View file

@ -471,52 +471,6 @@ pub struct RenderAttachment {
pub store_op: vk::AttachmentStoreOp, pub store_op: vk::AttachmentStoreOp,
} }
impl RenderAttachment {
pub fn side_effect(&self, mut map: SideEffectMap, load: bool) {
for attachment in &self.color_attachments {
// The dependencies declared here need to be barrier'd before and
// after the render pass: the spec says pipelineBarrier inside of a
// render pass instance referencing framebuffer-local stages must
// contain only framebuffer-local stages.
map.with(
vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT,
if attachment.load_op == vk::AttachmentLoadOp::LOAD {
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 {
map.with(
vk::PipelineStageFlags2::EARLY_FRAGMENT_TESTS,
if depth.load_op == vk::AttachmentLoadOp::LOAD {
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 {
map.with(
vk::PipelineStageFlags2::EARLY_FRAGMENT_TESTS,
if stencil.load_op == vk::AttachmentLoadOp::LOAD {
vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_READ
} else {
vk::AccessFlags2::empty()
},
Some(stencil.layout),
)
.insert_range(stencil.texture.id(), stencil.region);
}
}
}
pub struct BeginRendering { pub struct BeginRendering {
pub color_attachments: Vec<RenderAttachment>, pub color_attachments: Vec<RenderAttachment>,
pub depth_attachment: Option<RenderAttachment>, pub depth_attachment: Option<RenderAttachment>,
@ -525,21 +479,7 @@ pub struct BeginRendering {
pub layers: u32, pub layers: u32,
} }
pub struct EndRendering { pub struct EndRendering;
pub color_attachments: Vec<RenderAttachment>,
pub depth_attachment: Option<RenderAttachment>,
pub stencil_attachment: Option<RenderAttachment>,
}
impl BeginRendering {
fn end_rendering(&self) -> EndRendering {
EndRendering {
color_attachments: self.color_attachments.clone(),
depth_attachment: self.depth_attachment.clone(),
stencil_attachment: self.stencil_attachment.clone(),
}
}
}
impl Command for BeginRendering { impl Command for BeginRendering {
fn side_effects(&self, mut map: SideEffectMap) { fn side_effects(&self, mut map: SideEffectMap) {

View file

@ -80,11 +80,7 @@ impl<'r> GraphBuilder<'r> {
mod api_demo { mod api_demo {
use crate::{ use crate::{EguiState, pipeline::Pipeline, render_graph::commands};
EguiState,
pipeline::Pipeline,
render_graph::{commands, resources::TextureRegion},
};
use super::*; use super::*;
use ash::vk::{self, BufferUsageFlags}; use ash::vk::{self, BufferUsageFlags};
@ -154,13 +150,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,
@ -177,7 +177,7 @@ 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,

View file

@ -224,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) {
@ -396,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,7 +2,7 @@ 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;

View file

@ -1,7 +1,10 @@
use std::sync::Arc; use std::sync::Arc;
use ash::vk; use ash::vk;
use glam::{f32::Mat4, vec3}; use glam::{
camera::rh::{proj, view},
vec3,
};
use gpu_allocator::MemoryLocation; use gpu_allocator::MemoryLocation;
pub use crate::egui_pass::{egui_pass, egui_pre_pass}; pub use crate::egui_pass::{egui_pass, egui_pre_pass};
@ -161,11 +164,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,14 +318,14 @@ 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()

View file

@ -15,6 +15,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 +148,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 +209,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 +247,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 +434,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 +543,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 +563,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,
@ -832,7 +854,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,119 @@
# 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.
- `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`, allocator mutex, adapter and instance, queues, synchronization threadpool, extension loaders, and inline `DevicePools`.
- `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, loading, allocator, 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.
- `AllocationStrategy` selects allocator-managed blocks or a dedicated allocation. `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 the selected graphics family to support presentation.
- `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.
- `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.
- `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 currently asserts that its chosen graphics family is present-capable.
- 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 allocator mutex. 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,22 @@
[package]
name = "renderer_device"
version = "0.1.0"
edition = "2024"
[dependencies]
ash = { workspace = true }
ash-window = { workspace = true }
gpu-allocator = { 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" }

View file

@ -3,21 +3,78 @@ use std::{
collections::{BTreeSet, HashMap, HashSet}, collections::{BTreeSet, HashMap, HashSet},
ffi::CStr, ffi::CStr,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
sync::Arc, sync::{Arc, Weak},
}; };
use ash::{ use ash::{
ext, khr, ext, khr,
prelude::VkResult, prelude::VkResult,
vk::{self, Handle}, vk,
}; };
use parking_lot::Mutex; use parking_lot::Mutex;
use raw_window_handle::RawDisplayHandle; use raw_window_handle::RawDisplayHandle;
#[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),
}
}
}
use crate::{ use crate::{
Instance, PhysicalDeviceFeatures, PhysicalDeviceInfo, Result, Instance, PhysicalDeviceFeatures, PhysicalDeviceInfo, Result,
device::asdf::traits::ExternallyManagedObject, device::asdf::traits::ExternallyManagedObject,
pipeline::pipeline_cache::PipelineCache, pipeline_cache::PipelineCache,
queue::{DeviceQueueInfos, DeviceQueues, Queue}, queue::{DeviceQueueInfos, DeviceQueues, Queue},
sync::{self, BinarySemaphore, TimelineSemaphore}, sync::{self, BinarySemaphore, TimelineSemaphore},
}; };
@ -36,21 +93,25 @@ bitflags::bitflags! {
} }
} }
struct DeviceDrop(ash::Device); struct DeviceDrop {
device: ash::Device,
pipeline_cache: PipelineCache,
}
impl Drop for DeviceDrop { impl Drop for DeviceDrop {
fn drop(&mut self) { fn drop(&mut self) {
unsafe { unsafe {
_ = self.0.device_wait_idle(); _ = self.device.device_wait_idle();
self.0.destroy_device(None); self.pipeline_cache.destroy(&self.device);
self.device.destroy_device(None);
} }
} }
} }
pub(crate) struct DeviceExtensions { pub struct DeviceExtensions {
pub(crate) debug_utils: ext::debug_utils::Device, pub debug_utils: ext::debug_utils::Device,
pub(crate) swapchain: Option<khr::swapchain::Device>, pub swapchain: Option<khr::swapchain::Device>,
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) mesh_shader: Option<ext::mesh_shader::Device>, pub mesh_shader: Option<ext::mesh_shader::Device>,
} }
type GpuAllocation = gpu_allocator::vulkan::Allocation; type GpuAllocation = gpu_allocator::vulkan::Allocation;
@ -75,7 +136,7 @@ pub enum Allocation {
} }
impl Allocation { impl Allocation {
pub(crate) fn allocation(&self) -> Option<&GpuAllocation> { pub fn allocation(&self) -> Option<&GpuAllocation> {
match self { match self {
Allocation::Owned(obj) => Some(obj), Allocation::Owned(obj) => Some(obj),
Allocation::Shared(arc) => Some(arc.as_ref()), Allocation::Shared(arc) => Some(arc.as_ref()),
@ -83,7 +144,7 @@ impl Allocation {
} }
} }
pub(crate) fn allocation_mut(&mut self) -> Option<&mut GpuAllocation> { pub fn allocation_mut(&mut self) -> Option<&mut GpuAllocation> {
match self { match self {
Allocation::Owned(obj) => Some(obj), Allocation::Owned(obj) => Some(obj),
Allocation::Shared(arc) => Arc::get_mut(arc).map(DerefMut::deref_mut), Allocation::Shared(arc) => Arc::get_mut(arc).map(DerefMut::deref_mut),
@ -93,15 +154,16 @@ impl Allocation {
} }
pub struct DeviceInner { pub struct DeviceInner {
pub(crate) alloc2: Mutex<gpu_allocator::vulkan::Allocator>, pub alloc2: Mutex<gpu_allocator::vulkan::Allocator>,
pub(crate) raw: ash::Device, pub raw: ash::Device,
pub(crate) adapter: PhysicalDeviceInfo, pub adapter: PhysicalDeviceInfo,
pub(crate) instance: Instance, pub instance: Instance,
pub(crate) queues: DeviceQueues, pub queues: DeviceQueues,
pub(crate) sync_threadpool: sync::SyncThreadpool, pub sync_threadpool: sync::SyncThreadpool,
pub(crate) device_extensions: DeviceExtensions, pub device_extensions: DeviceExtensions,
pub pools: DevicePools,
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) enabled_extensions: Vec<&'static CStr>, pub enabled_extensions: Vec<&'static CStr>,
_drop: DeviceDrop, _drop: DeviceDrop,
} }
@ -150,7 +212,7 @@ impl<'a> std::hash::Hash for Extension<'a> {
} }
} }
pub(crate) fn get_available_extensions( pub fn get_available_extensions(
entry: &ash::Entry, entry: &ash::Entry,
layers: &[&CStr], layers: &[&CStr],
) -> Result<Vec<ash::vk::ExtensionProperties>> { ) -> Result<Vec<ash::vk::ExtensionProperties>> {
@ -170,7 +232,7 @@ pub(crate) fn get_available_extensions(
} }
/// returns a tuple of supported-or-enabled extensions and unsupported-and-requested extensions /// returns a tuple of supported-or-enabled extensions and unsupported-and-requested extensions
pub(crate) fn get_extensions<'a>( pub fn get_extensions<'a>(
entry: &ash::Entry, entry: &ash::Entry,
layers: &[&'a CStr], layers: &[&'a CStr],
mut extensions: Vec<Extension<'a>>, mut extensions: Vec<Extension<'a>>,
@ -288,7 +350,7 @@ pub(crate) fn get_extensions<'a>(
} }
/// returns a list of enabled, or a tuple of enabled and unsupported but requested layers. /// returns a list of enabled, or a tuple of enabled and unsupported but requested layers.
pub(crate) fn get_layers<'a>( pub fn get_layers<'a>(
entry: &ash::Entry, entry: &ash::Entry,
wants_layers: Vec<&'a CStr>, wants_layers: Vec<&'a CStr>,
) -> core::result::Result<Vec<&'a CStr>, (Vec<&'a CStr>, Vec<&'a CStr>)> { ) -> core::result::Result<Vec<&'a CStr>, (Vec<&'a CStr>, Vec<&'a CStr>)> {
@ -393,7 +455,8 @@ impl PhysicalDeviceInfo {
}, },
})?; })?;
let inner = DeviceInner { let pipeline_cache = PipelineCache::new(&device, &self).unwrap();
let shared = Arc::new_cyclic(|inner: &Weak<DeviceInner>| DeviceInner {
raw: device.clone(), raw: device.clone(),
alloc2: Mutex::new(alloc2), alloc2: Mutex::new(alloc2),
instance: instance.clone(), instance: instance.clone(),
@ -402,14 +465,13 @@ impl PhysicalDeviceInfo {
device_extensions, device_extensions,
enabled_extensions, enabled_extensions,
sync_threadpool: sync::SyncThreadpool::new(), sync_threadpool: sync::SyncThreadpool::new(),
_drop: DeviceDrop(device), pools: DevicePools::new(inner.clone(), pipeline_cache),
}; _drop: DeviceDrop {
device,
let shared = Arc::new(inner); pipeline_cache,
Ok(Device { },
pools: Arc::new(DevicePools::new(shared.clone())), });
shared, Ok(Device(shared))
})
} }
fn required_extensions(&self, requested_extensions: &[Extension<'static>]) -> Vec<*const i8> { fn required_extensions(&self, requested_extensions: &[Extension<'static>]) -> Vec<*const i8> {
@ -438,42 +500,58 @@ impl PhysicalDeviceInfo {
} }
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct DevicePools { pub struct DevicePools {
pub(crate) pipeline_cache: asdf::DeviceObject<PipelineCache, Arc<DeviceInner>>, pub pipeline_cache: PipelineCache,
pub(crate) fences: Arc<Pool<vk::Fence>>, pub fences: Pool<vk::Fence>,
pub(crate) binary_semaphores: Pool<BinarySemaphore>, pub binary_semaphores: Pool<BinarySemaphore>,
pub(crate) timeline_semaphores: Pool<TimelineSemaphore>, pub timeline_semaphores: Pool<TimelineSemaphore>,
} }
impl AsRef<DevicePools> for DevicePools { impl DevicePools {
fn as_ref(&self) -> &DevicePools { fn new(device: Weak<DeviceInner>, pipeline_cache: PipelineCache) -> Self {
Self {
fences: Pool::new(device.clone()),
binary_semaphores: Pool::new(device.clone()),
timeline_semaphores: Pool::new(device.clone()),
pipeline_cache,
}
}
}
pub trait DeviceRef {
fn device(&self) -> &Device;
}
impl DeviceRef for Device {
fn device(&self) -> &Device {
self self
} }
} }
impl DevicePools { impl DeviceRef for Arc<Device> {
pub fn new(device: Arc<DeviceInner>) -> Self { fn device(&self) -> &Device {
Self { self.as_ref()
fences: Arc::new(Pool::new(device.clone())),
binary_semaphores: Pool::new(device.clone()),
timeline_semaphores: Pool::new(device.clone()),
pipeline_cache: asdf::DeviceObject::new(
device.clone(),
PipelineCache::new(&device.raw, &device.adapter).unwrap(),
),
} }
}
impl DeviceRef for &Device {
fn device(&self) -> &Device {
self
}
}
impl DeviceRef for &Arc<Device> {
fn device(&self) -> &Device {
self.as_ref()
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Device { pub struct Device(Arc<DeviceInner>);
pub(crate) shared: Arc<DeviceInner>,
pub(crate) pools: Arc<DevicePools>,
}
impl PartialEq for Device { impl PartialEq for Device {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.shared, &other.shared) Arc::ptr_eq(&self.0, &other.0)
} }
} }
@ -483,7 +561,7 @@ impl core::ops::Deref for Device {
type Target = DeviceInner; type Target = DeviceInner;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.shared &self.0
} }
} }
@ -516,7 +594,7 @@ impl DeviceInner {
pub fn features(&self) -> &crate::PhysicalDeviceFeatures { pub fn features(&self) -> &crate::PhysicalDeviceFeatures {
&self.adapter.features &self.adapter.features
} }
pub(crate) fn properties(&self) -> &crate::PhysicalDeviceProperties { pub fn properties(&self) -> &crate::PhysicalDeviceProperties {
&self.adapter.properties &self.adapter.properties
} }
pub fn physical_device(&self) -> &PhysicalDeviceInfo { pub fn physical_device(&self) -> &PhysicalDeviceInfo {
@ -625,75 +703,7 @@ impl DeviceInner {
} }
} }
#[derive(Clone)] pub use asdf::DeviceObject;
pub struct DeviceOwnedDebugObject<T> {
pub(crate) device: Device,
pub(crate) object: T,
#[cfg(debug_assertions)]
name: Option<Cow<'static, str>>,
}
impl<T: Eq> Eq for DeviceOwnedDebugObject<T> {}
impl<T: PartialEq> PartialEq for DeviceOwnedDebugObject<T> {
fn eq(&self, other: &Self) -> bool {
std::sync::Arc::ptr_eq(&self.device.shared, &other.device.shared)
&& self.object == other.object
}
}
impl<T: std::fmt::Debug + vk::Handle + Copy> std::fmt::Debug for DeviceOwnedDebugObject<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut fmt = f.debug_struct(core::any::type_name::<T>());
fmt.field_with("device", |f| {
write!(f, "0x{:x}", self.device.raw.handle().as_raw())
})
.field_with("handle", |f| write!(f, "0x{:x}", &self.object.as_raw()));
#[cfg(debug_assertions)]
{
fmt.field("name", &self.name);
}
fmt.finish()
}
}
impl<T> DeviceOwnedDebugObject<T> {
pub fn new(
device: crate::Device,
object: T,
name: Option<Cow<'static, str>>,
) -> ash::prelude::VkResult<Self>
where
T: vk::Handle + Copy,
{
if let Some(name) = name.as_ref() {
unsafe {
device.debug_name_object(object, name);
}
}
Ok(Self {
device,
object,
#[cfg(debug_assertions)]
name,
})
}
pub fn dev(&self) -> &crate::Device {
&self.device
}
pub fn handle(&self) -> T
where
T: Copy,
{
self.object
}
}
pub use asdf::{DeviceObject, InnerDeviceObject};
pub trait DeviceHandle { pub trait DeviceHandle {
/// # Safety /// # Safety
@ -708,18 +718,68 @@ impl<O, T: ExternallyManagedObject<O>> ExternallyManagedObject<O> for Mutex<T> {
} }
} }
impl<T: AsRef<DeviceInner>> ExternallyManagedObject<T> for vk::Buffer { impl ExternallyManagedObject<Device> for vk::Buffer {
unsafe fn destroy(self, device: &T) { unsafe fn destroy(self, device: &Device) {
unsafe { unsafe { device.raw.destroy_buffer(self, None) };
device.as_ref().raw.destroy_buffer(self, None);
} }
}
impl ExternallyManagedObject<Device> for vk::Image {
unsafe fn destroy(self, device: &Device) {
unsafe { device.raw.destroy_image(self, None) };
}
}
impl ExternallyManagedObject<Device> for vk::ImageView {
unsafe fn destroy(self, device: &Device) {
unsafe { device.raw.destroy_image_view(self, None) };
} }
} }
impl<T: AsRef<DeviceInner>> ExternallyManagedObject<T> for vk::SwapchainKHR { impl DeviceHandle for vk::DescriptorPool {
unsafe fn destroy(self, device: &T) { unsafe fn destroy(&mut self, device: &Device) {
unsafe { device.dev().destroy_descriptor_pool(*self, None) };
}
}
impl ExternallyManagedObject<Device> for vk::DescriptorPool {
unsafe fn destroy(self, device: &Device) {
unsafe { device.raw.destroy_descriptor_pool(self, None) };
}
}
impl ExternallyManagedObject<Device> for vk::DescriptorSetLayout {
unsafe fn destroy(self, device: &Device) {
unsafe { device.raw.destroy_descriptor_set_layout(self, None) };
}
}
impl ExternallyManagedObject<Device> for vk::PipelineLayout {
unsafe fn destroy(self, device: &Device) {
unsafe { device.raw.destroy_pipeline_layout(self, None) };
}
}
impl ExternallyManagedObject<Device> for vk::Sampler {
unsafe fn destroy(self, device: &Device) {
unsafe { device.raw.destroy_sampler(self, None) };
}
}
impl ExternallyManagedObject<Device> for vk::ShaderModule {
unsafe fn destroy(self, device: &Device) {
unsafe { device.raw.destroy_shader_module(self, None) };
}
}
impl ExternallyManagedObject<Device> for vk::Pipeline {
unsafe fn destroy(self, device: &Device) {
unsafe { device.raw.destroy_pipeline(self, None) };
}
}
impl ExternallyManagedObject<Device> for vk::SwapchainKHR {
unsafe fn destroy(self, device: &Device) {
unsafe { unsafe {
if let Some(swapchain) = device.as_ref().device_extensions.swapchain.as_ref() { if let Some(swapchain) = device.device_extensions.swapchain.as_ref() {
swapchain.destroy_swapchain(self, None) swapchain.destroy_swapchain(self, None)
} }
} }
@ -735,17 +795,22 @@ pub trait Pooled: Sized {
fn create_from_pool(pool: &Pool<Self>) -> Result<Self>; fn create_from_pool(pool: &Pool<Self>) -> Result<Self>;
} }
/// A pooled handle which knows how to return itself to its pool.
pub trait Recycle: Sized {
fn recycle(self, device: &Device);
}
#[derive(Debug)] #[derive(Debug)]
pub struct Pool<T> { pub struct Pool<T> {
pub(crate) pool: Mutex<Vec<T>>, pub pool: Mutex<Vec<T>>,
pub(crate) device: Arc<DeviceInner>, device: Weak<DeviceInner>,
} }
impl<T> Pool<T> { impl<T> Pool<T> {
pub fn push(&self, item: T) { pub fn push(&self, item: T) {
self.pool.lock().push(item); self.pool.lock().push(item);
} }
pub fn new(device: Arc<DeviceInner>) -> Self { pub fn new(device: Weak<DeviceInner>) -> Self {
Self { Self {
pool: Mutex::new(Vec::new()), pool: Mutex::new(Vec::new()),
device, device,
@ -754,6 +819,10 @@ impl<T> Pool<T> {
pub fn pop(&self) -> Option<T> { pub fn pop(&self) -> Option<T> {
self.pool.lock().pop() self.pool.lock().pop()
} }
pub fn device(&self) -> Arc<DeviceInner> {
self.device.upgrade().expect("pool outlived its device")
}
} }
impl<T> AsRef<Pool<T>> for Pool<T> { impl<T> AsRef<Pool<T>> for Pool<T> {
@ -762,7 +831,43 @@ impl<T> AsRef<Pool<T>> for Pool<T> {
} }
} }
pub type PoolObject<T, U = Arc<Pool<T>>> = asdf::ExternallyManagedObject<T, U>; pub struct PoolObject<T: Recycle> {
inner: std::mem::ManuallyDrop<T>,
device: Device,
}
impl<T: Recycle> PoolObject<T> {
pub fn new(inner: T, device: Device) -> Self {
Self {
inner: std::mem::ManuallyDrop::new(inner),
device,
}
}
pub fn device(&self) -> &Device {
&self.device
}
}
impl<T: Recycle> Deref for PoolObject<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T: Recycle> DerefMut for PoolObject<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl<T: Recycle> Drop for PoolObject<T> {
fn drop(&mut self) {
let inner = unsafe { std::mem::ManuallyDrop::take(&mut self.inner) };
inner.recycle(&self.device);
}
}
impl<T: Pooled> Pool<T> { impl<T: Pooled> Pool<T> {
pub fn get(&self) -> Result<T> { pub fn get(&self) -> Result<T> {
@ -784,90 +889,33 @@ impl<T: Pooled> Pool<T> {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
{ {
let name = name.map(Into::into).unwrap_or_default(); let name = name.map(Into::into).unwrap_or_default();
<T as asdf::traits::DebugNameable>::debug_name(&obj, &self.device, &name); let device = self.device();
<T as asdf::traits::DebugNameable>::debug_name(&obj, &device, &name);
} }
Ok(obj) Ok(obj)
} }
} }
// Macro for helping create and destroy Vulkan objects which are owned by a device. pub mod asdf {
#[macro_export]
macro_rules! define_device_owned_handle {
($(#[$attr:meta])*
$ty_vis:vis $ty:ident($handle:ty) {
$($(#[$field_attr:meta])* $field_vis:vis $field_name:ident : $field_ty:ty),*
$(,)?
} $(=> |$this:ident| $dtor:stmt)?) => {
$(#[$attr])*
$ty_vis struct $ty {
inner: $crate::device::DeviceOwnedDebugObject<$handle>,
$(
$(#[$field_attr])*
$field_vis $field_name: $field_ty,
)*
}
impl $crate::device::DeviceOwned<$handle> for $ty {
fn device(&self) -> &$crate::device::Device {
self.inner.dev()
}
fn handle(&self) -> $handle {
self.inner.handle()
}
}
impl $ty {
#[allow(clippy::too_many_arguments, reason = "This function is generated by a macro")]
fn construct(
device: $crate::device::Device,
handle: $handle,
name: Option<::std::borrow::Cow<'static, str>>,
$($field_name: $field_ty,)*
) -> ::ash::prelude::VkResult<Self> {
Ok(Self {
inner: $crate::device::DeviceOwnedDebugObject::new(
device,
handle,
name,
)?,
$($field_name,)*
})
}
}
$(
impl Drop for $ty {
fn drop(&mut self) {
#[allow(unused_mut)]
let mut $this = self;
$dtor
}
}
)?
};
}
// This module is an experiment in a more generic way to manage device-owned resources.
// #[cfg(false)]
pub(crate) mod asdf {
use std::{ use std::{
mem::{ManuallyDrop, MaybeUninit}, mem::ManuallyDrop,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
sync::Arc,
}; };
use ash::vk; use ash::vk;
use ash::vk::Handle;
use crate::{device::DeviceInner, util::DebugName}; use crate::device::{DebugName, DeviceInner};
use crate::Device;
pub mod traits { pub mod traits {
/// A trait describing an object owned by some manager-type, which is /// A trait describing an object owned by some manager-type, which is
/// responsible for destroying it. /// responsible for destroying it.
pub trait ExternallyManagedObject<T> { pub trait ExternallyManagedObject<Owner> {
/// # Safety /// # Safety
/// The caller must ensure this function is only called once for a given object. /// The caller must ensure this function is only called once for a given object.
unsafe fn destroy(self, owner: &T); unsafe fn destroy(self, owner: &Owner);
} }
/// A trait describing an object which can have a debug name assigned to it. /// A trait describing an object which can have a debug name assigned to it.
@ -904,45 +952,6 @@ pub(crate) mod asdf {
owner, owner,
} }
} }
pub fn owner(&self) -> &O {
&self.owner
}
pub fn map_inner<U>(self, f: impl FnOnce(T) -> U) -> ExternallyManagedObject<U, O>
where
U: traits::ExternallyManagedObject<O>,
{
unsafe {
let mut this = MaybeUninit::new(self);
let inner = ManuallyDrop::take(&mut this.assume_init_mut().inner);
let owner = core::ptr::read(&raw const this.assume_init_mut().owner);
let new_inner = f(inner);
ExternallyManagedObject {
inner: ManuallyDrop::new(new_inner),
owner,
}
}
}
pub fn map_owner<U>(self, f: impl FnOnce(O) -> U) -> ExternallyManagedObject<T, U>
where
T: traits::ExternallyManagedObject<U>,
{
unsafe {
let mut this = MaybeUninit::new(self);
let inner = ManuallyDrop::take(&mut this.assume_init_mut().inner);
// get the old owner without calling `Self::drop`
let owner = core::ptr::read(&raw const this.assume_init_mut().owner);
ExternallyManagedObject {
inner: ManuallyDrop::new(inner),
owner: f(owner),
}
}
}
} }
impl<T, O> Drop for ExternallyManagedObject<T, O> impl<T, O> Drop for ExternallyManagedObject<T, O>
@ -957,138 +966,131 @@ pub(crate) mod asdf {
} }
} }
pub type InnerDeviceObject<T> = DeviceObject<T, Arc<DeviceInner>>;
/// A wrapper for vulkan types which are owned by the device, taking care of destruction. /// A wrapper for vulkan types which are owned by the device, taking care of destruction.
#[derive(Debug)] pub struct DeviceObject<T: traits::ExternallyManagedObject<Device>> {
pub struct DeviceObject< inner: ExternallyManagedObject<T, Device>,
T: traits::ExternallyManagedObject<O>,
O: AsRef<super::DeviceInner> = super::Device,
> {
inner: ExternallyManagedObject<T, O>,
#[allow(dead_code)] #[allow(dead_code)]
name: Option<DebugName>, name: Option<DebugName>,
} }
impl< impl<T: traits::ExternallyManagedObject<Device> + traits::DebugNameable> DeviceObject<T> {
T: traits::ExternallyManagedObject<O> + traits::DebugNameable, pub fn new_debug_named(
O: AsRef<super::DeviceInner>, device: Device,
> DeviceObject<T, O> inner: T,
{ name: Option<impl Into<DebugName>>,
pub fn new_debug_named(owner: O, inner: T, name: Option<impl Into<DebugName>>) -> Self { ) -> Self {
let name = name.map(Into::into); let name = name.map(Into::into);
if let Some(ref name) = name { if let Some(ref name) = name {
traits::DebugNameable::debug_name(&inner, owner.as_ref(), name); traits::DebugNameable::debug_name(&inner, &device, name);
} }
let obj = ExternallyManagedObject::new(inner, owner); let obj = ExternallyManagedObject::new(inner, device);
Self { inner: obj, name } Self { inner: obj, name }
} }
} }
impl<T: traits::ExternallyManagedObject<O>, O: AsRef<super::DeviceInner>> DeviceObject<T, O> { impl<T: traits::ExternallyManagedObject<Device>> DeviceObject<T> {
pub fn new(owner: O, inner: T) -> Self { pub fn new(device: Device, inner: T) -> Self {
let inner = ExternallyManagedObject::new(inner, owner); let inner = ExternallyManagedObject::new(inner, device);
Self { inner, name: None } Self { inner, name: None }
} }
pub fn new_debug_named_with<D>( pub fn device(&self) -> &Device {
owner: O, &self.inner.owner
inner: T, }
name: Option<impl Into<DebugName>>,
debug_namable: impl FnOnce(&T) -> D, pub fn handle(&self) -> T
) -> Self
where where
D: traits::DebugNameable, T: Copy,
{ {
let name = name.map(Into::into); *Deref::deref(self)
if let Some(ref name) = name {
traits::DebugNameable::debug_name(&debug_namable(&inner), owner.as_ref(), name);
}
let obj = ExternallyManagedObject::new(inner, owner);
Self { inner: obj, name }
}
pub fn device(&self) -> &O {
self.inner.owner()
} }
pub fn name(&self) -> Option<&str> { pub fn name(&self) -> Option<&str> {
self.name.as_ref().map(|n| &**n) self.name.as_deref()
}
} }
pub fn map_inner<U>(self, f: impl FnOnce(T) -> U) -> DeviceObject<U, O> impl<T> std::fmt::Debug for DeviceObject<T>
where where
U: traits::ExternallyManagedObject<O>, T: std::fmt::Debug + traits::ExternallyManagedObject<Device>,
{ {
DeviceObject { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
inner: self.inner.map_inner(f), let mut fmt = f.debug_struct(core::any::type_name::<T>());
name: self.name, let inner: &T = &self.inner;
fmt.field_with("device", |f| {
write!(
f,
"0x{:x}", self.inner.owner.raw.handle().as_raw()
)
})
.field_with("handle", |f| write!(f, "{inner:?}"))
.field("name", &self.name);
fmt.finish()
} }
} }
pub fn map_owner<U>(self, f: impl FnOnce(O) -> U) -> DeviceObject<T, U> impl<T> Deref for DeviceObject<T>
where where
T: traits::ExternallyManagedObject<U>, T: traits::ExternallyManagedObject<Device>,
U: AsRef<super::DeviceInner>,
{
DeviceObject {
inner: self.inner.map_owner(f),
name: self.name,
}
}
}
impl<T, O> Deref for DeviceObject<T, O>
where
T: traits::ExternallyManagedObject<O>,
O: AsRef<super::DeviceInner>,
{ {
type Target = T; type Target = T;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&*self.inner &self.inner
} }
} }
impl<T, O> DerefMut for DeviceObject<T, O> impl<T> DerefMut for DeviceObject<T>
where where
T: traits::ExternallyManagedObject<O>, T: traits::ExternallyManagedObject<Device>,
O: AsRef<super::DeviceInner>,
{ {
fn deref_mut(&mut self) -> &mut Self::Target { fn deref_mut(&mut self) -> &mut Self::Target {
&mut *self.inner &mut self.inner
} }
} }
mod impls { mod impls {
use crate::device::{DeviceInner, DevicePools, GpuAllocation}; use crate::device::GpuAllocation;
use super::*; use super::*;
impl<T: AsRef<DeviceInner>> traits::ExternallyManagedObject<T> for vk::Semaphore { impl traits::ExternallyManagedObject<Device> for vk::Semaphore {
unsafe fn destroy(self, device: &T) { unsafe fn destroy(self, device: &Device) {
unsafe { unsafe { device.raw.destroy_semaphore(self, None) };
device.as_ref().raw.destroy_semaphore(self, None);
}
}
}
impl<T: AsRef<DeviceInner>> traits::ExternallyManagedObject<T> for GpuAllocation {
unsafe fn destroy(self, device: &T) {
_ = device.as_ref().alloc2.lock().free(self);
} }
} }
impl traits::ExternallyManagedObject<DevicePools> for vk::Semaphore { impl traits::ExternallyManagedObject<Device> for GpuAllocation {
unsafe fn destroy(self, owner: &DevicePools) { unsafe fn destroy(self, device: &Device) {
owner _ = device.alloc2.lock().free(self);
.binary_semaphores }
.push(crate::sync::BinarySemaphore(self)); }
impl traits::ExternallyManagedObject<Device> for vk::CommandBuffer {
// Command buffers cannot be destroyed individually; their outer wrappers free them in Drop.
unsafe fn destroy(self, _: &Device) {}
}
impl traits::ExternallyManagedObject<Device> for vk::CommandPool {
unsafe fn destroy(self, device: &Device) {
unsafe { device.raw.destroy_command_pool(self, None) };
}
}
impl<T> crate::device::DeviceOwned<T> for DeviceObject<T>
where
T: vk::Handle + Copy + traits::ExternallyManagedObject<Device>,
{
fn device(&self) -> &Device {
self.device()
}
fn handle(&self) -> T {
self.handle()
} }
} }
@ -1107,30 +1109,27 @@ pub(crate) mod asdf {
#[allow(dead_code)] #[allow(dead_code)]
#[cfg(test)] #[cfg(test)]
fn asdf() { fn asdf() {
use crate::device::{DevicePools, GpuAllocation}; use crate::device::GpuAllocation;
fn summon<T>() -> T { fn summon<T>() -> T {
unimplemented!() unimplemented!()
} }
let _inner_ref: DeviceObject<vk::Semaphore, &DeviceInner> = DeviceObject::new_debug_named( let _inner_ref: DeviceObject<vk::Semaphore> = DeviceObject::new_debug_named(
summon::<&DeviceInner>(), summon::<Device>(),
summon::<vk::Semaphore>(), summon::<vk::Semaphore>(),
Some("my semaphore"), Some("my semaphore"),
); );
let _device_owned: DeviceObject<vk::Semaphore, super::Device> = let _device_owned: DeviceObject<vk::Semaphore> = DeviceObject::new_debug_named(
DeviceObject::new_debug_named(
summon::<super::Device>(), summon::<super::Device>(),
summon::<vk::Semaphore>(), summon::<vk::Semaphore>(),
Some("my other semaphore"), Some("my other semaphore"),
); );
let _allocation: DeviceObject<GpuAllocation, Arc<super::DeviceInner>> = DeviceObject::new( let _allocation: DeviceObject<GpuAllocation> =
summon::<Arc<super::DeviceInner>>(), DeviceObject::new(summon::<Device>(), summon::<GpuAllocation>());
summon::<GpuAllocation>(),
);
let _pool_owned: ExternallyManagedObject<vk::Semaphore, DevicePools> = let _pool_owned: DeviceObject<vk::Semaphore> =
ExternallyManagedObject::new(summon::<vk::Semaphore>(), summon::<DevicePools>()); DeviceObject::new(summon::<Device>(), summon::<vk::Semaphore>());
} }
} }

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,21 @@ 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 {
pub raw: ash::Instance, pub raw: ash::Instance,
pub(crate) extensions: InstanceExtensions, pub extensions: InstanceExtensions,
pub entry: Entry, pub entry: Entry,
pub(crate) _debug_utils: DebugUtils, pub _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 {
@ -255,28 +254,23 @@ 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 {
surface functor.get_physical_device_surface_formats(pdev, raw)?
.functor
.get_physical_device_surface_formats(pdev, surface.raw)?
}; };
let present_modes = unsafe { let present_modes = unsafe {
surface 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 {
surface functor.get_physical_device_surface_capabilities(pdev, raw)?
.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) =
@ -285,7 +279,7 @@ impl Instance {
{ {
let mut capabilities = vk::SurfaceCapabilities2KHR::default(); let mut capabilities = vk::SurfaceCapabilities2KHR::default();
let mut surface_info = let mut surface_info =
vk::PhysicalDeviceSurfaceInfo2KHR::default().surface(surface.raw()); vk::PhysicalDeviceSurfaceInfo2KHR::default().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,7 +318,7 @@ impl Instance {
}) })
} }
pub(crate) fn expose_adapter( pub fn expose_adapter(
&self, &self,
pdev: vk::PhysicalDevice, pdev: vk::PhysicalDevice,
) -> crate::Result<PhysicalDeviceInfo> { ) -> crate::Result<PhysicalDeviceInfo> {
@ -338,9 +332,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 +447,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 +491,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,626 @@
#![feature(debug_closure_helpers, slice_partition_dedup)]
extern crate self as renderer_device;
use std::{fmt::Debug, path::PathBuf};
use ash::{ext, vk::{self, SurfaceCapabilitiesKHR}};
use dyn_clone::DynClone;
use crate::instance::InstanceInner;
pub mod device;
pub mod instance;
pub mod pipeline_cache;
pub mod queue;
pub mod sync;
mod debug;
pub use device::{Device, DeviceOwned, DeviceRef, Extension};
pub use instance::{DebugUtils, DebugUtilsCreateInfo, Instance};
#[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(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)]
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)]
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(

View file

@ -6,9 +6,9 @@ use std::{
}; };
use crate::device::{ use crate::device::{
DevicePools, Pool, PoolObject, Pooled, Pool, PoolObject, Pooled, Recycle,
asdf::{ asdf::{
DeviceObject, InnerDeviceObject, DeviceObject,
traits::ExternallyManagedObject as ExternallyManagedObjectTrait, traits::ExternallyManagedObject as ExternallyManagedObjectTrait,
}, },
}; };
@ -62,7 +62,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 {
@ -160,17 +160,17 @@ impl SyncThreadpool {
pub enum Fence { pub enum Fence {
Dedicated { Dedicated {
fence: InnerDeviceObject<vk::Fence>, fence: DeviceObject<vk::Fence>,
}, },
Pooled { Pooled {
fence: PoolObject<vk::Fence, Arc<Pool<vk::Fence>>>, fence: PoolObject<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 +179,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 +205,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 +223,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 +276,21 @@ 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.binary_semaphores.push(BinarySemaphore(semaphore)) owner.pools.binary_semaphores.push(BinarySemaphore(semaphore))
} }
SemaphoreInner::Timeline(semaphore) => { SemaphoreInner::Timeline(semaphore) => {
owner.timeline_semaphores.push(TimelineSemaphore(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 +334,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 +377,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 +388,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 +419,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 +432,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 +448,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,6 @@
[package]
name = "renderer_rendergraph"
version = "0.1.0"
edition = "2024"
[dependencies]

View file

@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

View file

@ -0,0 +1,72 @@
# 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 four public resource modules.
- `buffers.rs`: `BufferDesc` and `Buffer`; Vulkan buffer creation, GPU allocation/binding, raw access, and host mapping.
- `images.rs`: `ImageDesc`, `Image`, `ImageViewDesc`, `ImageView`, subresource ranges, extents, offsets, and queue ownership. `ImageInner` distinguishes allocated images from borrowed swapchain images.
- `pipeline.rs`: shader-stage and graphics/compute pipeline descriptions; `DescriptorPool`, `DescriptorSetLayout`, `PipelineLayout`, `Sampler`, `ShaderModule`, and `Pipeline`.
- `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, memory location, allocation strategy, and optional debug name. `Buffer::new` allocates/binds 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 allocator block size.
- `ImageDesc` configures image type, format, extent, layers/mips, samples, tiling, usage, initial layout, queue sharing, and allocation.
- `Image::new` creates an allocated image. `Image::new_with_allocation` accepts an explicit `renderer_device::device::Allocation` and validates size and memory-type compatibility.
- `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` returns raw descriptor-set handles from the provided `DescriptorSetLayout` values and optionally assigns names from `DescriptorSetAllocDesc`.
- `DescriptorSetLayout::new` applies per-binding descriptor flags when the device supports Vulkan 1.2 or `VK_EXT_descriptor_indexing`.
- `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.
- `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.
- `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.
- `DescriptorPool::allocate` returns raw `vk::DescriptorSet` handles; keep the owning pool alive while those sets are in use. Descriptor sets are not wrapped in owning resource objects here.
- 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,13 @@
[package]
name = "renderer_resources"
version = "0.1.0"
edition = "2024"
[dependencies]
renderer_device = { path = "../renderer_device" }
ash = { workspace = true }
gpu-allocator = { workspace = true }
parking_lot = { workspace = true }
bytemuck = { workspace = true }
tracing = { workspace = true }
ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" }

View file

@ -2,6 +2,7 @@ use std::borrow::Cow;
use ash::vk; use ash::vk;
use gpu_allocator::vulkan::AllocationScheme; use gpu_allocator::vulkan::AllocationScheme;
use renderer_device::queue::QueueFamilyIndices;
use crate::{ use crate::{
Device, Device,
@ -49,7 +50,6 @@ impl PartialEq for BufferDesc {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.flags == other.flags self.flags == other.flags
// for hashmaps, `Eq` may be more strict than `Hash` // for hashmaps, `Eq` may be more strict than `Hash`
&& self.name == other.name
&& self.size == other.size && self.size == other.size
&& self.usage == other.usage && self.usage == other.usage
&& self.queue_families == other.queue_families && self.queue_families == other.queue_families
@ -133,7 +133,7 @@ impl Buffer {
let create_info = vk::BufferCreateInfo::default() let create_info = vk::BufferCreateInfo::default()
.size(desc.size) .size(desc.size)
.usage(desc.usage) .usage(desc.usage)
.queue_family_indices(&queue_families) .queue_family_indices(queue_families.as_u32_slice())
.sharing_mode(sharing_mode); .sharing_mode(sharing_mode);
let buffer = unsafe { device.dev().create_buffer(&create_info, None)? }; let buffer = unsafe { device.dev().create_buffer(&create_info, None)? };

View file

@ -6,12 +6,8 @@ use std::{
}; };
use crate::{ use crate::{
device::{ device::{Allocation, AllocationStrategy, DeviceObject, QueueFlags},
Allocation, AllocationStrategy, DeviceInner, DeviceObject, QueueFlags, util::WeakVec,
asdf::traits::ExternallyManagedObject,
},
swapchain::Swapchain,
util::weak_vec::WeakVec,
}; };
use super::Device; use super::Device;
@ -19,6 +15,7 @@ use ash::vk::{self, Handle};
use ash_formats::FormatExt; use ash_formats::FormatExt;
use gpu_allocator::vulkan::{AllocationCreateDesc, AllocationScheme}; use gpu_allocator::vulkan::{AllocationCreateDesc, AllocationScheme};
use parking_lot::Mutex; use parking_lot::Mutex;
use renderer_device::queue::QueueFamilyIndices;
#[derive(Clone)] #[derive(Clone)]
pub struct ImageDesc { pub struct ImageDesc {
@ -76,7 +73,6 @@ impl Eq for ImageDesc {}
impl PartialEq for ImageDesc { impl PartialEq for ImageDesc {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.flags == other.flags self.flags == other.flags
&& self.name == other.name
&& self.format == other.format && self.format == other.format
&& self.kind == other.kind && self.kind == other.kind
&& self.mip_levels == other.mip_levels && self.mip_levels == other.mip_levels
@ -91,7 +87,7 @@ impl PartialEq for ImageDesc {
} }
} }
impl<'a> std::fmt::Debug for ImageDesc { impl std::fmt::Debug for ImageDesc {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ImageDesc") f.debug_struct("ImageDesc")
.field("flags", &self.flags) .field("flags", &self.flags)
@ -139,14 +135,6 @@ enum ImageInner {
Allocated(DeviceObject<vk::Image>, Allocation), Allocated(DeviceObject<vk::Image>, Allocation),
} }
impl<T: AsRef<DeviceInner>> ExternallyManagedObject<T> for vk::Image {
unsafe fn destroy(self, device: &T) {
unsafe {
device.as_ref().raw.destroy_image(self, None);
}
}
}
impl ImageInner { impl ImageInner {
fn image(&self) -> vk::Image { fn image(&self) -> vk::Image {
match self { match self {
@ -264,22 +252,24 @@ impl Image {
Self::new_with_allocation_unchecked(device, image, allocation, desc) Self::new_with_allocation_unchecked(device, image, allocation, desc)
} }
pub fn from_swapchain_image(image: vk::Image, swapchain: &Swapchain) -> Self { pub fn from_swapchain_raw(
image: vk::Image,
device: Device,
format: vk::Format,
extent: vk::Extent3D,
usage: vk::ImageUsageFlags,
) -> Self {
Self { Self {
image: ImageInner::Swapchain(image, swapchain.swapchain.device().clone()), image: ImageInner::Swapchain(image, device),
desc: ImageDesc { desc: ImageDesc {
format: swapchain.config.format, format,
kind: vk::ImageType::TYPE_2D, kind: vk::ImageType::TYPE_2D,
mip_levels: 1, mip_levels: 1,
array_layers: 1, array_layers: 1,
samples: vk::SampleCountFlags::TYPE_1, samples: vk::SampleCountFlags::TYPE_1,
extent: vk::Extent3D { extent,
width: swapchain.config.extent.width,
height: swapchain.config.extent.height,
depth: 1,
},
tiling: vk::ImageTiling::OPTIMAL, tiling: vk::ImageTiling::OPTIMAL,
usage: swapchain.config.usage, usage,
queue_families: QueueFlags::PRESENT, queue_families: QueueFlags::PRESENT,
layout: vk::ImageLayout::UNDEFINED, layout: vk::ImageLayout::UNDEFINED,
mem_location: gpu_allocator::MemoryLocation::GpuOnly, mem_location: gpu_allocator::MemoryLocation::GpuOnly,
@ -327,7 +317,7 @@ impl Image {
.tiling(*tiling) .tiling(*tiling)
.usage(*usage) .usage(*usage)
.sharing_mode(sharing_mode) .sharing_mode(sharing_mode)
.queue_family_indices(&queue_families) .queue_family_indices(queue_families.as_u32_slice())
.array_layers(*array_layers) .array_layers(*array_layers)
.mip_levels(*mip_levels); .mip_levels(*mip_levels);
@ -1017,14 +1007,6 @@ impl ImageView {
} }
} }
impl<T: AsRef<DeviceInner>> ExternallyManagedObject<T> for vk::ImageView {
unsafe fn destroy(self, device: &T) {
unsafe {
device.as_ref().raw.destroy_image_view(self, None);
}
}
}
pub struct QueueOwnership { pub struct QueueOwnership {
pub src: u32, pub src: u32,
pub dst: u32, pub dst: u32,

View file

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

View file

@ -5,7 +5,7 @@ use parking_lot::Mutex;
use crate::{ use crate::{
device::{ device::{
Device, DeviceHandle, DeviceInner, DeviceObject, asdf::traits::ExternallyManagedObject, Device, DeviceObject,
}, },
make_extension, make_extension,
}; };
@ -229,20 +229,6 @@ pub struct DescriptorSetAllocDesc<'a> {
pub layout: &'a DescriptorSetLayout, 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)] #[derive(Debug)]
pub struct DescriptorPool { pub struct DescriptorPool {
pool: DeviceObject<vk::DescriptorPool>, pool: DeviceObject<vk::DescriptorPool>,
@ -300,17 +286,6 @@ impl DescriptorPool {
} }
} }
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)] #[derive(Debug)]
pub struct DescriptorSetLayout { pub struct DescriptorSetLayout {
layout: DeviceObject<vk::DescriptorSetLayout>, layout: DeviceObject<vk::DescriptorSetLayout>,
@ -360,14 +335,6 @@ impl DescriptorSetLayout {
} }
} }
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)] #[derive(Debug)]
pub struct PipelineLayout { pub struct PipelineLayout {
layout: DeviceObject<vk::PipelineLayout>, layout: DeviceObject<vk::PipelineLayout>,
@ -457,17 +424,6 @@ impl std::hash::Hash for SamplerDesc {
} }
} }
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)] #[derive(Debug)]
pub struct Sampler { pub struct Sampler {
sampler: DeviceObject<vk::Sampler>, sampler: DeviceObject<vk::Sampler>,
@ -505,14 +461,6 @@ impl 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)] #[derive(Debug)]
pub struct ShaderModule { pub struct ShaderModule {
module: DeviceObject<vk::ShaderModule>, module: DeviceObject<vk::ShaderModule>,
@ -557,14 +505,6 @@ pub struct Pipeline {
layout: Arc<PipelineLayout>, 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 { impl Pipeline {
pub fn new_compute(device: Device, desc: ComputePipelineDesc) -> crate::Result<Self> { pub fn new_compute(device: Device, desc: ComputePipelineDesc) -> crate::Result<Self> {
let info = &vk::ComputePipelineCreateInfo::default() let info = &vk::ComputePipelineCreateInfo::default()
@ -777,122 +717,3 @@ impl Pipeline {
&self.layout &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

@ -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

@ -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)]