Compare commits
No commits in common. "ad22ff8c76629232c9590ed18f09901d2e2c54e3" and "f7c67f987274bdf49fe3ed73ee7b74c2de3b1760" have entirely different histories.
ad22ff8c76
...
f7c67f9872
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,2 @@
|
||||||
/target/
|
/target/
|
||||||
/Cargo.lock
|
/Cargo.lock
|
||||||
**/pipeline_cache.bin
|
|
||||||
|
|
|
||||||
10
Cargo.toml
10
Cargo.toml
|
|
@ -12,12 +12,11 @@ members = [
|
||||||
"crates/window",
|
"crates/window",
|
||||||
"crates/game",
|
"crates/game",
|
||||||
"crates/text",
|
"crates/text",
|
||||||
"crates/bevy_vulkan_render", "crates/renderer_device", "crates/renderer_resources", "crates/renderer_rendergraph", "crates/datastructures",
|
"crates/bevy_vulkan_render", "crates/renderer_device", "crates/renderer_resources", "crates/renderer_rendergraph",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy_vulkan_render = { path = "crates/bevy_vulkan_render" }
|
bevy_vulkan_render = { path = "crates/bevy_vulkan_render" }
|
||||||
bitflags.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
bevy = { workspace = true }
|
bevy = { workspace = true }
|
||||||
|
|
@ -29,7 +28,6 @@ debug = true
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1.0.89"
|
anyhow = "1.0.89"
|
||||||
criterion = "0.7"
|
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
derive_more = { version = "1.0.0", features = ["deref", "debug", "deref_mut"] }
|
derive_more = { version = "1.0.0", features = ["deref", "debug", "deref_mut"] }
|
||||||
|
|
||||||
|
|
@ -43,15 +41,15 @@ thread_local = "1.1.8"
|
||||||
|
|
||||||
ash = "0.38.0"
|
ash = "0.38.0"
|
||||||
ash-window = "0.13.0"
|
ash-window = "0.13.0"
|
||||||
vk-mem = "0.5.0"
|
# vk-mem = "0.5.0"
|
||||||
|
gpu-allocator = { git = "https://github.com/janis-bhm/gpu-allocator", branch = "main" }
|
||||||
rectangle-pack = "0.4.2"
|
rectangle-pack = "0.4.2"
|
||||||
# vk-sync = "0.1.6"
|
# vk-sync = "0.1.6"
|
||||||
|
|
||||||
arrayvec = "0.7.6"
|
arrayvec = "0.7.6"
|
||||||
tinyvec = { version = "1.8", features = ["alloc"] }
|
tinyvec = "1.8"
|
||||||
indexmap = "2"
|
indexmap = "2"
|
||||||
petgraph = "0.7"
|
petgraph = "0.7"
|
||||||
fixedbitset = "0.5"
|
|
||||||
# itertools = "0.14.0"
|
# itertools = "0.14.0"
|
||||||
ahash = "0.8"
|
ahash = "0.8"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "datastructures"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
|
|
@ -1,471 +0,0 @@
|
||||||
//! A map that stores values over disjoint, half-open intervals.
|
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
/// Operations applied to the covered and uncovered parts of a range.
|
|
||||||
pub trait IntervalUpdateOps<K, V> {
|
|
||||||
/// Updates the value of a subrange that was already present in the map.
|
|
||||||
fn update(&mut self, range: core::range::Range<K>, value: &mut V);
|
|
||||||
|
|
||||||
/// Creates a value for a subrange that was not previously present.
|
|
||||||
fn infill(&mut self, range: core::range::Range<K>) -> V;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K, V, T> IntervalUpdateOps<K, V> for &mut T
|
|
||||||
where
|
|
||||||
T: IntervalUpdateOps<K, V> + ?Sized,
|
|
||||||
{
|
|
||||||
fn update(&mut self, range: core::range::Range<K>, value: &mut V) {
|
|
||||||
(**self).update(range, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn infill(&mut self, range: core::range::Range<K>) -> V {
|
|
||||||
(**self).infill(range)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An interval and its associated value.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct Entry<K, V> {
|
|
||||||
/// The covered interval; its start is always the BTreeMap key.
|
|
||||||
range: core::range::Range<K>,
|
|
||||||
value: V,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A map from disjoint, half-open ranges of ordered keys to values.
|
|
||||||
///
|
|
||||||
/// Stored ranges are pairwise disjoint and sorted by their start key.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct DisjointIntervalMap<K, V> {
|
|
||||||
map: BTreeMap<K, Entry<K, V>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K, V> DisjointIntervalMap<K, V> {
|
|
||||||
/// Creates an empty interval map.
|
|
||||||
pub const fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
map: BTreeMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Applies operations to every existing and missing subrange within `range`.
|
|
||||||
///
|
|
||||||
/// On return, every point in the non-empty range is covered by exactly one
|
|
||||||
/// map entry. Existing values are updated, and missing intervals are
|
|
||||||
/// created through [`IntervalUpdateOps::infill`].
|
|
||||||
pub fn split_apply(
|
|
||||||
&mut self,
|
|
||||||
range: core::range::Range<K>,
|
|
||||||
mut ops: impl IntervalUpdateOps<K, V>,
|
|
||||||
) where
|
|
||||||
K: Ord + Copy,
|
|
||||||
V: Clone,
|
|
||||||
{
|
|
||||||
if range.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.split(range.start);
|
|
||||||
self.split(range.end);
|
|
||||||
|
|
||||||
let mut cursor = range.start;
|
|
||||||
let mut gaps = Vec::new();
|
|
||||||
for entry in self
|
|
||||||
.map
|
|
||||||
.range_mut(range.start..range.end)
|
|
||||||
.map(|(_, entry)| entry)
|
|
||||||
{
|
|
||||||
if cursor < entry.range.start {
|
|
||||||
gaps.push(core::range::Range {
|
|
||||||
start: cursor,
|
|
||||||
end: entry.range.start,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let entry_range = entry.range;
|
|
||||||
ops.update(entry_range, &mut entry.value);
|
|
||||||
cursor = entry_range.end;
|
|
||||||
}
|
|
||||||
|
|
||||||
if cursor < range.end {
|
|
||||||
gaps.push(core::range::Range {
|
|
||||||
start: cursor,
|
|
||||||
end: range.end,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for gap in gaps {
|
|
||||||
let value = ops.infill(gap);
|
|
||||||
self.map.insert(gap.start, Entry { range: gap, value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Inserts `range` unless it is empty or overlaps an existing interval.
|
|
||||||
#[allow(clippy::result_unit_err)]
|
|
||||||
pub fn try_insert(&mut self, range: core::range::Range<K>, value: V) -> Result<(), ()>
|
|
||||||
where
|
|
||||||
K: Ord + Copy,
|
|
||||||
{
|
|
||||||
if range.is_empty() {
|
|
||||||
return Err(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if self
|
|
||||||
.map
|
|
||||||
.range(..range.end)
|
|
||||||
.next_back()
|
|
||||||
.is_some_and(|(_, entry)| entry.range.end > range.start)
|
|
||||||
{
|
|
||||||
return Err(());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.map.insert(range.start, Entry { range, value });
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes all mapped portions covered by `range`.
|
|
||||||
pub fn remove(&mut self, range: core::range::Range<K>)
|
|
||||||
where
|
|
||||||
K: Ord + Copy,
|
|
||||||
V: Clone,
|
|
||||||
{
|
|
||||||
if range.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.split(range.start);
|
|
||||||
self.split(range.end);
|
|
||||||
let keys: Vec<K> = self
|
|
||||||
.map
|
|
||||||
.range(range.start..range.end)
|
|
||||||
.map(|(key, _)| *key)
|
|
||||||
.collect();
|
|
||||||
for key in keys {
|
|
||||||
self.map.remove(&key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Splits the interval containing `pos` into two intervals at `pos`.
|
|
||||||
///
|
|
||||||
/// Does nothing if no interval contains `pos` or an interval already
|
|
||||||
/// starts there.
|
|
||||||
pub fn split(&mut self, pos: K)
|
|
||||||
where
|
|
||||||
K: Ord + Copy,
|
|
||||||
V: Clone,
|
|
||||||
{
|
|
||||||
let Some((key, interval)) = self
|
|
||||||
.map
|
|
||||||
.range_mut(..=pos)
|
|
||||||
.next_back()
|
|
||||||
.map(|(key, entry)| (*key, entry.range))
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if key == pos || pos >= interval.end {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let entry = self.map.remove(&key).expect("candidate entry must exist");
|
|
||||||
let right_value = entry.value.clone();
|
|
||||||
let left_range = core::range::Range {
|
|
||||||
start: key,
|
|
||||||
end: pos,
|
|
||||||
};
|
|
||||||
let right_range = core::range::Range {
|
|
||||||
start: pos,
|
|
||||||
end: entry.range.end,
|
|
||||||
};
|
|
||||||
self.map.insert(
|
|
||||||
key,
|
|
||||||
Entry {
|
|
||||||
range: left_range,
|
|
||||||
value: entry.value,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
self.map.insert(
|
|
||||||
pos,
|
|
||||||
Entry {
|
|
||||||
range: right_range,
|
|
||||||
value: right_value,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the value of the interval containing `pos`, if any.
|
|
||||||
pub fn get(&self, pos: K) -> Option<&V>
|
|
||||||
where
|
|
||||||
K: Ord,
|
|
||||||
{
|
|
||||||
let (_, entry) = self
|
|
||||||
.map
|
|
||||||
.range((std::ops::Bound::Unbounded, std::ops::Bound::Included(&pos)))
|
|
||||||
.next_back()?;
|
|
||||||
(pos < entry.range.end).then_some(&entry.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a mutable reference to the value of the interval containing `pos`.
|
|
||||||
pub fn get_mut(&mut self, pos: K) -> Option<&mut V>
|
|
||||||
where
|
|
||||||
K: Ord,
|
|
||||||
{
|
|
||||||
let (_, entry) = self
|
|
||||||
.map
|
|
||||||
.range_mut((std::ops::Bound::Unbounded, std::ops::Bound::Included(&pos)))
|
|
||||||
.next_back()?;
|
|
||||||
(pos < entry.range.end).then_some(&mut entry.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterates over intervals and their values in start-key order.
|
|
||||||
pub fn iter(&self) -> impl Iterator<Item = (&core::range::Range<K>, &V)> {
|
|
||||||
self.map.values().map(|entry| (&entry.range, &entry.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterates over values in start-key order.
|
|
||||||
pub fn values(&self) -> impl Iterator<Item = &V> {
|
|
||||||
self.map.values().map(|entry| &entry.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterates over intervals in start-key order.
|
|
||||||
pub fn keys(&self) -> impl Iterator<Item = &core::range::Range<K>> {
|
|
||||||
self.map.values().map(|entry| &entry.range)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the number of stored intervals.
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.map.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether the map contains no intervals.
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.map.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes all intervals from the map.
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.map.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K, V> Default for DisjointIntervalMap<K, V> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::{DisjointIntervalMap, IntervalUpdateOps};
|
|
||||||
|
|
||||||
fn r(start: i32, end: i32) -> core::range::Range<i32> {
|
|
||||||
core::range::Range::from(start..end)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct CountingOps {
|
|
||||||
updates: Vec<(i32, i32)>,
|
|
||||||
infills: Vec<(i32, i32)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntervalUpdateOps<i32, i32> for CountingOps {
|
|
||||||
fn update(&mut self, range: core::range::Range<i32>, value: &mut i32) {
|
|
||||||
*value += 1;
|
|
||||||
self.updates.push((range.start, range.end));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn infill(&mut self, range: core::range::Range<i32>) -> i32 {
|
|
||||||
self.infills.push((range.start, range.end));
|
|
||||||
100 + range.start
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn intervals(map: &DisjointIntervalMap<i32, i32>) -> Vec<(i32, i32, i32)> {
|
|
||||||
map.iter()
|
|
||||||
.map(|(range, value)| (range.start, range.end, *value))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn try_insert_overlap_adjacency_and_lookup() {
|
|
||||||
let mut map = DisjointIntervalMap::new();
|
|
||||||
assert!(map.try_insert(r(5, 10), 1).is_ok());
|
|
||||||
assert!(map.try_insert(r(0, 5), 2).is_ok());
|
|
||||||
assert!(map.try_insert(r(10, 15), 3).is_ok());
|
|
||||||
|
|
||||||
for overlap in [r(4, 6), r(9, 12), r(6, 8), r(4, 11)] {
|
|
||||||
assert_eq!(map.try_insert(overlap, 9), Err(()));
|
|
||||||
}
|
|
||||||
assert_eq!(map.try_insert(r(2, 2), 9), Err(()));
|
|
||||||
|
|
||||||
assert_eq!(map.get(0), Some(&2));
|
|
||||||
assert_eq!(map.get(5), Some(&1));
|
|
||||||
assert_eq!(map.get(14), Some(&3));
|
|
||||||
assert_eq!(map.get(15), None);
|
|
||||||
assert_eq!(map.get(-1), None);
|
|
||||||
*map.get_mut(5).unwrap() = 7;
|
|
||||||
assert_eq!(map.get(5), Some(&7));
|
|
||||||
assert!(map.get_mut(15).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn split_boundaries_and_interior() {
|
|
||||||
let mut map = DisjointIntervalMap::new();
|
|
||||||
assert!(map.try_insert(r(0, 10), 4).is_ok());
|
|
||||||
|
|
||||||
map.split(5);
|
|
||||||
assert_eq!(intervals(&map), vec![(0, 5, 4), (5, 10, 4)]);
|
|
||||||
for pos in [0, 10, -1, 11] {
|
|
||||||
let before = intervals(&map);
|
|
||||||
map.split(pos);
|
|
||||||
assert_eq!(intervals(&map), before);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut empty = DisjointIntervalMap::<i32, i32>::new();
|
|
||||||
empty.split(1);
|
|
||||||
assert!(empty.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn remove_exact_partial_spanning_and_noop_ranges() {
|
|
||||||
let mut exact = DisjointIntervalMap::new();
|
|
||||||
exact.try_insert(r(0, 5), 1).unwrap();
|
|
||||||
exact.remove(r(0, 5));
|
|
||||||
assert!(exact.is_empty());
|
|
||||||
|
|
||||||
let mut partial = DisjointIntervalMap::new();
|
|
||||||
partial.try_insert(r(0, 5), 1).unwrap();
|
|
||||||
partial.try_insert(r(5, 10), 2).unwrap();
|
|
||||||
partial.remove(r(3, 7));
|
|
||||||
assert_eq!(intervals(&partial), vec![(0, 3, 1), (7, 10, 2)]);
|
|
||||||
|
|
||||||
partial.remove(r(8, 12));
|
|
||||||
assert_eq!(intervals(&partial), vec![(0, 3, 1), (7, 8, 2)]);
|
|
||||||
|
|
||||||
let mut spanning = DisjointIntervalMap::new();
|
|
||||||
for start in [0, 5, 10] {
|
|
||||||
spanning.try_insert(r(start, start + 5), start).unwrap();
|
|
||||||
}
|
|
||||||
spanning.remove(r(2, 13));
|
|
||||||
assert_eq!(intervals(&spanning), vec![(0, 2, 0), (13, 15, 10)]);
|
|
||||||
|
|
||||||
let before = intervals(&spanning);
|
|
||||||
spanning.remove(r(20, 25));
|
|
||||||
spanning.remove(r(4, 4));
|
|
||||||
assert_eq!(intervals(&spanning), before);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn split_apply_on_empty_and_fully_covered_maps() {
|
|
||||||
let mut empty = DisjointIntervalMap::new();
|
|
||||||
let mut ops = CountingOps::default();
|
|
||||||
empty.split_apply(r(2, 6), &mut ops);
|
|
||||||
assert_eq!(intervals(&empty), vec![(2, 6, 102)]);
|
|
||||||
assert!(ops.updates.is_empty());
|
|
||||||
assert_eq!(ops.infills, vec![(2, 6)]);
|
|
||||||
|
|
||||||
let mut covered = DisjointIntervalMap::new();
|
|
||||||
covered.try_insert(r(0, 10), 5).unwrap();
|
|
||||||
let mut ops = CountingOps::default();
|
|
||||||
covered.split_apply(r(0, 10), &mut ops);
|
|
||||||
assert_eq!(intervals(&covered), vec![(0, 10, 6)]);
|
|
||||||
assert_eq!(ops.updates, vec![(0, 10)]);
|
|
||||||
assert!(ops.infills.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn split_apply_mixed_coverage_splits_and_fills_gaps() {
|
|
||||||
let mut map = DisjointIntervalMap::new();
|
|
||||||
map.try_insert(r(0, 4), 1).unwrap();
|
|
||||||
map.try_insert(r(6, 10), 2).unwrap();
|
|
||||||
let mut ops = CountingOps::default();
|
|
||||||
map.split_apply(r(2, 8), &mut ops);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
intervals(&map),
|
|
||||||
vec![(0, 2, 1), (2, 4, 2), (4, 6, 104), (6, 8, 3), (8, 10, 2)]
|
|
||||||
);
|
|
||||||
assert_eq!(ops.updates, vec![(2, 4), (6, 8)]);
|
|
||||||
assert_eq!(ops.infills, vec![(4, 6)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn split_apply_inside_one_entry_and_empty_range() {
|
|
||||||
let mut map = DisjointIntervalMap::new();
|
|
||||||
map.try_insert(r(0, 10), 7).unwrap();
|
|
||||||
let mut ops = CountingOps::default();
|
|
||||||
map.split_apply(r(3, 5), &mut ops);
|
|
||||||
assert_eq!(intervals(&map), vec![(0, 3, 7), (3, 5, 8), (5, 10, 7)]);
|
|
||||||
assert_eq!(ops.updates, vec![(3, 5)]);
|
|
||||||
assert!(ops.infills.is_empty());
|
|
||||||
|
|
||||||
let before = intervals(&map);
|
|
||||||
let prior_ops = ops.updates.clone();
|
|
||||||
let prior_infills = ops.infills.clone();
|
|
||||||
map.split_apply(r(4, 4), &mut ops);
|
|
||||||
assert_eq!(intervals(&map), before);
|
|
||||||
assert_eq!(ops.updates, prior_ops);
|
|
||||||
assert_eq!(ops.infills, prior_infills);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn split_apply_mutable_ops_accumulate_state() {
|
|
||||||
let mut map = DisjointIntervalMap::new();
|
|
||||||
let mut ops = CountingOps::default();
|
|
||||||
map.split_apply(r(0, 2), &mut ops);
|
|
||||||
map.split_apply(r(4, 6), &mut ops);
|
|
||||||
assert_eq!(ops.infills, vec![(0, 2), (4, 6)]);
|
|
||||||
assert_eq!(map.len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn iteration_and_collection_basics() {
|
|
||||||
let mut map = DisjointIntervalMap::new();
|
|
||||||
assert!(map.is_empty());
|
|
||||||
map.try_insert(r(5, 8), 2).unwrap();
|
|
||||||
map.try_insert(r(0, 3), 1).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(map.len(), 2);
|
|
||||||
assert!(!map.is_empty());
|
|
||||||
assert_eq!(
|
|
||||||
map.iter()
|
|
||||||
.map(|(range, value)| ((range.start, range.end), *value))
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
vec![((0, 3), 1), ((5, 8), 2)]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
map.keys()
|
|
||||||
.map(|range| (range.start, range.end))
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
vec![(0, 3), (5, 8)]
|
|
||||||
);
|
|
||||||
assert_eq!(map.values().copied().collect::<Vec<_>>(), vec![1, 2]);
|
|
||||||
|
|
||||||
map.clear();
|
|
||||||
assert!(map.is_empty());
|
|
||||||
assert_eq!(map.len(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn operations_preserve_disjoint_nonempty_sorted_invariant() {
|
|
||||||
let mut map = DisjointIntervalMap::new();
|
|
||||||
map.try_insert(r(0, 4), 1).unwrap();
|
|
||||||
map.try_insert(r(6, 10), 2).unwrap();
|
|
||||||
map.split(2);
|
|
||||||
map.remove(r(3, 7));
|
|
||||||
let mut ops = CountingOps::default();
|
|
||||||
map.split_apply(r(-2, 12), &mut ops);
|
|
||||||
map.try_insert(r(20, 24), 3).unwrap();
|
|
||||||
map.split(22);
|
|
||||||
map.remove(r(1, 2));
|
|
||||||
|
|
||||||
let ranges = map.keys().collect::<Vec<_>>();
|
|
||||||
for range in &ranges {
|
|
||||||
assert!(!range.is_empty());
|
|
||||||
}
|
|
||||||
for pair in ranges.windows(2) {
|
|
||||||
assert!(pair[0].start < pair[1].start);
|
|
||||||
assert!(pair[0].end <= pair[1].start);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
- `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.
|
- `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.
|
- `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.
|
- Resource types are defined in `renderer_resources`: `Buffer`/`BufferDesc`; `Image`/`ImageDesc`, `ImageView`/`ImageViewDesc`, `MipRange`, `Extent`, and `Offset`; and pipeline, descriptor, shader, and sampler types.
|
||||||
- Buffer/image descriptors carry Vulkan usage, `renderer_device::device::MemoryLocation`, queue-sharing flags, and allocation strategy; `Buffer::map`/`map_mut` expose mapped memory for VMA-mapped allocations (CPU-mapped locations require host-visible, host-coherent memory).
|
- 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.
|
- 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.
|
- `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.
|
- `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.
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
- 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`.
|
- 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.
|
- `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`).
|
- `ImageView` retains an `Arc<Image>` so its source image outlives the Vulkan view. `Image::create_view` in `renderer_resources` returns `ManuallyDrop<ImageView>`; renderer call sites transfer it deliberately (the swapchain path uses `ManuallyDrop::into_inner`).
|
||||||
- For allocated images, `Allocation::Owned` frees VMA allocation memory with its `DeviceObject`; `Allocation::Shared` permits shared ownership and only exposes mutable access when uniquely held; `Unmanaged` carries no allocation.
|
- 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.
|
- `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.
|
- `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.
|
- `Surface` uses a `DropGuard` for surface destruction and keeps its `Instance`; field order drops the swapchain lock first, then the guard, then the instance.
|
||||||
|
|
@ -89,7 +89,7 @@
|
||||||
- The crate root also exposes `Extent2D` and `utils` alongside renderer modules and device re-exports.
|
- 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.
|
- `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`.
|
- `Pipeline::new_graphics` supports dynamic rendering state through `RenderingState`; its render-pass and rendering choices are supplied in `GraphicsPipelineDesc`.
|
||||||
- `Buffer` and `Image` creation in `renderer_resources` allocates/binds through `DeviceInner::allocate_for_buffer` / `allocate_for_image` and `bind_buffer_memory` / `bind_image_memory`; device ownership and final VMA allocation release are handled by the device-layer wrappers.
|
- `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.
|
- 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.
|
- `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.
|
- `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.
|
||||||
|
|
@ -123,7 +123,7 @@
|
||||||
- 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 `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`.
|
- 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.
|
- `weak_vec` moved to `renderer_resources::util` and is not re-exported by `renderer`; it prunes dead weak references on capacity pressure. Image-side view tracking should remain weak to avoid ownership cycles.
|
||||||
- Render graph IDs are distinct: legacy `GraphResourceId` wraps `u32`; the newer internal `resources::ResourceId` also wraps `u32` (constructed via `ResourceId::from_u32`).
|
- 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.
|
- 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.
|
- `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.
|
- The supported surface capability extension query can filter present modes based on the selected mode; its behavior depends on optional `InstanceExtensions` support.
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ tracing = { workspace = true }
|
||||||
ash = { workspace = true }
|
ash = { workspace = true }
|
||||||
ash-window = { workspace = true }
|
ash-window = { workspace = true }
|
||||||
# vk-mem = { workspace = true }
|
# vk-mem = { workspace = true }
|
||||||
|
gpu-allocator = { workspace = true }
|
||||||
rectangle-pack = { workspace = true }
|
rectangle-pack = { workspace = true }
|
||||||
|
|
||||||
raw-window-handle = { workspace = true }
|
raw-window-handle = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,14 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
|
use gpu_allocator::MemoryLocation;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
EguiState,
|
EguiState,
|
||||||
buffers::{Buffer, BufferDesc},
|
buffers::{Buffer, BufferDesc},
|
||||||
commands::traits::CommandBufferExt,
|
commands::traits::CommandBufferExt,
|
||||||
device::{self, AllocationStrategy, MemoryLocation},
|
device::{self, AllocationStrategy},
|
||||||
images::{Image, ImageDesc, ImageViewDesc},
|
images::{Image, ImageDesc, ImageViewDesc},
|
||||||
render_graph::{
|
render_graph::{
|
||||||
Access, Barrier, GraphResourceDesc, GraphResourceId, PassDesc, RecordFn, RenderContext,
|
Access, Barrier, GraphResourceDesc, GraphResourceId, PassDesc, RecordFn, RenderContext,
|
||||||
|
|
@ -571,7 +572,7 @@ pub fn egui_pass(
|
||||||
.values()
|
.values()
|
||||||
.map(|entry| {
|
.map(|entry| {
|
||||||
let texture = texture_handler.get_texture(entry.id).unwrap();
|
let texture = texture_handler.get_texture(entry.id).unwrap();
|
||||||
let view = Arc::new(
|
let view = Arc::new(ManuallyDrop::into_inner(
|
||||||
texture
|
texture
|
||||||
.create_view(
|
.create_view(
|
||||||
ImageViewDesc::color_2d()
|
ImageViewDesc::color_2d()
|
||||||
|
|
@ -579,7 +580,7 @@ pub fn egui_pass(
|
||||||
.with_layer_range(0..1),
|
.with_layer_range(0..1),
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
));
|
||||||
let sampler = samplers.get_sampler_owned(entry.as_sampler_desc()).unwrap();
|
let sampler = samplers.get_sampler_owned(entry.as_sampler_desc()).unwrap();
|
||||||
(view, sampler)
|
(view, sampler)
|
||||||
})
|
})
|
||||||
|
|
@ -770,7 +771,7 @@ pub fn egui_pass(
|
||||||
|
|
||||||
let color_attachment = &vk::RenderingAttachmentInfo::default()
|
let color_attachment = &vk::RenderingAttachmentInfo::default()
|
||||||
.image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
.image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
||||||
.image_view(ManuallyDrop::new(target.create_view(ImageViewDesc::color_2d())?).raw())
|
.image_view(target.create_view(ImageViewDesc::color_2d())?.raw())
|
||||||
.load_op(vk::AttachmentLoadOp::LOAD)
|
.load_op(vk::AttachmentLoadOp::LOAD)
|
||||||
.store_op(vk::AttachmentStoreOp::STORE);
|
.store_op(vk::AttachmentStoreOp::STORE);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,7 @@ use ash::vk;
|
||||||
use derive_more::derive::{Deref, DerefMut};
|
use derive_more::derive::{Deref, DerefMut};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
buffers,
|
buffers, images,
|
||||||
device::MemoryLocation,
|
|
||||||
images,
|
|
||||||
render_graph::{
|
render_graph::{
|
||||||
recorder::CommandList,
|
recorder::CommandList,
|
||||||
resources::{self, ResourceId},
|
resources::{self, ResourceId},
|
||||||
|
|
@ -50,7 +48,7 @@ impl<'r> GraphBuilder<'r> {
|
||||||
&self,
|
&self,
|
||||||
_size: u64,
|
_size: u64,
|
||||||
_usage: vk::BufferUsageFlags,
|
_usage: vk::BufferUsageFlags,
|
||||||
_location: MemoryLocation,
|
_location: gpu_allocator::MemoryLocation,
|
||||||
) -> Buffer<'r> {
|
) -> Buffer<'r> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
@ -59,7 +57,7 @@ impl<'r> GraphBuilder<'r> {
|
||||||
&self,
|
&self,
|
||||||
_size: u64,
|
_size: u64,
|
||||||
_usage: vk::BufferUsageFlags,
|
_usage: vk::BufferUsageFlags,
|
||||||
_location: MemoryLocation,
|
_location: gpu_allocator::MemoryLocation,
|
||||||
) -> Buffer<'r> {
|
) -> Buffer<'r> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
@ -68,7 +66,7 @@ impl<'r> GraphBuilder<'r> {
|
||||||
&self,
|
&self,
|
||||||
_size: u64,
|
_size: u64,
|
||||||
_usage: vk::BufferUsageFlags,
|
_usage: vk::BufferUsageFlags,
|
||||||
_location: MemoryLocation,
|
_location: gpu_allocator::MemoryLocation,
|
||||||
) -> Buffer<'r> {
|
) -> Buffer<'r> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
@ -86,6 +84,7 @@ mod api_demo {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use ash::vk::{self, BufferUsageFlags};
|
use ash::vk::{self, BufferUsageFlags};
|
||||||
|
use gpu_allocator::MemoryLocation;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
|
||||||
fn egui_graph<'a>(
|
fn egui_graph<'a>(
|
||||||
|
|
|
||||||
|
|
@ -228,17 +228,7 @@ mod impls {
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
pub struct ResourceId(u32);
|
pub struct ResourceId(pub u64);
|
||||||
|
|
||||||
impl ResourceId {
|
|
||||||
pub fn from_u32(index: u32) -> Self {
|
|
||||||
Self(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn index(self) -> usize {
|
|
||||||
self.0 as usize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum ResourceRange {
|
pub enum ResourceRange {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
use std::{mem::ManuallyDrop, sync::Arc};
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use crate::egui_pass::{egui_pass, egui_pre_pass};
|
|
||||||
|
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
use glam::{
|
use glam::{
|
||||||
camera::rh::{proj, view},
|
camera::rh::{proj, view},
|
||||||
vec3,
|
vec3,
|
||||||
};
|
};
|
||||||
|
use gpu_allocator::MemoryLocation;
|
||||||
|
|
||||||
|
pub use crate::egui_pass::{egui_pass, egui_pre_pass};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Result,
|
Result,
|
||||||
buffers::{Buffer, BufferDesc},
|
buffers::{Buffer, BufferDesc},
|
||||||
commands::{self, traits::CommandBufferExt},
|
commands::{self, traits::CommandBufferExt},
|
||||||
device::{Device, MemoryLocation},
|
device::Device,
|
||||||
images::ImageViewDesc,
|
images::ImageViewDesc,
|
||||||
pipeline,
|
pipeline,
|
||||||
render_graph::{
|
render_graph::{
|
||||||
|
|
@ -329,9 +330,7 @@ impl Wireframe {
|
||||||
|
|
||||||
let color_attachment = &vk::RenderingAttachmentInfo::default()
|
let color_attachment = &vk::RenderingAttachmentInfo::default()
|
||||||
.image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
.image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
||||||
.image_view(
|
.image_view(target.create_view(ImageViewDesc::color_2d())?.raw())
|
||||||
ManuallyDrop::new(target.create_view(ImageViewDesc::color_2d())?).raw(),
|
|
||||||
)
|
|
||||||
.load_op(vk::AttachmentLoadOp::LOAD)
|
.load_op(vk::AttachmentLoadOp::LOAD)
|
||||||
.store_op(vk::AttachmentStoreOp::STORE);
|
.store_op(vk::AttachmentStoreOp::STORE);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
mem::ManuallyDrop,
|
||||||
num::NonZero,
|
num::NonZero,
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
sync::{
|
sync::{
|
||||||
|
|
@ -588,7 +589,7 @@ impl Swapchain {
|
||||||
SwapchainImage {
|
SwapchainImage {
|
||||||
index: idx as u32,
|
index: idx as u32,
|
||||||
swapchain: self,
|
swapchain: self,
|
||||||
view,
|
view: ManuallyDrop::into_inner(view),
|
||||||
acquire,
|
acquire,
|
||||||
release,
|
release,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,11 @@
|
||||||
- `sync.rs`: fence/semaphore wrappers and pools, `FenceFuture`, and background fence waiters.
|
- `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.
|
- `pipeline_cache.rs`: keyed Vulkan pipeline cache with `pipeline_cache.bin` load/export.
|
||||||
- `debug.rs`: Vulkan debug callback forwarding messages to tracing.
|
- `debug.rs`: Vulkan debug callback forwarding messages to tracing.
|
||||||
- `testing.rs` (feature `testing`): `TestVulkan` creates real validation-enabled Vulkan devices and captures validation messages for smoke/integration tests.
|
|
||||||
- `renderer_macros/src/lib.rs`: separate workspace proc-macro source; derives `DeviceRef` for a struct field marked `#[device]`.
|
- `renderer_macros/src/lib.rs`: separate workspace proc-macro source; derives `DeviceRef` for a struct field marked `#[device]`.
|
||||||
|
|
||||||
## Key types and ownership
|
## Key types and ownership
|
||||||
- `Device(Arc<DeviceInner>)` is the cloneable owning handle. It dereferences to `DeviceInner`; equality is `Arc::ptr_eq`.
|
- `Device(Arc<DeviceInner>)` is the cloneable owning handle. It dereferences to `DeviceInner`; equality is `Arc::ptr_eq`.
|
||||||
- `DeviceInner` keeps the raw `ash::Device`, VMA (`vk_mem`) allocator mutex, adapter and instance, queues, synchronization threadpool, extension loaders, and inline `DevicePools`.
|
- `DeviceInner` keeps the raw `ash::Device`, allocator mutex, adapter and instance, queues, synchronization threadpool, extension loaders, and inline `DevicePools`.
|
||||||
- `Device::enabled_features()` returns the feature snapshot requested for logical-device creation; it is distinct from `Device::features()`, which reports adapter-supported features. Descriptor capability validation must use the enabled snapshot, not support data.
|
- `Device::enabled_features()` returns the feature snapshot requested for logical-device creation; it is distinct from `Device::features()`, which reports adapter-supported features. Descriptor capability validation must use the enabled snapshot, not support data.
|
||||||
- `PhysicalDeviceInfo::create_logical_device` selects queues, enables requested/supported extensions and features, initializes the allocator/cache, and constructs `DeviceInner` with `Arc::new_cyclic`.
|
- `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.
|
- `DeviceInner::dev`, `instance`, `queues`, `physical_device`, `main_queue`, `compute_queue`, and `transfer_queue` are the main convenience accessors for downstream code.
|
||||||
|
|
@ -27,19 +26,17 @@
|
||||||
- The `DeviceRef` proc macro is implemented in the separate `renderer_macros` package and emits paths rooted at `::renderer_device::`.
|
- 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.
|
- `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.
|
- `PhysicalDeviceInfo`, `PhysicalDeviceFeatures`, `PhysicalDeviceProperties`, and `SurfaceCapabilities` carry adapter/capability data. `Instance::choose_adapter_default` accepts `Option<vk::SurfaceKHR>` (a raw handle), not a swapchain/surface wrapper.
|
||||||
- `Error` and `Result<T>` are the crate-wide error types; `Error` covers Vulkan/VMA (`vk_mem`), loading, I/O, and renderer-facing resource/capability failures.
|
- `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.
|
- `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.
|
- `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.
|
- `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.
|
- `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.
|
- `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.
|
- `DeviceExtensions` holds the debug-utils device loader and optional swapchain/mesh-shader loaders. `DeviceInner.enabled_extensions` records enabled extension names.
|
||||||
- `MemoryLocation` is the public allocator-independent memory preference; `AllocationDesc` combines it with a resource name and `AllocationStrategy` (`AllocatorManaged` or `Dedicated`). `GpuAllocation` wraps the private `vk_mem::Allocation` and retains memory properties needed by callers.
|
- `AllocationStrategy` selects allocator-managed blocks or a dedicated allocation. `Allocation` represents `Owned`, `Shared`, or `Unmanaged` GPU allocations.
|
||||||
- `DeviceInner::allocate_for_buffer` / `allocate_for_image` obtain VMA allocations, and `bind_buffer_memory` / `bind_image_memory` bind them to Vulkan resources without exposing `vk_mem` types to upper layers. CPU-mapped locations require host-visible, host-coherent memory.
|
|
||||||
- `Allocation` represents `Owned`, `Shared`, or `Unmanaged` GPU allocations.
|
|
||||||
- `get_extensions` returns supported/enabled and unsupported requested extension sets; `get_layers` returns available requested layers or the supported/unsupported split.
|
- `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.
|
- `Queue` shares a `QueueInner` through `Arc`; `with_locked` serializes operations on the Vulkan queue. `QueueFamily` records index, count, flags, granularity, and timestamp bits.
|
||||||
- `DeviceQueues` provides graphics, compute, and transfer queues; absent compute/transfer queues alias the graphics queue. Queue selection requires presentation support only when a display handle is provided.
|
- `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.
|
- `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.
|
- `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.
|
- `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.
|
||||||
|
|
@ -48,7 +45,6 @@
|
||||||
- Pooled timeline semaphores are created at value zero; `Semaphore::from_pool` signals the requested initial value when it obtains one.
|
- 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.
|
- `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.
|
- `DebugName` is the wrapper for Vulkan object debug names.
|
||||||
- `testing::TestVulkan::acquire` returns a real Vulkan test device or skips with a reason; `expect` requires Vulkan. `take_messages`, `assert_no_validation_errors`, and `assert_no_warnings` inspect the captured debug-utils callback output. Run the device smoke tests using `direnv exec . cargo test -p renderer_device --features testing` from the workspace root.
|
|
||||||
- `Extension` is the name/version pair used by extension helpers and adapter creation. `make_extension!`, `get_extensions`, `get_layers`, and the support/intersection functions centralize Vulkan extension discovery.
|
- `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.
|
- `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.
|
- `DebugUtilsCreateInfo` is the small severity/message-type configuration record used with debug utilities; `DebugUtils` itself owns the instance loader and messenger handle.
|
||||||
|
|
@ -81,7 +77,7 @@
|
||||||
- `queue::QueueFlags` and `device::QueueFlags` intentionally serve different purposes. Keep imports qualified when adding queue-family selection logic.
|
- `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.
|
- `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.
|
- Logical-device setup always includes the swapchain extension in its required extension list, regardless of the caller's extra requested extensions.
|
||||||
- Queue-family selection uses the optional display handle to query presentation support and requires its chosen graphics family to be present-capable only when a display handle is provided.
|
- 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.
|
- 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.
|
- `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.
|
- `DebugUtils` is stored inside `InstanceInner`; its destructor destroys the messenger, so retain the loader/messenger for the instance lifetime.
|
||||||
|
|
@ -96,7 +92,7 @@
|
||||||
- `Device::PartialEq` compares `Arc` identity, not raw Vulkan handles or physical-device identity.
|
- `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.
|
- `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.
|
- In debug builds `DebugName` stores its string; in non-debug builds its dereference returns a placeholder and does not retain the supplied name. `debug_name_object` assigns names through debug utils.
|
||||||
- GPU allocation free is routed through `DeviceObject<GpuAllocation>` into the VMA allocator mutex with `free_memory`. Do not call allocator free on an allocation after ownership has moved or dropped.
|
- 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.
|
- `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.
|
- Device handle types are hand-written around `DeviceObject` with `DeviceOwned` implementations where needed; the old `define_device_owned_handle!` macro no longer exists.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,10 @@ name = "renderer_device"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
|
||||||
testing = []
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ash = { workspace = true }
|
ash = { workspace = true }
|
||||||
ash-window = { workspace = true }
|
ash-window = { workspace = true }
|
||||||
vk-mem = { workspace = true }
|
gpu-allocator = { workspace = true }
|
||||||
parking_lot = { workspace = true }
|
parking_lot = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
raw-window-handle = { workspace = true }
|
raw-window-handle = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@ use std::{
|
||||||
collections::{BTreeSet, HashMap, HashSet},
|
collections::{BTreeSet, HashMap, HashSet},
|
||||||
ffi::CStr,
|
ffi::CStr,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
ptr::NonNull,
|
sync::{Arc, Weak},
|
||||||
sync::{Arc, OnceLock, Weak},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use ash::{ext, khr, prelude::VkResult, vk};
|
use ash::{ext, khr, prelude::VkResult, vk};
|
||||||
|
|
@ -67,7 +66,7 @@ impl From<String> for DebugName {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
use crate::{
|
use crate::{
|
||||||
Error, Instance, PhysicalDeviceFeatures, PhysicalDeviceInfo, Result,
|
Instance, PhysicalDeviceFeatures, PhysicalDeviceInfo, Result,
|
||||||
device::asdf::traits::ExternallyManagedObject,
|
device::asdf::traits::ExternallyManagedObject,
|
||||||
pipeline_cache::PipelineCache,
|
pipeline_cache::PipelineCache,
|
||||||
queue::{DeviceQueueInfos, DeviceQueues, Queue},
|
queue::{DeviceQueueInfos, DeviceQueues, Queue},
|
||||||
|
|
@ -109,89 +108,12 @@ pub struct DeviceExtensions {
|
||||||
pub mesh_shader: Option<ext::mesh_shader::Device>,
|
pub mesh_shader: Option<ext::mesh_shader::Device>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
type GpuAllocation = gpu_allocator::vulkan::Allocation;
|
||||||
pub enum MemoryLocation {
|
|
||||||
#[default]
|
|
||||||
Unknown,
|
|
||||||
GpuOnly,
|
|
||||||
CpuToGpu,
|
|
||||||
GpuToCpu,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
struct SendSyncPtr(NonNull<u8>);
|
|
||||||
|
|
||||||
unsafe impl Send for SendSyncPtr {}
|
|
||||||
unsafe impl Sync for SendSyncPtr {}
|
|
||||||
|
|
||||||
impl core::fmt::Debug for SendSyncPtr {
|
|
||||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
||||||
f.debug_tuple("SendSyncPtr").field(&self.0).finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct GpuAllocation {
|
|
||||||
raw: vk_mem::Allocation,
|
|
||||||
memory: vk::DeviceMemory,
|
|
||||||
offset: u64,
|
|
||||||
size: u64,
|
|
||||||
memory_type_index: u32,
|
|
||||||
mapped: Option<SendSyncPtr>,
|
|
||||||
dedicated_memory: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl Send for GpuAllocation {}
|
|
||||||
unsafe impl Sync for GpuAllocation {}
|
|
||||||
|
|
||||||
impl core::fmt::Debug for GpuAllocation {
|
|
||||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
||||||
f.debug_struct("GpuAllocation")
|
|
||||||
.field("memory", &self.memory)
|
|
||||||
.field("offset", &self.offset)
|
|
||||||
.field("size", &self.size)
|
|
||||||
.field("memory_type_index", &self.memory_type_index)
|
|
||||||
.field("mapped", &self.mapped)
|
|
||||||
.field("dedicated_memory", &self.dedicated_memory)
|
|
||||||
.finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GpuAllocation {
|
|
||||||
pub fn memory(&self) -> vk::DeviceMemory {
|
|
||||||
self.memory
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn offset(&self) -> u64 {
|
|
||||||
self.offset
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn size(&self) -> u64 {
|
|
||||||
self.size
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn memory_type_index(&self) -> u32 {
|
|
||||||
self.memory_type_index
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_dedicated(&self) -> bool {
|
|
||||||
self.dedicated_memory
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mapped_slice(&self) -> Option<&[u8]> {
|
|
||||||
let mapped = self.mapped?;
|
|
||||||
Some(unsafe { core::slice::from_raw_parts(mapped.0.as_ptr(), self.size as usize) })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mapped_slice_mut(&mut self) -> Option<&mut [u8]> {
|
|
||||||
let mapped = self.mapped?;
|
|
||||||
Some(unsafe { core::slice::from_raw_parts_mut(mapped.0.as_ptr(), self.size as usize) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum AllocationStrategy {
|
pub enum AllocationStrategy {
|
||||||
#[default]
|
#[default]
|
||||||
/// Let the allocator manage the memory for this allocation, sub-allocating
|
/// Let gpu_allocator manage the memory for this allocation, sub-allocating
|
||||||
/// from larger blocks as needed.
|
/// from larger blocks as needed.
|
||||||
AllocatorManaged,
|
AllocatorManaged,
|
||||||
/// Allocate a dedicated block of memory for this allocation. This is
|
/// Allocate a dedicated block of memory for this allocation. This is
|
||||||
|
|
@ -200,84 +122,11 @@ pub enum AllocationStrategy {
|
||||||
Dedicated,
|
Dedicated,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AllocationDesc<'a> {
|
|
||||||
pub name: &'a str,
|
|
||||||
pub location: MemoryLocation,
|
|
||||||
pub strategy: AllocationStrategy,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Vulkan memory requirements for a buffer or image.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct MemoryRequirements {
|
|
||||||
pub size: vk::DeviceSize,
|
|
||||||
pub alignment: vk::DeviceSize,
|
|
||||||
pub memory_type_bits: u32,
|
|
||||||
pub requires_dedicated_allocation: bool,
|
|
||||||
pub prefers_dedicated_allocation: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A buffer or image to bind into a shared allocation at an allocation-local offset.
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum MemoryBinding {
|
|
||||||
Buffer {
|
|
||||||
buffer: vk::Buffer,
|
|
||||||
offset: vk::DeviceSize,
|
|
||||||
requirements: MemoryRequirements,
|
|
||||||
},
|
|
||||||
Image {
|
|
||||||
image: vk::Image,
|
|
||||||
offset: vk::DeviceSize,
|
|
||||||
requirements: MemoryRequirements,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A failed grouped bind and the number of earlier members that remain bound.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct GroupBindError {
|
|
||||||
pub error: crate::Error,
|
|
||||||
pub bound_count: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the first memory type allowed by `memory_type_bits` with all requested properties.
|
|
||||||
fn find_memory_type_index(
|
|
||||||
memory_properties: &vk::PhysicalDeviceMemoryProperties,
|
|
||||||
memory_type_bits: u32,
|
|
||||||
property_flags: vk::MemoryPropertyFlags,
|
|
||||||
) -> Result<u32> {
|
|
||||||
(0..memory_properties.memory_type_count)
|
|
||||||
.find(|&index| {
|
|
||||||
memory_type_bits & (1 << index) != 0
|
|
||||||
&& memory_properties.memory_types[index as usize]
|
|
||||||
.property_flags
|
|
||||||
.contains(property_flags)
|
|
||||||
})
|
|
||||||
.ok_or(Error::NoCompatibleMemoryType {
|
|
||||||
memory_type_bits,
|
|
||||||
property_flags,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<vk::MemoryRequirements> for MemoryRequirements {
|
|
||||||
fn from(requirements: vk::MemoryRequirements) -> Self {
|
|
||||||
Self {
|
|
||||||
size: requirements.size,
|
|
||||||
alignment: requirements.alignment,
|
|
||||||
memory_type_bits: requirements.memory_type_bits,
|
|
||||||
requires_dedicated_allocation: false,
|
|
||||||
prefers_dedicated_allocation: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Allocation {
|
pub enum Allocation {
|
||||||
Owned(DeviceObject<GpuAllocation>),
|
Owned(DeviceObject<GpuAllocation>),
|
||||||
Shared(Arc<DeviceObject<GpuAllocation>>),
|
Shared(Arc<DeviceObject<GpuAllocation>>),
|
||||||
/// An unbound resource. Requirements are a pure function of its Vulkan handle, so
|
Unmanaged,
|
||||||
/// resources can safely cache the first query here for their lifetime.
|
|
||||||
Unmanaged {
|
|
||||||
requirements: OnceLock<MemoryRequirements>,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Allocation {
|
impl Allocation {
|
||||||
|
|
@ -285,7 +134,7 @@ impl Allocation {
|
||||||
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()),
|
||||||
Allocation::Unmanaged { .. } => None,
|
Allocation::Unmanaged => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -293,13 +142,13 @@ impl Allocation {
|
||||||
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),
|
||||||
Allocation::Unmanaged { .. } => None,
|
Allocation::Unmanaged => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DeviceInner {
|
pub struct DeviceInner {
|
||||||
pub allocator: Mutex<vk_mem::Allocator>,
|
pub alloc2: Mutex<gpu_allocator::vulkan::Allocator>,
|
||||||
pub raw: ash::Device,
|
pub raw: ash::Device,
|
||||||
pub adapter: PhysicalDeviceInfo,
|
pub adapter: PhysicalDeviceInfo,
|
||||||
pub instance: Instance,
|
pub instance: Instance,
|
||||||
|
|
@ -589,32 +438,25 @@ impl PhysicalDeviceInfo {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut allocator_info =
|
let alloc2 =
|
||||||
vk_mem::AllocatorCreateInfo::new(&instance.inner.raw, &device, self.pdev);
|
gpu_allocator::vulkan::Allocator::new(&gpu_allocator::vulkan::AllocatorCreateDesc {
|
||||||
|
instance: instance.inner.raw.clone(),
|
||||||
allocator_info.preferred_large_heap_block_size = 64 * 1024 * 1024;
|
device: device.clone(),
|
||||||
allocator_info.vulkan_api_version = instance
|
physical_device: self.pdev,
|
||||||
.inner
|
debug_settings: Default::default(),
|
||||||
.api_version
|
buffer_device_address: false,
|
||||||
.min(self.properties.device_api_version);
|
allocation_sizes: {
|
||||||
|
const MB: u64 = 1024 * 1024;
|
||||||
if allocator_info.vulkan_api_version < vk::API_VERSION_1_1 {
|
gpu_allocator::AllocationSizes::new(8 * MB, 64 * MB)
|
||||||
if enabled_extensions.contains(&khr::dedicated_allocation::NAME)
|
.with_max_host_memblock_size(256 * MB)
|
||||||
&& enabled_extensions.contains(&khr::get_memory_requirements2::NAME)
|
.with_max_device_memblock_size(256 * MB)
|
||||||
{
|
},
|
||||||
allocator_info.flags |= vk_mem::AllocatorCreateFlags::KHR_DEDICATED_ALLOCATION;
|
})?;
|
||||||
}
|
|
||||||
if enabled_extensions.contains(&khr::bind_memory2::NAME) {
|
|
||||||
allocator_info.flags |= vk_mem::AllocatorCreateFlags::KHR_BIND_MEMORY2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let allocator = unsafe { vk_mem::Allocator::new(allocator_info) }?;
|
|
||||||
|
|
||||||
let pipeline_cache = PipelineCache::new(&device, &self).unwrap();
|
let pipeline_cache = PipelineCache::new(&device, &self).unwrap();
|
||||||
let shared = Arc::new_cyclic(|inner: &Weak<DeviceInner>| DeviceInner {
|
let shared = Arc::new_cyclic(|inner: &Weak<DeviceInner>| DeviceInner {
|
||||||
raw: device.clone(),
|
raw: device.clone(),
|
||||||
allocator: Mutex::new(allocator),
|
alloc2: Mutex::new(alloc2),
|
||||||
instance: instance.clone(),
|
instance: instance.clone(),
|
||||||
adapter: self,
|
adapter: self,
|
||||||
queues: device_queues,
|
queues: device_queues,
|
||||||
|
|
@ -736,495 +578,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Device {
|
|
||||||
/// Allocates aliasable memory compatible with `requirements` and `property_flags`.
|
|
||||||
///
|
|
||||||
/// The returned `Arc` is an allocation handle intended to be transferred into each bound
|
|
||||||
/// resource's `Allocation::Shared`. Binding consumes one Arc reference; the allocation is
|
|
||||||
/// freed by its `DeviceObject` when the last bound resource drops. This helper does not
|
|
||||||
/// retain a reference after a successful bind.
|
|
||||||
pub fn allocate_memory(
|
|
||||||
&self,
|
|
||||||
requirements: &MemoryRequirements,
|
|
||||||
property_flags: vk::MemoryPropertyFlags,
|
|
||||||
) -> Result<Arc<DeviceObject<GpuAllocation>>> {
|
|
||||||
let allocation = self
|
|
||||||
.0
|
|
||||||
.allocate_aliasable_memory(requirements, property_flags)?;
|
|
||||||
Ok(Arc::new(DeviceObject::new(self.clone(), allocation)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DeviceInner {
|
impl DeviceInner {
|
||||||
fn api_version(&self) -> u32 {
|
|
||||||
self.instance
|
|
||||||
.inner
|
|
||||||
.api_version
|
|
||||||
.min(self.adapter.properties.device_api_version)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_enabled_extension(&self, name: &CStr) -> bool {
|
|
||||||
self.enabled_extensions.contains(&name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_memory2_extension_set(&self) -> bool {
|
|
||||||
self.has_enabled_extension(khr::get_memory_requirements2::NAME)
|
|
||||||
&& self.has_enabled_extension(khr::bind_memory2::NAME)
|
|
||||||
&& self.has_enabled_extension(khr::dedicated_allocation::NAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn require_offset_binding_support(&self) -> Result<()> {
|
|
||||||
if self.api_version() < vk::API_VERSION_1_1 && !self.has_memory2_extension_set() {
|
|
||||||
return Err(Error::OffsetBindingRequiresApi11);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Finds a compatible memory type satisfying `property_flags`.
|
|
||||||
pub fn find_memory_type_index(
|
|
||||||
&self,
|
|
||||||
requirements: &MemoryRequirements,
|
|
||||||
property_flags: vk::MemoryPropertyFlags,
|
|
||||||
) -> Result<u32> {
|
|
||||||
find_memory_type_index(
|
|
||||||
&self.adapter.properties.memory_properties,
|
|
||||||
requirements.memory_type_bits,
|
|
||||||
property_flags,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Queries buffer memory requirements, including dedicated-allocation hints when available.
|
|
||||||
pub fn buffer_memory_requirements(&self, buffer: vk::Buffer) -> Result<MemoryRequirements> {
|
|
||||||
let api11 = self.api_version() >= vk::API_VERSION_1_1;
|
|
||||||
|
|
||||||
let get_requirements2 =
|
|
||||||
api11 || self.has_enabled_extension(khr::get_memory_requirements2::NAME);
|
|
||||||
let dedicated_available =
|
|
||||||
api11 || self.has_enabled_extension(khr::dedicated_allocation::NAME);
|
|
||||||
|
|
||||||
if !get_requirements2 {
|
|
||||||
let requirements = unsafe { self.raw.get_buffer_memory_requirements(buffer) };
|
|
||||||
return Ok(requirements.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dedicated = vk::MemoryDedicatedRequirements::default();
|
|
||||||
let mut requirements = vk::MemoryRequirements2::default();
|
|
||||||
|
|
||||||
if dedicated_available {
|
|
||||||
requirements = requirements.push_next(&mut dedicated);
|
|
||||||
}
|
|
||||||
|
|
||||||
let info = vk::BufferMemoryRequirementsInfo2::default().buffer(buffer);
|
|
||||||
unsafe {
|
|
||||||
if api11 {
|
|
||||||
self.raw
|
|
||||||
.get_buffer_memory_requirements2(&info, &mut requirements);
|
|
||||||
} else {
|
|
||||||
khr::get_memory_requirements2::Device::new(&self.instance.inner.raw, &self.raw)
|
|
||||||
.get_buffer_memory_requirements2(&info, &mut requirements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(MemoryRequirements {
|
|
||||||
size: requirements.memory_requirements.size,
|
|
||||||
alignment: requirements.memory_requirements.alignment,
|
|
||||||
memory_type_bits: requirements.memory_requirements.memory_type_bits,
|
|
||||||
requires_dedicated_allocation: dedicated_available
|
|
||||||
&& dedicated.requires_dedicated_allocation == vk::TRUE,
|
|
||||||
prefers_dedicated_allocation: dedicated_available
|
|
||||||
&& dedicated.prefers_dedicated_allocation == vk::TRUE,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Queries image memory requirements, including dedicated-allocation hints when available.
|
|
||||||
pub fn image_memory_requirements(&self, image: vk::Image) -> Result<MemoryRequirements> {
|
|
||||||
let api11 = self.api_version() >= vk::API_VERSION_1_1;
|
|
||||||
|
|
||||||
let get_requirements2 =
|
|
||||||
api11 || self.has_enabled_extension(khr::get_memory_requirements2::NAME);
|
|
||||||
let dedicated_available =
|
|
||||||
api11 || self.has_enabled_extension(khr::dedicated_allocation::NAME);
|
|
||||||
|
|
||||||
if !get_requirements2 {
|
|
||||||
let requirements = unsafe { self.raw.get_image_memory_requirements(image) };
|
|
||||||
return Ok(requirements.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dedicated = vk::MemoryDedicatedRequirements::default();
|
|
||||||
let mut requirements = vk::MemoryRequirements2::default();
|
|
||||||
if dedicated_available {
|
|
||||||
requirements = requirements.push_next(&mut dedicated);
|
|
||||||
}
|
|
||||||
|
|
||||||
let info = vk::ImageMemoryRequirementsInfo2::default().image(image);
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
if api11 {
|
|
||||||
self.raw
|
|
||||||
.get_image_memory_requirements2(&info, &mut requirements);
|
|
||||||
} else {
|
|
||||||
khr::get_memory_requirements2::Device::new(&self.instance.inner.raw, &self.raw)
|
|
||||||
.get_image_memory_requirements2(&info, &mut requirements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(MemoryRequirements {
|
|
||||||
size: requirements.memory_requirements.size,
|
|
||||||
alignment: requirements.memory_requirements.alignment,
|
|
||||||
memory_type_bits: requirements.memory_requirements.memory_type_bits,
|
|
||||||
requires_dedicated_allocation: dedicated_available
|
|
||||||
&& dedicated.requires_dedicated_allocation == vk::TRUE,
|
|
||||||
prefers_dedicated_allocation: dedicated_available
|
|
||||||
&& dedicated.prefers_dedicated_allocation == vk::TRUE,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allocates aliasable memory compatible with `requirements` and `property_flags`.
|
|
||||||
///
|
|
||||||
/// Resources that require a dedicated allocation cannot use this resource-independent path.
|
|
||||||
fn allocate_aliasable_memory(
|
|
||||||
&self,
|
|
||||||
requirements: &MemoryRequirements,
|
|
||||||
property_flags: vk::MemoryPropertyFlags,
|
|
||||||
) -> Result<GpuAllocation> {
|
|
||||||
use vk_mem::{Alloc as _, AllocationCreateFlags, AllocationCreateInfo};
|
|
||||||
|
|
||||||
if requirements.requires_dedicated_allocation {
|
|
||||||
return Err(Error::DedicatedAllocationRequiresResource);
|
|
||||||
}
|
|
||||||
|
|
||||||
let memory_type_index = self.find_memory_type_index(requirements, property_flags)?;
|
|
||||||
let create_info = AllocationCreateInfo {
|
|
||||||
flags: AllocationCreateFlags::CAN_ALIAS,
|
|
||||||
required_flags: property_flags,
|
|
||||||
memory_type_bits: 1 << memory_type_index,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let raw_requirements = vk::MemoryRequirements {
|
|
||||||
size: requirements.size,
|
|
||||||
alignment: requirements.alignment,
|
|
||||||
memory_type_bits: requirements.memory_type_bits,
|
|
||||||
};
|
|
||||||
|
|
||||||
let allocator = self.allocator.lock();
|
|
||||||
let raw = unsafe { allocator.allocate_memory(&raw_requirements, &create_info) }?;
|
|
||||||
|
|
||||||
let allocation_info = allocator.get_allocation_info2(&raw);
|
|
||||||
let dedicated_memory = allocation_info.dedicated_memory;
|
|
||||||
let allocation_info = allocation_info.allocation_info;
|
|
||||||
let mapped = NonNull::new(allocation_info.mapped_data.cast::<u8>()).map(SendSyncPtr);
|
|
||||||
|
|
||||||
let allocation = GpuAllocation {
|
|
||||||
raw,
|
|
||||||
memory: allocation_info.device_memory,
|
|
||||||
offset: allocation_info.offset,
|
|
||||||
size: allocation_info.size,
|
|
||||||
memory_type_index: allocation_info.memory_type,
|
|
||||||
mapped,
|
|
||||||
dedicated_memory,
|
|
||||||
};
|
|
||||||
Ok(allocation)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Binds a buffer to aliasable memory at an allocation-local offset.
|
|
||||||
///
|
|
||||||
/// On success, ownership of `allocation` transfers to the returned `Allocation::Shared`;
|
|
||||||
/// store it in the bound resource and drop any caller-side references. The allocation is
|
|
||||||
/// freed when the last bound resource drops. The resource must be destroyed before its
|
|
||||||
/// allocation ownership is dropped. `requirements` must be the buffer's own requirements,
|
|
||||||
/// as returned by the buffer requirements query. If the supplied requirements are wrong,
|
|
||||||
/// the Vulkan bind (`vkBindBufferMemory2` via VMA) remains the authoritative validation.
|
|
||||||
/// Calls are externally synchronized by VMA.
|
|
||||||
pub fn bind_buffer_at_offset(
|
|
||||||
&self,
|
|
||||||
buffer: vk::Buffer,
|
|
||||||
requirements: &MemoryRequirements,
|
|
||||||
allocation: Arc<DeviceObject<GpuAllocation>>,
|
|
||||||
local_offset: vk::DeviceSize,
|
|
||||||
) -> Result<Allocation> {
|
|
||||||
self.require_offset_binding_support()?;
|
|
||||||
self.validate_shared_binding(&allocation, local_offset, *requirements)?;
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
self.allocator.lock().bind_buffer_memory2(
|
|
||||||
&allocation.raw,
|
|
||||||
local_offset,
|
|
||||||
buffer,
|
|
||||||
core::ptr::null(),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Allocation::Shared(allocation))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Binds an image to aliasable memory at an allocation-local offset.
|
|
||||||
///
|
|
||||||
/// On success, ownership of `allocation` transfers to the returned `Allocation::Shared`;
|
|
||||||
/// store it in the bound resource and drop any caller-side references. The allocation is
|
|
||||||
/// freed when the last bound resource drops. The resource must be destroyed before its
|
|
||||||
/// allocation ownership is dropped. `requirements` must be the image's own requirements,
|
|
||||||
/// as returned by the image requirements query. If the supplied requirements are wrong,
|
|
||||||
/// the Vulkan bind (`vkBindImageMemory2` via VMA) remains the authoritative validation.
|
|
||||||
/// Calls are externally synchronized by VMA.
|
|
||||||
pub fn bind_image_at_offset(
|
|
||||||
&self,
|
|
||||||
image: vk::Image,
|
|
||||||
requirements: &MemoryRequirements,
|
|
||||||
allocation: Arc<DeviceObject<GpuAllocation>>,
|
|
||||||
local_offset: vk::DeviceSize,
|
|
||||||
) -> Result<Allocation> {
|
|
||||||
self.require_offset_binding_support()?;
|
|
||||||
self.validate_shared_binding(&allocation, local_offset, *requirements)?;
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
self.allocator.lock().bind_image_memory2(
|
|
||||||
&allocation.raw,
|
|
||||||
local_offset,
|
|
||||||
image,
|
|
||||||
core::ptr::null(),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Allocation::Shared(allocation))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Binds a group of buffers and images into one aliasable allocation.
|
|
||||||
///
|
|
||||||
/// Every member is validated before any Vulkan bind is attempted. Binds then run in slice
|
|
||||||
/// order under one allocator lock; if a bind fails, the first error is returned and earlier
|
|
||||||
/// successful binds remain bound, matching repeated single-resource binds. `bound_count`
|
|
||||||
/// identifies that successfully bound prefix so callers can retain the allocation for it.
|
|
||||||
pub fn bind_group_at_offset(
|
|
||||||
&self,
|
|
||||||
allocation: &Arc<DeviceObject<GpuAllocation>>,
|
|
||||||
bindings: &[MemoryBinding],
|
|
||||||
) -> core::result::Result<(), GroupBindError> {
|
|
||||||
self.require_offset_binding_support()
|
|
||||||
.map_err(|error| GroupBindError {
|
|
||||||
error,
|
|
||||||
bound_count: 0,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
for binding in bindings {
|
|
||||||
let (offset, requirements) = match binding {
|
|
||||||
MemoryBinding::Buffer {
|
|
||||||
offset,
|
|
||||||
requirements,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| MemoryBinding::Image {
|
|
||||||
offset,
|
|
||||||
requirements,
|
|
||||||
..
|
|
||||||
} => (*offset, *requirements),
|
|
||||||
};
|
|
||||||
self.validate_shared_binding(allocation, offset, requirements)
|
|
||||||
.map_err(|error| GroupBindError {
|
|
||||||
error,
|
|
||||||
bound_count: 0,
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let allocator = self.allocator.lock();
|
|
||||||
for (bound_count, binding) in bindings.iter().enumerate() {
|
|
||||||
unsafe {
|
|
||||||
match binding {
|
|
||||||
MemoryBinding::Buffer { buffer, offset, .. } => {
|
|
||||||
allocator
|
|
||||||
.bind_buffer_memory2(
|
|
||||||
&allocation.raw,
|
|
||||||
*offset,
|
|
||||||
*buffer,
|
|
||||||
core::ptr::null(),
|
|
||||||
)
|
|
||||||
.map_err(|error| GroupBindError {
|
|
||||||
error: error.into(),
|
|
||||||
bound_count,
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
MemoryBinding::Image { image, offset, .. } => {
|
|
||||||
allocator
|
|
||||||
.bind_image_memory2(&allocation.raw, *offset, *image, core::ptr::null())
|
|
||||||
.map_err(|error| GroupBindError {
|
|
||||||
error: error.into(),
|
|
||||||
bound_count,
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_shared_binding(
|
|
||||||
&self,
|
|
||||||
allocation: &DeviceObject<GpuAllocation>,
|
|
||||||
local_offset: vk::DeviceSize,
|
|
||||||
requirements: MemoryRequirements,
|
|
||||||
) -> Result<()> {
|
|
||||||
if !core::ptr::eq(Arc::as_ptr(&allocation.device().0), self) {
|
|
||||||
return Err(Error::AllocationDeviceMismatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
if requirements.requires_dedicated_allocation {
|
|
||||||
return Err(Error::DedicatedAllocationRequiresResource);
|
|
||||||
}
|
|
||||||
|
|
||||||
let gpu_allocation: &GpuAllocation = allocation;
|
|
||||||
if requirements.memory_type_bits & (1 << gpu_allocation.memory_type_index()) == 0 {
|
|
||||||
return Err(Error::IncompatibleAllocationMemoryType {
|
|
||||||
memory_type_index: gpu_allocation.memory_type_index(),
|
|
||||||
memory_type_bits: requirements.memory_type_bits,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let end = local_offset.checked_add(requirements.size);
|
|
||||||
if requirements.alignment == 0
|
|
||||||
|| !local_offset.is_multiple_of(requirements.alignment)
|
|
||||||
|| end.is_none_or(|end| end > gpu_allocation.size())
|
|
||||||
{
|
|
||||||
return Err(Error::InvalidAllocationOffset {
|
|
||||||
offset: local_offset,
|
|
||||||
alignment: requirements.alignment,
|
|
||||||
required_size: requirements.size,
|
|
||||||
allocation_size: gpu_allocation.size(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn allocation_create_info(
|
|
||||||
&self,
|
|
||||||
desc: &AllocationDesc<'_>,
|
|
||||||
) -> Result<vk_mem::AllocationCreateInfo> {
|
|
||||||
use vk_mem::{AllocationCreateFlags, AllocationCreateInfo, MemoryUsage};
|
|
||||||
|
|
||||||
#[allow(deprecated)]
|
|
||||||
let mut info = AllocationCreateInfo {
|
|
||||||
usage: match desc.location {
|
|
||||||
MemoryLocation::Unknown => MemoryUsage::Unknown,
|
|
||||||
MemoryLocation::GpuOnly => MemoryUsage::GpuOnly,
|
|
||||||
MemoryLocation::CpuToGpu => MemoryUsage::CpuToGpu,
|
|
||||||
MemoryLocation::GpuToCpu => MemoryUsage::GpuToCpu,
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
if matches!(
|
|
||||||
desc.location,
|
|
||||||
MemoryLocation::CpuToGpu | MemoryLocation::GpuToCpu
|
|
||||||
) {
|
|
||||||
let memory_properties = &self.adapter.properties.memory_properties;
|
|
||||||
|
|
||||||
let coherent_host_types = memory_properties
|
|
||||||
.memory_types
|
|
||||||
.iter()
|
|
||||||
.take(memory_properties.memory_type_count as usize)
|
|
||||||
.enumerate()
|
|
||||||
.filter(|(_, memory_type)| {
|
|
||||||
memory_type.property_flags.contains(
|
|
||||||
vk::MemoryPropertyFlags::HOST_VISIBLE
|
|
||||||
| vk::MemoryPropertyFlags::HOST_COHERENT,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.fold(0_u32, |bits, (index, _)| bits | (1 << index));
|
|
||||||
|
|
||||||
if coherent_host_types == 0 {
|
|
||||||
return Err(vk::Result::ERROR_FEATURE_NOT_PRESENT.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
info.memory_type_bits = coherent_host_types;
|
|
||||||
info.flags |= AllocationCreateFlags::MAPPED;
|
|
||||||
}
|
|
||||||
|
|
||||||
let api_version = self
|
|
||||||
.instance
|
|
||||||
.inner
|
|
||||||
.api_version
|
|
||||||
.min(self.adapter.properties.device_api_version);
|
|
||||||
|
|
||||||
if desc.strategy == AllocationStrategy::Dedicated && api_version < vk::API_VERSION_1_1 {
|
|
||||||
return Err(Error::DedicatedAllocationRequiresApi11);
|
|
||||||
}
|
|
||||||
|
|
||||||
if desc.strategy == AllocationStrategy::Dedicated {
|
|
||||||
info.flags |= AllocationCreateFlags::DEDICATED_MEMORY;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn allocate_for_buffer(
|
|
||||||
&self,
|
|
||||||
buffer: vk::Buffer,
|
|
||||||
desc: AllocationDesc<'_>,
|
|
||||||
) -> Result<GpuAllocation> {
|
|
||||||
use vk_mem::Alloc as _;
|
|
||||||
|
|
||||||
let create_info = self.allocation_create_info(&desc)?;
|
|
||||||
let allocator = self.allocator.lock();
|
|
||||||
|
|
||||||
let raw = unsafe { allocator.allocate_memory_for_buffer(buffer, &create_info) }?;
|
|
||||||
let allocation_info = allocator.get_allocation_info2(&raw);
|
|
||||||
let dedicated_memory = allocation_info.dedicated_memory;
|
|
||||||
let allocation_info = allocation_info.allocation_info;
|
|
||||||
let mapped = NonNull::new(allocation_info.mapped_data.cast::<u8>()).map(SendSyncPtr);
|
|
||||||
|
|
||||||
Ok(GpuAllocation {
|
|
||||||
raw,
|
|
||||||
memory: allocation_info.device_memory,
|
|
||||||
offset: allocation_info.offset,
|
|
||||||
size: allocation_info.size,
|
|
||||||
memory_type_index: allocation_info.memory_type,
|
|
||||||
mapped,
|
|
||||||
dedicated_memory,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn allocate_for_image(
|
|
||||||
&self,
|
|
||||||
image: vk::Image,
|
|
||||||
desc: AllocationDesc<'_>,
|
|
||||||
) -> Result<GpuAllocation> {
|
|
||||||
use vk_mem::Alloc as _;
|
|
||||||
|
|
||||||
let create_info = self.allocation_create_info(&desc)?;
|
|
||||||
let allocator = self.allocator.lock();
|
|
||||||
|
|
||||||
let raw = unsafe { allocator.allocate_memory_for_image(image, &create_info) }?;
|
|
||||||
let allocation_info = allocator.get_allocation_info2(&raw);
|
|
||||||
let dedicated_memory = allocation_info.dedicated_memory;
|
|
||||||
let allocation_info = allocation_info.allocation_info;
|
|
||||||
let mapped = NonNull::new(allocation_info.mapped_data.cast::<u8>()).map(SendSyncPtr);
|
|
||||||
|
|
||||||
Ok(GpuAllocation {
|
|
||||||
raw,
|
|
||||||
memory: allocation_info.device_memory,
|
|
||||||
offset: allocation_info.offset,
|
|
||||||
size: allocation_info.size,
|
|
||||||
memory_type_index: allocation_info.memory_type,
|
|
||||||
mapped,
|
|
||||||
dedicated_memory,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bind_buffer_memory(&self, alloc: &GpuAllocation, buffer: vk::Buffer) -> Result<()> {
|
|
||||||
unsafe {
|
|
||||||
self.allocator
|
|
||||||
.lock()
|
|
||||||
.bind_buffer_memory(&alloc.raw, buffer)?
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bind_image_memory(&self, alloc: &GpuAllocation, image: vk::Image) -> Result<()> {
|
|
||||||
unsafe { self.allocator.lock().bind_image_memory(&alloc.raw, image)? };
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sync_threadpool(&self) -> &sync::SyncThreadpool {
|
pub fn sync_threadpool(&self) -> &sync::SyncThreadpool {
|
||||||
&self.sync_threadpool
|
&self.sync_threadpool
|
||||||
}
|
}
|
||||||
|
|
@ -1718,8 +1072,8 @@ pub mod asdf {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl traits::ExternallyManagedObject<Device> for GpuAllocation {
|
impl traits::ExternallyManagedObject<Device> for GpuAllocation {
|
||||||
unsafe fn destroy(mut self, device: &Device) {
|
unsafe fn destroy(self, device: &Device) {
|
||||||
unsafe { device.allocator.lock().free_memory(&mut self.raw) };
|
_ = device.alloc2.lock().free(self);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1801,11 +1155,4 @@ pub mod asdf {
|
||||||
vk::FALSE
|
vk::FALSE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn gpu_allocation_is_send_sync() {
|
|
||||||
fn assert_send_sync<T: Send + Sync>() {}
|
|
||||||
|
|
||||||
assert_send_sync::<super::GpuAllocation>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,10 @@ pub struct InstanceExtensions {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct InstanceInner {
|
pub struct InstanceInner {
|
||||||
// Messengers must be destroyed while the raw instance is still alive. Keep this first so
|
|
||||||
// it drops before `raw` during instance teardown.
|
|
||||||
pub _debug_utils: DebugUtils,
|
|
||||||
pub raw: ash::Instance,
|
pub raw: ash::Instance,
|
||||||
pub api_version: u32,
|
|
||||||
pub extensions: InstanceExtensions,
|
pub extensions: InstanceExtensions,
|
||||||
pub entry: Entry,
|
pub entry: Entry,
|
||||||
|
pub _debug_utils: DebugUtils,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -76,7 +73,7 @@ const DEBUG_LAYER_SETTINGS: &[vk::LayerSettingEXT] = &[
|
||||||
p_layer_name: VALIDATION_LAYER.as_ptr(),
|
p_layer_name: VALIDATION_LAYER.as_ptr(),
|
||||||
p_setting_name: c"validate_best_practices".as_ptr(),
|
p_setting_name: c"validate_best_practices".as_ptr(),
|
||||||
value_count: 1,
|
value_count: 1,
|
||||||
p_values: &[vk::TRUE] as *const u32 as _,
|
p_values: &[1u8; 1] as *const u8 as _,
|
||||||
ty: vk::LayerSettingTypeEXT::BOOL32,
|
ty: vk::LayerSettingTypeEXT::BOOL32,
|
||||||
_marker: core::marker::PhantomData,
|
_marker: core::marker::PhantomData,
|
||||||
},
|
},
|
||||||
|
|
@ -84,7 +81,7 @@ const DEBUG_LAYER_SETTINGS: &[vk::LayerSettingEXT] = &[
|
||||||
p_layer_name: VALIDATION_LAYER.as_ptr(),
|
p_layer_name: VALIDATION_LAYER.as_ptr(),
|
||||||
p_setting_name: c"validate_best_practices_amd".as_ptr(),
|
p_setting_name: c"validate_best_practices_amd".as_ptr(),
|
||||||
value_count: 1,
|
value_count: 1,
|
||||||
p_values: &[vk::TRUE] as *const u32 as _,
|
p_values: &[1u8; 1] as *const u8 as _,
|
||||||
ty: vk::LayerSettingTypeEXT::BOOL32,
|
ty: vk::LayerSettingTypeEXT::BOOL32,
|
||||||
_marker: core::marker::PhantomData,
|
_marker: core::marker::PhantomData,
|
||||||
},
|
},
|
||||||
|
|
@ -92,7 +89,7 @@ const DEBUG_LAYER_SETTINGS: &[vk::LayerSettingEXT] = &[
|
||||||
p_layer_name: VALIDATION_LAYER.as_ptr(),
|
p_layer_name: VALIDATION_LAYER.as_ptr(),
|
||||||
p_setting_name: c"validate_sync".as_ptr(),
|
p_setting_name: c"validate_sync".as_ptr(),
|
||||||
value_count: 1,
|
value_count: 1,
|
||||||
p_values: &[vk::TRUE] as *const u32 as _,
|
p_values: &[1u8; 1] as *const u8 as _,
|
||||||
ty: vk::LayerSettingTypeEXT::BOOL32,
|
ty: vk::LayerSettingTypeEXT::BOOL32,
|
||||||
_marker: core::marker::PhantomData,
|
_marker: core::marker::PhantomData,
|
||||||
},
|
},
|
||||||
|
|
@ -124,14 +121,16 @@ impl Instance {
|
||||||
unsafe { entry.try_enumerate_instance_version()? }.unwrap_or(vk::API_VERSION_1_0);
|
unsafe { entry.try_enumerate_instance_version()? }.unwrap_or(vk::API_VERSION_1_0);
|
||||||
|
|
||||||
let app_info = vk::ApplicationInfo::default()
|
let app_info = vk::ApplicationInfo::default()
|
||||||
// ash/VMA support Vulkan 1.3, and the application must not request a
|
.api_version(if version < vk::API_VERSION_1_1 {
|
||||||
// version newer than the loader supports.
|
vk::API_VERSION_1_0
|
||||||
.api_version(version.min(vk::API_VERSION_1_3))
|
} else {
|
||||||
|
// ash doesn't support 1.4 yet
|
||||||
|
vk::API_VERSION_1_3
|
||||||
|
})
|
||||||
.application_name(&app_name)
|
.application_name(&app_name)
|
||||||
.application_version(desc.app_version)
|
.application_version(desc.app_version)
|
||||||
.engine_name(c"Bevy Engine")
|
.engine_name(c"Bevy Engine")
|
||||||
.engine_version(vk::make_api_version(0, 0, 1, 0));
|
.engine_version(vk::make_api_version(0, 0, 1, 0));
|
||||||
let api_version = app_info.api_version;
|
|
||||||
|
|
||||||
let mut validation_info =
|
let mut validation_info =
|
||||||
vk::LayerSettingsCreateInfoEXT::default().settings(desc.layer_settings);
|
vk::LayerSettingsCreateInfoEXT::default().settings(desc.layer_settings);
|
||||||
|
|
@ -151,9 +150,8 @@ impl Instance {
|
||||||
let mut requested_extensions = desc.instance_extensions.to_vec();
|
let mut requested_extensions = desc.instance_extensions.to_vec();
|
||||||
|
|
||||||
requested_extensions.push(make_extension!(ext::debug_utils as 1));
|
requested_extensions.push(make_extension!(ext::debug_utils as 1));
|
||||||
if !desc.layer_settings.is_empty() {
|
#[cfg(debug_assertions)]
|
||||||
requested_extensions.push(make_extension!(ext::layer_settings));
|
requested_extensions.push(make_extension!(ext::layer_settings));
|
||||||
}
|
|
||||||
|
|
||||||
// These are wanted for surface capabilities querying, but not strictly
|
// These are wanted for surface capabilities querying, but not strictly
|
||||||
// required, and are only supported on newer devices, especially on
|
// required, and are only supported on newer devices, especially on
|
||||||
|
|
@ -225,10 +223,9 @@ impl Instance {
|
||||||
};
|
};
|
||||||
|
|
||||||
let instance = Arc::new(InstanceInner {
|
let instance = Arc::new(InstanceInner {
|
||||||
_debug_utils: debug_utils,
|
|
||||||
raw: instance,
|
raw: instance,
|
||||||
api_version,
|
|
||||||
extensions,
|
extensions,
|
||||||
|
_debug_utils: debug_utils,
|
||||||
entry,
|
entry,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,6 @@ pub mod instance;
|
||||||
pub mod pipeline_cache;
|
pub mod pipeline_cache;
|
||||||
pub mod queue;
|
pub mod queue;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
#[cfg(feature = "testing")]
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub mod testing;
|
|
||||||
|
|
||||||
pub use device::{Device, DeviceOwned, DeviceRef, Extension};
|
pub use device::{Device, DeviceOwned, DeviceRef, Extension};
|
||||||
pub use instance::{DebugUtils, DebugUtilsCreateInfo, Instance};
|
pub use instance::{DebugUtils, DebugUtilsCreateInfo, Instance};
|
||||||
|
|
@ -77,45 +74,10 @@ pub enum Error {
|
||||||
Utf8Error(#[from] core::str::Utf8Error),
|
Utf8Error(#[from] core::str::Utf8Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
NulError(#[from] std::ffi::NulError),
|
NulError(#[from] std::ffi::NulError),
|
||||||
|
#[error(transparent)]
|
||||||
|
GpuAllocatorError(#[from] gpu_allocator::AllocationError),
|
||||||
#[error("No Physical Device found.")]
|
#[error("No Physical Device found.")]
|
||||||
NoPhysicalDevice,
|
NoPhysicalDevice,
|
||||||
#[error(
|
|
||||||
"Dedicated allocation requires Vulkan 1.1 or both VK_KHR_dedicated_allocation and VK_KHR_get_memory_requirements2 extensions."
|
|
||||||
)]
|
|
||||||
DedicatedAllocationRequiresApi11,
|
|
||||||
#[error(
|
|
||||||
"Offset-aware memory binding requires Vulkan 1.1 or VK_KHR_get_memory_requirements2, VK_KHR_bind_memory2, and VK_KHR_dedicated_allocation to be enabled."
|
|
||||||
)]
|
|
||||||
OffsetBindingRequiresApi11,
|
|
||||||
#[error(
|
|
||||||
"No memory type matches requirements mask {memory_type_bits:#010x} and required flags {property_flags:?}."
|
|
||||||
)]
|
|
||||||
NoCompatibleMemoryType {
|
|
||||||
memory_type_bits: u32,
|
|
||||||
property_flags: vk::MemoryPropertyFlags,
|
|
||||||
},
|
|
||||||
#[error(
|
|
||||||
"Memory allocation requirements request a dedicated allocation, which cannot be allocated without its resource."
|
|
||||||
)]
|
|
||||||
DedicatedAllocationRequiresResource,
|
|
||||||
#[error(
|
|
||||||
"Allocation offset {offset} is invalid: alignment is {alignment}, requested size is {required_size}, allocation size is {allocation_size}."
|
|
||||||
)]
|
|
||||||
InvalidAllocationOffset {
|
|
||||||
offset: u64,
|
|
||||||
alignment: u64,
|
|
||||||
required_size: u64,
|
|
||||||
allocation_size: u64,
|
|
||||||
},
|
|
||||||
#[error(
|
|
||||||
"Allocation memory type {memory_type_index} is not allowed by requirements mask {memory_type_bits:#010x}."
|
|
||||||
)]
|
|
||||||
IncompatibleAllocationMemoryType {
|
|
||||||
memory_type_index: u32,
|
|
||||||
memory_type_bits: u32,
|
|
||||||
},
|
|
||||||
#[error("The allocation and resource belong to different Vulkan devices.")]
|
|
||||||
AllocationDeviceMismatch,
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
#[error(
|
#[error(
|
||||||
|
|
|
||||||
|
|
@ -364,12 +364,10 @@ impl DeviceQueueInfos {
|
||||||
})
|
})
|
||||||
.expect("No graphics queue family found");
|
.expect("No graphics queue family found");
|
||||||
|
|
||||||
if display_handle.is_some() {
|
assert!(
|
||||||
assert!(
|
graphics.flags.contains(QueueFlags::PRESENT),
|
||||||
graphics.flags.contains(QueueFlags::PRESENT),
|
"Selected graphics queue family does not support presentation"
|
||||||
"Selected graphics queue family does not support presentation"
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let compute = queue_families
|
let compute = queue_families
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
||||||
|
|
@ -1,513 +0,0 @@
|
||||||
//! Real Vulkan test devices with Khronos validation output captured for assertions.
|
|
||||||
//!
|
|
||||||
//! Run the device smoke tests with `cargo test -p renderer_device --features testing`.
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
ffi::{CStr, c_void},
|
|
||||||
fmt,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
use ash::{Entry, ext, khr, vk};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
Device, Instance, PhysicalDeviceFeatures,
|
|
||||||
instance::{DebugUtils, InstanceDesc, VALIDATION_LAYER},
|
|
||||||
};
|
|
||||||
|
|
||||||
const TEST_LAYER_SETTINGS: &[vk::LayerSettingEXT<'static>] = &[
|
|
||||||
vk::LayerSettingEXT {
|
|
||||||
p_layer_name: VALIDATION_LAYER.as_ptr(),
|
|
||||||
p_setting_name: c"validate_sync".as_ptr(),
|
|
||||||
value_count: 1,
|
|
||||||
p_values: &[vk::TRUE] as *const u32 as _,
|
|
||||||
ty: vk::LayerSettingTypeEXT::BOOL32,
|
|
||||||
_marker: core::marker::PhantomData,
|
|
||||||
},
|
|
||||||
vk::LayerSettingEXT {
|
|
||||||
p_layer_name: VALIDATION_LAYER.as_ptr(),
|
|
||||||
p_setting_name: c"validate_best_practices".as_ptr(),
|
|
||||||
value_count: 1,
|
|
||||||
p_values: &[vk::TRUE] as *const u32 as _,
|
|
||||||
ty: vk::LayerSettingTypeEXT::BOOL32,
|
|
||||||
_marker: core::marker::PhantomData,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/// One validation callback message captured from a real Vulkan instance.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct CapturedMessage {
|
|
||||||
pub severity: vk::DebugUtilsMessageSeverityFlagsEXT,
|
|
||||||
pub types: vk::DebugUtilsMessageTypeFlagsEXT,
|
|
||||||
pub message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for CapturedMessage {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{:?} {:?}: {}", self.severity, self.types, self.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A Vulkan instance and logical device configured for validation-backed tests.
|
|
||||||
///
|
|
||||||
/// Instances are independent; tests may acquire their own device without sharing Vulkan state.
|
|
||||||
pub struct TestVulkan {
|
|
||||||
device: Device,
|
|
||||||
// Drop the callback messenger while its instance and user data are still alive.
|
|
||||||
_messenger: DebugUtils,
|
|
||||||
messages: Arc<Mutex<Vec<CapturedMessage>>>,
|
|
||||||
instance: Instance,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestVulkan {
|
|
||||||
/// Attempt to create a Vulkan instance/device, printing a reason and returning `None` when
|
|
||||||
/// Vulkan, a physical device, or the Khronos validation layer is unavailable.
|
|
||||||
pub fn acquire() -> Option<Self> {
|
|
||||||
match Self::create() {
|
|
||||||
Ok(test) => {
|
|
||||||
eprintln!(
|
|
||||||
"renderer_device Vulkan test adapter: {}",
|
|
||||||
test.device
|
|
||||||
.properties()
|
|
||||||
.core
|
|
||||||
.device_name_as_c_str()
|
|
||||||
.map(CStr::to_string_lossy)
|
|
||||||
.unwrap_or_else(|_| "<invalid device name>".into())
|
|
||||||
);
|
|
||||||
Some(test)
|
|
||||||
}
|
|
||||||
Err(reason) => {
|
|
||||||
eprintln!("Skipping Vulkan test: {reason}");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a validation-backed Vulkan test device or panic. Useful for CI jobs where Vulkan is
|
|
||||||
/// a required test dependency.
|
|
||||||
pub fn expect() -> Self {
|
|
||||||
Self::acquire().expect("Vulkan test device is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create() -> Result<Self, String> {
|
|
||||||
// Do not rewrite loader environment variables here. The loader's default search paths and
|
|
||||||
// environment provided by the development shell are both supported.
|
|
||||||
let entry = unsafe { Entry::load() }.map_err(|error| format!("loading Vulkan: {error}"))?;
|
|
||||||
let layers = unsafe { entry.enumerate_instance_layer_properties() }
|
|
||||||
.map_err(|error| format!("enumerating instance layers: {error}"))?;
|
|
||||||
if !layers
|
|
||||||
.iter()
|
|
||||||
.any(|layer| layer.layer_name_as_c_str().ok() == Some(VALIDATION_LAYER))
|
|
||||||
{
|
|
||||||
return Err("VK_LAYER_KHRONOS_validation is not available".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let instance = Instance::new(&InstanceDesc {
|
|
||||||
app_name: Some("renderer_device validation tests"),
|
|
||||||
instance_extensions: &[crate::make_extension!(khr::surface)],
|
|
||||||
layer_settings: TEST_LAYER_SETTINGS,
|
|
||||||
layers: &[VALIDATION_LAYER],
|
|
||||||
display_handle: None,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.map_err(|error| format!("creating Vulkan instance: {error}"))?;
|
|
||||||
|
|
||||||
let messages = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
let debug_loader =
|
|
||||||
ext::debug_utils::Instance::new(&instance.inner.entry, &instance.inner.raw);
|
|
||||||
let create_info = vk::DebugUtilsMessengerCreateInfoEXT::default()
|
|
||||||
.message_severity(
|
|
||||||
vk::DebugUtilsMessageSeverityFlagsEXT::ERROR
|
|
||||||
| vk::DebugUtilsMessageSeverityFlagsEXT::WARNING,
|
|
||||||
)
|
|
||||||
.message_type(
|
|
||||||
vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
|
|
||||||
| vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION
|
|
||||||
| vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE,
|
|
||||||
)
|
|
||||||
.user_data(Arc::as_ptr(&messages) as *mut c_void)
|
|
||||||
.pfn_user_callback(Some(capture_callback));
|
|
||||||
let raw_messenger =
|
|
||||||
unsafe { debug_loader.create_debug_utils_messenger(&create_info, None) }
|
|
||||||
.map_err(|error| format!("creating validation message messenger: {error}"))?;
|
|
||||||
let messenger = DebugUtils {
|
|
||||||
instance: debug_loader,
|
|
||||||
messenger: raw_messenger,
|
|
||||||
};
|
|
||||||
|
|
||||||
let adapter = instance
|
|
||||||
.choose_adapter_default(None, &[], None)
|
|
||||||
.map_err(|error| format!("selecting physical device: {error}"))?;
|
|
||||||
let supported = &adapter.features;
|
|
||||||
let requested = PhysicalDeviceFeatures {
|
|
||||||
core: vk::PhysicalDeviceFeatures {
|
|
||||||
shader_sampled_image_array_dynamic_indexing: supported
|
|
||||||
.core
|
|
||||||
.shader_sampled_image_array_dynamic_indexing,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
core12: vk::PhysicalDeviceVulkan12Features {
|
|
||||||
descriptor_indexing: supported.core12.descriptor_indexing,
|
|
||||||
descriptor_binding_partially_bound: supported
|
|
||||||
.core12
|
|
||||||
.descriptor_binding_partially_bound,
|
|
||||||
descriptor_binding_update_unused_while_pending: supported
|
|
||||||
.core12
|
|
||||||
.descriptor_binding_update_unused_while_pending,
|
|
||||||
descriptor_binding_sampled_image_update_after_bind: supported
|
|
||||||
.core12
|
|
||||||
.descriptor_binding_sampled_image_update_after_bind,
|
|
||||||
descriptor_binding_storage_buffer_update_after_bind: supported
|
|
||||||
.core12
|
|
||||||
.descriptor_binding_storage_buffer_update_after_bind,
|
|
||||||
runtime_descriptor_array: supported.core12.runtime_descriptor_array,
|
|
||||||
timeline_semaphore: supported.core12.timeline_semaphore,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
core13: vk::PhysicalDeviceVulkan13Features {
|
|
||||||
synchronization2: supported.core13.synchronization2,
|
|
||||||
dynamic_rendering: supported.core13.dynamic_rendering,
|
|
||||||
maintenance4: supported.core13.maintenance4,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let device = adapter
|
|
||||||
.create_logical_device(&instance, &[], requested, None)
|
|
||||||
.map_err(|error| format!("creating logical device: {error}"))?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
device,
|
|
||||||
_messenger: messenger,
|
|
||||||
messages,
|
|
||||||
instance,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn instance(&self) -> &Instance {
|
|
||||||
&self.instance
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn device(&self) -> &Device {
|
|
||||||
&self.device
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn physical_device(&self) -> &crate::PhysicalDeviceInfo {
|
|
||||||
self.device.physical_device()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prints all warnings and fails only for validation errors.
|
|
||||||
pub fn assert_no_validation_errors(&self) {
|
|
||||||
let messages = self.take_messages();
|
|
||||||
for message in &messages {
|
|
||||||
if message
|
|
||||||
.severity
|
|
||||||
.contains(vk::DebugUtilsMessageSeverityFlagsEXT::WARNING)
|
|
||||||
{
|
|
||||||
eprintln!("Vulkan validation warning: {message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let errors = messages
|
|
||||||
.into_iter()
|
|
||||||
.filter(|message| {
|
|
||||||
message
|
|
||||||
.severity
|
|
||||||
.contains(vk::DebugUtilsMessageSeverityFlagsEXT::ERROR)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
assert!(
|
|
||||||
errors.is_empty(),
|
|
||||||
"Vulkan validation errors:\n{}",
|
|
||||||
format_messages(&errors)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fail for warnings as well as errors, for tests that explicitly require a clean log.
|
|
||||||
pub fn assert_no_warnings(&self) {
|
|
||||||
let messages = self.take_messages();
|
|
||||||
assert!(
|
|
||||||
messages.is_empty(),
|
|
||||||
"Vulkan validation messages:\n{}",
|
|
||||||
format_messages(&messages)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Drain captured messages; useful for tests that intentionally exercise a validation error.
|
|
||||||
pub fn take_messages(&self) -> Vec<CapturedMessage> {
|
|
||||||
std::mem::take(
|
|
||||||
&mut *self
|
|
||||||
.messages
|
|
||||||
.lock()
|
|
||||||
.expect("validation message lock poisoned"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_messages(messages: &[CapturedMessage]) -> String {
|
|
||||||
messages
|
|
||||||
.iter()
|
|
||||||
.map(ToString::to_string)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe extern "system" fn capture_callback(
|
|
||||||
severity: vk::DebugUtilsMessageSeverityFlagsEXT,
|
|
||||||
types: vk::DebugUtilsMessageTypeFlagsEXT,
|
|
||||||
callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>,
|
|
||||||
user_data: *mut c_void,
|
|
||||||
) -> vk::Bool32 {
|
|
||||||
if callback_data.is_null() || user_data.is_null() {
|
|
||||||
return vk::FALSE;
|
|
||||||
}
|
|
||||||
let data = unsafe { &*callback_data };
|
|
||||||
let message = if data.p_message.is_null() {
|
|
||||||
"<empty validation message>".into()
|
|
||||||
} else {
|
|
||||||
unsafe { CStr::from_ptr(data.p_message) }
|
|
||||||
.to_string_lossy()
|
|
||||||
.into_owned()
|
|
||||||
};
|
|
||||||
let messages = unsafe { &*(user_data as *const Mutex<Vec<CapturedMessage>>) };
|
|
||||||
if let Ok(mut messages) = messages.lock() {
|
|
||||||
messages.push(CapturedMessage {
|
|
||||||
severity,
|
|
||||||
types,
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
vk::FALSE
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::device::{
|
|
||||||
Allocation, AllocationDesc, AllocationStrategy, DeviceObject, MemoryLocation,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn instance_and_device_creation() {
|
|
||||||
let Some(test) = TestVulkan::acquire() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
assert_ne!(test.instance().inner.raw.handle(), vk::Instance::null());
|
|
||||||
assert_ne!(test.device().dev().handle(), vk::Device::null());
|
|
||||||
test.device().wait_idle().expect("device wait idle");
|
|
||||||
test.assert_no_validation_errors();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn buffer_lifecycle_under_validation() {
|
|
||||||
let Some(test) = TestVulkan::acquire() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let device = test.device();
|
|
||||||
let info = vk::BufferCreateInfo::default()
|
|
||||||
.size(256)
|
|
||||||
.usage(vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST)
|
|
||||||
.sharing_mode(vk::SharingMode::EXCLUSIVE);
|
|
||||||
let buffer = unsafe { device.dev().create_buffer(&info, None) }.expect("create buffer");
|
|
||||||
let buffer = DeviceObject::new(device.clone(), buffer);
|
|
||||||
let allocation = device
|
|
||||||
.allocate_for_buffer(
|
|
||||||
*buffer,
|
|
||||||
AllocationDesc {
|
|
||||||
name: "renderer_device validation smoke buffer",
|
|
||||||
location: MemoryLocation::CpuToGpu,
|
|
||||||
strategy: AllocationStrategy::AllocatorManaged,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("allocate buffer memory");
|
|
||||||
let allocation = DeviceObject::new(device.clone(), allocation);
|
|
||||||
device
|
|
||||||
.bind_buffer_memory(&allocation, *buffer)
|
|
||||||
.expect("bind buffer memory");
|
|
||||||
drop(buffer);
|
|
||||||
drop(allocation);
|
|
||||||
test.assert_no_validation_errors();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aliasable_buffer_allocation_binds_at_zero_under_validation() {
|
|
||||||
let Some(test) = TestVulkan::acquire() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let device = test.device();
|
|
||||||
let create_info = vk::BufferCreateInfo::default()
|
|
||||||
.size(256)
|
|
||||||
.usage(vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST)
|
|
||||||
.sharing_mode(vk::SharingMode::EXCLUSIVE);
|
|
||||||
let buffer = unsafe { device.dev().create_buffer(&create_info, None) }
|
|
||||||
.expect("create aliasable buffer");
|
|
||||||
let buffer = DeviceObject::new(device.clone(), buffer);
|
|
||||||
let requirements = device
|
|
||||||
.buffer_memory_requirements(*buffer)
|
|
||||||
.expect("query buffer requirements");
|
|
||||||
let memory = device
|
|
||||||
.allocate_memory(&requirements, vk::MemoryPropertyFlags::empty())
|
|
||||||
.expect("allocate aliasable buffer memory");
|
|
||||||
let binding = device
|
|
||||||
.bind_buffer_at_offset(*buffer, &requirements, memory, 0)
|
|
||||||
.expect("bind aliasable buffer memory");
|
|
||||||
assert!(matches!(binding, Allocation::Shared(_)));
|
|
||||||
|
|
||||||
drop(buffer);
|
|
||||||
drop(binding);
|
|
||||||
test.assert_no_validation_errors();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aliasable_image_allocation_binds_at_zero_under_validation() {
|
|
||||||
let Some(test) = TestVulkan::acquire() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let device = test.device();
|
|
||||||
let create_info = vk::ImageCreateInfo::default()
|
|
||||||
.image_type(vk::ImageType::TYPE_2D)
|
|
||||||
.format(vk::Format::R8G8B8A8_UNORM)
|
|
||||||
.extent(vk::Extent3D {
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
depth: 1,
|
|
||||||
})
|
|
||||||
.mip_levels(1)
|
|
||||||
.array_layers(1)
|
|
||||||
.samples(vk::SampleCountFlags::TYPE_1)
|
|
||||||
.tiling(vk::ImageTiling::OPTIMAL)
|
|
||||||
.usage(vk::ImageUsageFlags::TRANSFER_DST)
|
|
||||||
.sharing_mode(vk::SharingMode::EXCLUSIVE)
|
|
||||||
.initial_layout(vk::ImageLayout::UNDEFINED);
|
|
||||||
let image = unsafe { device.dev().create_image(&create_info, None) }
|
|
||||||
.expect("create aliasable image");
|
|
||||||
let image = DeviceObject::new(device.clone(), image);
|
|
||||||
let requirements = device
|
|
||||||
.image_memory_requirements(*image)
|
|
||||||
.expect("query image requirements");
|
|
||||||
let memory = device
|
|
||||||
.allocate_memory(&requirements, vk::MemoryPropertyFlags::empty())
|
|
||||||
.expect("allocate aliasable image memory");
|
|
||||||
let binding = device
|
|
||||||
.bind_image_at_offset(*image, &requirements, memory, 0)
|
|
||||||
.expect("bind aliasable image memory");
|
|
||||||
assert!(matches!(binding, Allocation::Shared(_)));
|
|
||||||
|
|
||||||
drop(image);
|
|
||||||
drop(binding);
|
|
||||||
test.assert_no_validation_errors();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn aliasable_binding_rejects_out_of_bounds_and_unaligned_offsets() {
|
|
||||||
let Some(test) = TestVulkan::acquire() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let device = test.device();
|
|
||||||
let create_info = vk::BufferCreateInfo::default()
|
|
||||||
.size(256)
|
|
||||||
.usage(vk::BufferUsageFlags::TRANSFER_DST)
|
|
||||||
.sharing_mode(vk::SharingMode::EXCLUSIVE);
|
|
||||||
|
|
||||||
let make_buffer = || {
|
|
||||||
let raw = unsafe { device.dev().create_buffer(&create_info, None) }
|
|
||||||
.expect("create offset validation buffer");
|
|
||||||
DeviceObject::new(device.clone(), raw)
|
|
||||||
};
|
|
||||||
let buffer = make_buffer();
|
|
||||||
let requirements = device
|
|
||||||
.buffer_memory_requirements(*buffer)
|
|
||||||
.expect("query offset validation requirements");
|
|
||||||
let memory = device
|
|
||||||
.allocate_memory(&requirements, vk::MemoryPropertyFlags::empty())
|
|
||||||
.expect("allocate offset validation memory");
|
|
||||||
assert!(matches!(
|
|
||||||
device.bind_buffer_at_offset(*buffer, &requirements, memory, requirements.size),
|
|
||||||
Err(crate::Error::InvalidAllocationOffset { .. })
|
|
||||||
));
|
|
||||||
drop(buffer);
|
|
||||||
|
|
||||||
let buffer = make_buffer();
|
|
||||||
let requirements = device
|
|
||||||
.buffer_memory_requirements(*buffer)
|
|
||||||
.expect("query unaligned validation requirements");
|
|
||||||
if requirements.alignment > 1 {
|
|
||||||
let memory = device
|
|
||||||
.allocate_memory(&requirements, vk::MemoryPropertyFlags::empty())
|
|
||||||
.expect("allocate unaligned validation memory");
|
|
||||||
assert!(matches!(
|
|
||||||
device.bind_buffer_at_offset(*buffer, &requirements, memory, 1),
|
|
||||||
Err(crate::Error::InvalidAllocationOffset { .. })
|
|
||||||
));
|
|
||||||
}
|
|
||||||
drop(buffer);
|
|
||||||
test.assert_no_validation_errors();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn validation_capture_works() {
|
|
||||||
let Some(test) = TestVulkan::acquire() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let device = test.device();
|
|
||||||
let buffer_info = vk::BufferCreateInfo::default()
|
|
||||||
.size(16)
|
|
||||||
.usage(vk::BufferUsageFlags::TRANSFER_DST)
|
|
||||||
.sharing_mode(vk::SharingMode::EXCLUSIVE);
|
|
||||||
let buffer = unsafe { device.dev().create_buffer(&buffer_info, None) }
|
|
||||||
.expect("create validation probe buffer");
|
|
||||||
let buffer = DeviceObject::new(device.clone(), buffer);
|
|
||||||
let allocation = device
|
|
||||||
.allocate_for_buffer(
|
|
||||||
*buffer,
|
|
||||||
AllocationDesc {
|
|
||||||
name: "renderer_device validation probe buffer",
|
|
||||||
location: MemoryLocation::CpuToGpu,
|
|
||||||
strategy: AllocationStrategy::AllocatorManaged,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("allocate validation probe buffer");
|
|
||||||
let allocation = DeviceObject::new(device.clone(), allocation);
|
|
||||||
device
|
|
||||||
.bind_buffer_memory(&allocation, *buffer)
|
|
||||||
.expect("bind validation probe buffer");
|
|
||||||
let pool_info = vk::CommandPoolCreateInfo::default()
|
|
||||||
.queue_family_index(device.queues.graphics().family.index.index());
|
|
||||||
let pool = unsafe { device.dev().create_command_pool(&pool_info, None) }
|
|
||||||
.expect("create command pool");
|
|
||||||
let allocate_info = vk::CommandBufferAllocateInfo::default()
|
|
||||||
.command_pool(pool)
|
|
||||||
.level(vk::CommandBufferLevel::PRIMARY)
|
|
||||||
.command_buffer_count(1);
|
|
||||||
let command_buffer = unsafe { device.dev().allocate_command_buffers(&allocate_info) }
|
|
||||||
.expect("allocate command buffer")[0];
|
|
||||||
unsafe {
|
|
||||||
device
|
|
||||||
.dev()
|
|
||||||
.begin_command_buffer(command_buffer, &vk::CommandBufferBeginInfo::default())
|
|
||||||
.expect("begin command buffer");
|
|
||||||
// The size must be a multiple of four. The invalid command is never submitted.
|
|
||||||
device
|
|
||||||
.dev()
|
|
||||||
.cmd_fill_buffer(command_buffer, *buffer, 0, 3, 0);
|
|
||||||
device
|
|
||||||
.dev()
|
|
||||||
.end_command_buffer(command_buffer)
|
|
||||||
.expect("end command buffer");
|
|
||||||
device.dev().destroy_command_pool(pool, None);
|
|
||||||
}
|
|
||||||
drop(buffer);
|
|
||||||
drop(allocation);
|
|
||||||
|
|
||||||
let messages = test.take_messages();
|
|
||||||
assert!(
|
|
||||||
messages.iter().any(|message| {
|
|
||||||
message
|
|
||||||
.severity
|
|
||||||
.contains(vk::DebugUtilsMessageSeverityFlagsEXT::ERROR)
|
|
||||||
&& message.message.contains("VUID-vkCmdFillBuffer-size")
|
|
||||||
}),
|
|
||||||
"expected a vkCmdFillBuffer size validation error, got:\n{}",
|
|
||||||
format_messages(&messages)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,18 +5,10 @@ edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ash.workspace = true
|
ash.workspace = true
|
||||||
fixedbitset.workspace = true
|
gpu-allocator.workspace = true
|
||||||
tinyvec.workspace = true
|
|
||||||
renderer_device = { path = "../renderer_device" }
|
renderer_device = { path = "../renderer_device" }
|
||||||
renderer_resources = { path = "../renderer_resources" }
|
renderer_resources = { path = "../renderer_resources" }
|
||||||
ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" }
|
ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" }
|
||||||
bitflags.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { workspace = true }
|
|
||||||
renderer_resources = { path = "../renderer_resources", features = ["testing"] }
|
renderer_resources = { path = "../renderer_resources", features = ["testing"] }
|
||||||
renderer_device = { path = "../renderer_device", features = ["testing"] }
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "compile"
|
|
||||||
harness = false
|
|
||||||
|
|
|
||||||
|
|
@ -1,381 +0,0 @@
|
||||||
//! These graph shapes compare command-level batching with render-pass span
|
|
||||||
//! contraction and barrier hoisting. Each has two disjoint buffer access sets
|
|
||||||
//! joined by a shared-buffer dependency across the passes. The resolve benchmarks
|
|
||||||
//! include device-dependent placement, alias-barrier rebuilding, and pass hoisting
|
|
||||||
//! in the timed region.
|
|
||||||
|
|
||||||
use std::{hint::black_box, sync::Arc, time::Duration};
|
|
||||||
|
|
||||||
use ash::vk;
|
|
||||||
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
|
|
||||||
use renderer_device::device::{MemoryLocation, QueueFlags};
|
|
||||||
use renderer_device::testing::TestVulkan;
|
|
||||||
use renderer_rendergraph::{
|
|
||||||
Buffer, BufferRange, GraphBufferDesc, RenderGraph,
|
|
||||||
commands::{BeginRendering, Command, EndRendering},
|
|
||||||
recorder::{CommandIndex, CommandRecorder, RecordError, RecordedCommand, SideEffectMap},
|
|
||||||
resources::{ResourceState, SubresourceScope, SubresourceScopeInner},
|
|
||||||
};
|
|
||||||
use renderer_resources::{buffers::Buffer as DeviceBuffer, buffers::BufferDesc};
|
|
||||||
|
|
||||||
struct Use {
|
|
||||||
buffer: Buffer,
|
|
||||||
write: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Command for Use {
|
|
||||||
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
|
|
||||||
map.insert(
|
|
||||||
command_index,
|
|
||||||
self.buffer.id(),
|
|
||||||
SubresourceScope {
|
|
||||||
stage: vk::PipelineStageFlags2::COMPUTE_SHADER,
|
|
||||||
access: if self.write {
|
|
||||||
vk::AccessFlags2::SHADER_WRITE
|
|
||||||
} else {
|
|
||||||
vk::AccessFlags2::SHADER_READ
|
|
||||||
},
|
|
||||||
subresource: SubresourceScopeInner::Buffer(BufferRange::full()),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RecordedCommand for Use {
|
|
||||||
fn allowed_in_render_pass(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply(&mut self, _recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UseWithBridge {
|
|
||||||
buffer: Buffer,
|
|
||||||
bridge: Buffer,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Command for UseWithBridge {
|
|
||||||
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
|
|
||||||
for buffer in [self.buffer, self.bridge] {
|
|
||||||
map.insert(
|
|
||||||
command_index,
|
|
||||||
buffer.id(),
|
|
||||||
SubresourceScope {
|
|
||||||
stage: vk::PipelineStageFlags2::COMPUTE_SHADER,
|
|
||||||
access: vk::AccessFlags2::SHADER_WRITE,
|
|
||||||
subresource: SubresourceScopeInner::Buffer(BufferRange::full()),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RecordedCommand for UseWithBridge {
|
|
||||||
fn allowed_in_render_pass(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply(&mut self, _recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build(n_groups: usize, per: usize, pass: bool) -> RenderGraph {
|
|
||||||
let mut graph = RenderGraph::new();
|
|
||||||
let a: Vec<Buffer> = (0..n_groups)
|
|
||||||
.map(|_| {
|
|
||||||
graph.create_buffer(GraphBufferDesc {
|
|
||||||
size: 4096,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let b: Vec<Buffer> = (0..n_groups)
|
|
||||||
.map(|_| {
|
|
||||||
graph.create_buffer(GraphBufferDesc {
|
|
||||||
size: 4096,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let shared = graph.create_buffer(GraphBufferDesc {
|
|
||||||
size: 4096,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
if pass {
|
|
||||||
graph.push(BeginRendering {
|
|
||||||
color: vec![],
|
|
||||||
depth: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for _ in 0..per {
|
|
||||||
for &buffer in &a {
|
|
||||||
graph.push(Use {
|
|
||||||
buffer,
|
|
||||||
write: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
graph.push(Use {
|
|
||||||
buffer: shared,
|
|
||||||
write: true,
|
|
||||||
});
|
|
||||||
if pass {
|
|
||||||
graph.push(EndRendering);
|
|
||||||
graph.push(BeginRendering {
|
|
||||||
color: vec![],
|
|
||||||
depth: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for _ in 0..per {
|
|
||||||
for &buffer in &b {
|
|
||||||
graph.push(Use {
|
|
||||||
buffer,
|
|
||||||
write: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
graph.push(Use {
|
|
||||||
buffer: shared,
|
|
||||||
write: false,
|
|
||||||
});
|
|
||||||
if pass {
|
|
||||||
graph.push(EndRendering);
|
|
||||||
}
|
|
||||||
|
|
||||||
graph
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_overlapping(n_groups: usize, per: usize) -> RenderGraph {
|
|
||||||
let mut graph = RenderGraph::new();
|
|
||||||
let buffers: Vec<Buffer> = (0..n_groups)
|
|
||||||
.map(|_| {
|
|
||||||
graph.create_buffer(GraphBufferDesc {
|
|
||||||
size: 4096,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let shared = graph.create_buffer(GraphBufferDesc {
|
|
||||||
size: 4096,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
graph.push(BeginRendering {
|
|
||||||
color: vec![],
|
|
||||||
depth: None,
|
|
||||||
});
|
|
||||||
for _ in 0..per {
|
|
||||||
for &buffer in &buffers {
|
|
||||||
graph.push(Use {
|
|
||||||
buffer,
|
|
||||||
write: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
graph.push(Use {
|
|
||||||
buffer: shared,
|
|
||||||
write: true,
|
|
||||||
});
|
|
||||||
graph.push(EndRendering);
|
|
||||||
|
|
||||||
graph.push(BeginRendering {
|
|
||||||
color: vec![],
|
|
||||||
depth: None,
|
|
||||||
});
|
|
||||||
for _ in 0..per {
|
|
||||||
for &buffer in &buffers {
|
|
||||||
graph.push(Use {
|
|
||||||
buffer,
|
|
||||||
write: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
graph.push(Use {
|
|
||||||
buffer: shared,
|
|
||||||
write: false,
|
|
||||||
});
|
|
||||||
graph.push(EndRendering);
|
|
||||||
|
|
||||||
graph
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_aliasing(
|
|
||||||
n_groups: usize,
|
|
||||||
per: usize,
|
|
||||||
bridge: &Arc<DeviceBuffer>,
|
|
||||||
) -> RenderGraph {
|
|
||||||
let mut graph = RenderGraph::new();
|
|
||||||
let a: Vec<Buffer> = (0..n_groups)
|
|
||||||
.map(|_| {
|
|
||||||
graph.create_buffer(GraphBufferDesc {
|
|
||||||
size: 4096,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let b: Vec<Buffer> = (0..n_groups)
|
|
||||||
.map(|_| {
|
|
||||||
graph.create_buffer(GraphBufferDesc {
|
|
||||||
size: 4096,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let bridge = graph
|
|
||||||
.import_buffer(bridge.clone(), ResourceState::default())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Keep the first set outside the pass; render-pass resources interfere with
|
|
||||||
// each other by design and otherwise prevent placement reuse.
|
|
||||||
for _ in 0..per {
|
|
||||||
for &buffer in &a {
|
|
||||||
graph.push(Use {
|
|
||||||
buffer,
|
|
||||||
write: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for &buffer in &a {
|
|
||||||
graph.push(UseWithBridge { buffer, bridge });
|
|
||||||
}
|
|
||||||
|
|
||||||
graph.push(BeginRendering {
|
|
||||||
color: vec![],
|
|
||||||
depth: None,
|
|
||||||
});
|
|
||||||
for &buffer in &b {
|
|
||||||
graph.push(UseWithBridge { buffer, bridge });
|
|
||||||
}
|
|
||||||
for _ in 0..per {
|
|
||||||
for &buffer in &b {
|
|
||||||
graph.push(Use {
|
|
||||||
buffer,
|
|
||||||
write: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
graph.push(EndRendering);
|
|
||||||
|
|
||||||
graph
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compile_benchmarks(criterion: &mut Criterion) {
|
|
||||||
let mut group = criterion.benchmark_group("compile");
|
|
||||||
group
|
|
||||||
.measurement_time(Duration::from_secs(4))
|
|
||||||
.sample_size(20);
|
|
||||||
|
|
||||||
for (pass_name, pass) in [("no_pass", false), ("pass", true)] {
|
|
||||||
for per in [20, 50, 100] {
|
|
||||||
group.bench_function(format!("two_pass/{pass_name}/per_{per}"), |bencher| {
|
|
||||||
bencher.iter_batched(
|
|
||||||
|| build(50, per, pass),
|
|
||||||
|graph| {
|
|
||||||
let compiled = graph.compile().unwrap();
|
|
||||||
black_box(compiled.barriers().len())
|
|
||||||
},
|
|
||||||
BatchSize::LargeInput,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
group.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_benchmarks(criterion: &mut Criterion, device: &renderer_device::Device) {
|
|
||||||
let bridge = Arc::new(
|
|
||||||
DeviceBuffer::new(
|
|
||||||
device.clone(),
|
|
||||||
BufferDesc {
|
|
||||||
size: 4096,
|
|
||||||
usage: vk::BufferUsageFlags::STORAGE_BUFFER,
|
|
||||||
queue_families: QueueFlags::empty(),
|
|
||||||
mem_location: MemoryLocation::GpuOnly,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("create alias benchmark bridge buffer"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut group = criterion.benchmark_group("compile_resolve");
|
|
||||||
group
|
|
||||||
.measurement_time(Duration::from_secs(4))
|
|
||||||
.sample_size(20);
|
|
||||||
|
|
||||||
for (name, overlapping) in [("alias", false), ("no_alias", true)] {
|
|
||||||
for per in [20, 50, 100] {
|
|
||||||
let graph = if overlapping {
|
|
||||||
build_overlapping(50, per)
|
|
||||||
} else {
|
|
||||||
build_aliasing(50, per, &bridge)
|
|
||||||
};
|
|
||||||
let mut probe = graph.compile().unwrap();
|
|
||||||
probe.resolve(device).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
probe
|
|
||||||
.resource_lifetimes()
|
|
||||||
.values()
|
|
||||||
.any(|lifetime| lifetime.placement.aliased),
|
|
||||||
!overlapping,
|
|
||||||
"unexpected placement aliasing in {name}/per_{per}"
|
|
||||||
);
|
|
||||||
let alias_barriers = probe
|
|
||||||
.barriers()
|
|
||||||
.iter()
|
|
||||||
.flat_map(|barrier| &barrier.resources)
|
|
||||||
.filter(|resource| {
|
|
||||||
probe
|
|
||||||
.resource_lifetimes()
|
|
||||||
.get(&resource.id)
|
|
||||||
.is_some_and(|lifetime| lifetime.placement.aliased)
|
|
||||||
&& resource.origin != renderer_rendergraph::BarrierOrigin::Initial
|
|
||||||
})
|
|
||||||
.count();
|
|
||||||
assert_eq!(
|
|
||||||
alias_barriers > 0,
|
|
||||||
!overlapping,
|
|
||||||
"unexpected alias barriers in {name}/per_{per}"
|
|
||||||
);
|
|
||||||
|
|
||||||
group.bench_function(format!("{name}/per_{per}"), |bencher| {
|
|
||||||
bencher.iter_batched(
|
|
||||||
|| {
|
|
||||||
if overlapping {
|
|
||||||
build_overlapping(50, per)
|
|
||||||
} else {
|
|
||||||
build_aliasing(50, per, &bridge)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|graph| {
|
|
||||||
let mut compiled = graph.compile().unwrap();
|
|
||||||
compiled.resolve(device).unwrap();
|
|
||||||
black_box(compiled.barriers().len())
|
|
||||||
},
|
|
||||||
BatchSize::LargeInput,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
group.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn benchmarks(criterion: &mut Criterion) {
|
|
||||||
let test = TestVulkan::acquire();
|
|
||||||
|
|
||||||
compile_benchmarks(criterion);
|
|
||||||
|
|
||||||
if let Some(test) = &test {
|
|
||||||
resolve_benchmarks(criterion, test.device());
|
|
||||||
} else {
|
|
||||||
eprintln!("Skipping compile_resolve benchmarks: no Vulkan device is available");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
criterion_group!(benches, benchmarks);
|
|
||||||
criterion_main!(benches);
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
use std::{cell::RefCell, sync::Arc};
|
|
||||||
|
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
use renderer_resources::images::{ImageView, ImageViewDesc};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
recorder::{CommandIndex, CommandRecorder, RecordError, RecordedCommand, SideEffectMap},
|
recorder::{CommandIndex, CommandRecorder, RecordError, RecordedCommand, SideEffectMap},
|
||||||
|
|
@ -12,243 +9,6 @@ use crate::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Classification used by the compiler to validate render-pass spans.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum CommandKind {
|
|
||||||
Begin,
|
|
||||||
End,
|
|
||||||
Graphics,
|
|
||||||
Transfer,
|
|
||||||
Compute,
|
|
||||||
Other,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A view used by a render-pass attachment.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum AttachmentView {
|
|
||||||
/// A view the graph must create after resource resolution, described concretely.
|
|
||||||
Created(ImageViewDesc),
|
|
||||||
/// A caller-supplied view over an imported image.
|
|
||||||
Imported(Arc<ImageView>),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A color attachment used by dynamic rendering.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ColorAttachment {
|
|
||||||
pub texture: Texture,
|
|
||||||
pub view: AttachmentView,
|
|
||||||
/// Layout used while rendering (must be a legal rendering layout; validated later).
|
|
||||||
pub layout: vk::ImageLayout,
|
|
||||||
pub load_op: vk::AttachmentLoadOp,
|
|
||||||
pub store_op: vk::AttachmentStoreOp,
|
|
||||||
pub clear_value: vk::ClearValue,
|
|
||||||
/// Concrete subresource range covered by the view. Created-view sentinels are preserved for
|
|
||||||
/// compile-time validation.
|
|
||||||
pub range: vk::ImageSubresourceRange,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for ColorAttachment {
|
|
||||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
formatter
|
|
||||||
.debug_struct("ColorAttachment")
|
|
||||||
.field("texture", &self.texture)
|
|
||||||
.field("view", &self.view)
|
|
||||||
.field("layout", &self.layout)
|
|
||||||
.field("load_op", &self.load_op)
|
|
||||||
.field("store_op", &self.store_op)
|
|
||||||
.field("clear_value", &"<Vulkan clear value>")
|
|
||||||
.field("range", &self.range)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ColorAttachment {
|
|
||||||
/// Builds an attachment around a caller-supplied view of an imported image.
|
|
||||||
pub fn imported(
|
|
||||||
texture: Texture,
|
|
||||||
view: Arc<ImageView>,
|
|
||||||
layout: vk::ImageLayout,
|
|
||||||
load_op: vk::AttachmentLoadOp,
|
|
||||||
store_op: vk::AttachmentStoreOp,
|
|
||||||
clear_value: vk::ClearValue,
|
|
||||||
) -> Self {
|
|
||||||
let range = view.subresource_range();
|
|
||||||
Self {
|
|
||||||
texture,
|
|
||||||
view: AttachmentView::Imported(view),
|
|
||||||
layout,
|
|
||||||
load_op,
|
|
||||||
store_op,
|
|
||||||
clear_value,
|
|
||||||
range,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds an attachment whose view will be created after graph resolution.
|
|
||||||
pub fn created(
|
|
||||||
texture: Texture,
|
|
||||||
desc: ImageViewDesc,
|
|
||||||
layout: vk::ImageLayout,
|
|
||||||
load_op: vk::AttachmentLoadOp,
|
|
||||||
store_op: vk::AttachmentStoreOp,
|
|
||||||
clear_value: vk::ClearValue,
|
|
||||||
) -> Self {
|
|
||||||
let range = image_view_desc_range(&desc);
|
|
||||||
Self {
|
|
||||||
texture,
|
|
||||||
view: AttachmentView::Created(desc),
|
|
||||||
layout,
|
|
||||||
load_op,
|
|
||||||
store_op,
|
|
||||||
clear_value,
|
|
||||||
range,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A depth/stencil attachment used by dynamic rendering. The view's aspect mask
|
|
||||||
/// selects which depth/stencil attachment slots receive the view.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct DepthStencilAttachment {
|
|
||||||
pub texture: Texture,
|
|
||||||
pub view: AttachmentView,
|
|
||||||
pub layout: vk::ImageLayout,
|
|
||||||
pub depth_load_op: vk::AttachmentLoadOp,
|
|
||||||
pub depth_store_op: vk::AttachmentStoreOp,
|
|
||||||
pub stencil_load_op: vk::AttachmentLoadOp,
|
|
||||||
pub stencil_store_op: vk::AttachmentStoreOp,
|
|
||||||
pub clear_depth: f32,
|
|
||||||
pub clear_stencil: u32,
|
|
||||||
pub range: vk::ImageSubresourceRange,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DepthStencilAttachment {
|
|
||||||
/// Builds an attachment around a caller-supplied view of an imported image.
|
|
||||||
pub fn imported(
|
|
||||||
texture: Texture,
|
|
||||||
view: Arc<ImageView>,
|
|
||||||
layout: vk::ImageLayout,
|
|
||||||
depth_load_op: vk::AttachmentLoadOp,
|
|
||||||
depth_store_op: vk::AttachmentStoreOp,
|
|
||||||
stencil_load_op: vk::AttachmentLoadOp,
|
|
||||||
stencil_store_op: vk::AttachmentStoreOp,
|
|
||||||
clear_depth: f32,
|
|
||||||
clear_stencil: u32,
|
|
||||||
) -> Self {
|
|
||||||
let range = view.subresource_range();
|
|
||||||
Self {
|
|
||||||
texture,
|
|
||||||
view: AttachmentView::Imported(view),
|
|
||||||
layout,
|
|
||||||
depth_load_op,
|
|
||||||
depth_store_op,
|
|
||||||
stencil_load_op,
|
|
||||||
stencil_store_op,
|
|
||||||
clear_depth,
|
|
||||||
clear_stencil,
|
|
||||||
range,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds an attachment whose view will be created after graph resolution.
|
|
||||||
pub fn created(
|
|
||||||
texture: Texture,
|
|
||||||
desc: ImageViewDesc,
|
|
||||||
layout: vk::ImageLayout,
|
|
||||||
depth_load_op: vk::AttachmentLoadOp,
|
|
||||||
depth_store_op: vk::AttachmentStoreOp,
|
|
||||||
stencil_load_op: vk::AttachmentLoadOp,
|
|
||||||
stencil_store_op: vk::AttachmentStoreOp,
|
|
||||||
clear_depth: f32,
|
|
||||||
clear_stencil: u32,
|
|
||||||
) -> Self {
|
|
||||||
let range = image_view_desc_range(&desc);
|
|
||||||
Self {
|
|
||||||
texture,
|
|
||||||
view: AttachmentView::Created(desc),
|
|
||||||
layout,
|
|
||||||
depth_load_op,
|
|
||||||
depth_store_op,
|
|
||||||
stencil_load_op,
|
|
||||||
stencil_store_op,
|
|
||||||
clear_depth,
|
|
||||||
clear_stencil,
|
|
||||||
range,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn image_view_desc_range(desc: &ImageViewDesc) -> vk::ImageSubresourceRange {
|
|
||||||
vk::ImageSubresourceRange::default()
|
|
||||||
.aspect_mask(desc.aspect)
|
|
||||||
.base_mip_level(desc.mip_range.start)
|
|
||||||
.level_count(if desc.mip_range.end == vk::REMAINING_MIP_LEVELS {
|
|
||||||
vk::REMAINING_MIP_LEVELS
|
|
||||||
} else {
|
|
||||||
desc.mip_range.end - desc.mip_range.start
|
|
||||||
})
|
|
||||||
.base_array_layer(desc.layer_range.start)
|
|
||||||
.layer_count(if desc.layer_range.end == vk::REMAINING_ARRAY_LAYERS {
|
|
||||||
vk::REMAINING_ARRAY_LAYERS
|
|
||||||
} else {
|
|
||||||
desc.layer_range.end - desc.layer_range.start
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A borrowed summary of a render-pass attachment configuration.
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct RenderPassDesc<'a> {
|
|
||||||
pub color: &'a [ColorAttachment],
|
|
||||||
pub depth: Option<&'a DepthStencilAttachment>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Begins a dynamic-rendering instance.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct BeginRendering {
|
|
||||||
pub color: Vec<ColorAttachment>,
|
|
||||||
pub depth: Option<DepthStencilAttachment>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ends a dynamic-rendering instance.
|
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
|
||||||
pub struct EndRendering;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct RenderingScratch {
|
|
||||||
color_attachments: Vec<vk::RenderingAttachmentInfo<'static>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
thread_local! {
|
|
||||||
static RENDERING_SCRATCH: RefCell<RenderingScratch> = RefCell::new(RenderingScratch::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_attachment_view(
|
|
||||||
texture: Texture,
|
|
||||||
view: &AttachmentView,
|
|
||||||
recorder: &CommandRecorder<'_>,
|
|
||||||
) -> Result<(vk::ImageView, vk::Extent2D), RecordError> {
|
|
||||||
recorder.image(texture.id())?;
|
|
||||||
let view = match view {
|
|
||||||
AttachmentView::Created(desc) => recorder.view(texture.id(), desc)?,
|
|
||||||
AttachmentView::Imported(view) => view.clone(),
|
|
||||||
};
|
|
||||||
let range = view.subresource_range();
|
|
||||||
let image_extent = view.image().extent_2d();
|
|
||||||
let mip = range.base_mip_level.min(u32::BITS - 1);
|
|
||||||
let extent = vk::Extent2D {
|
|
||||||
width: (image_extent.width >> mip).max(1),
|
|
||||||
height: (image_extent.height >> mip).max(1),
|
|
||||||
};
|
|
||||||
Ok((view.raw(), extent))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rendering_extent(current: Option<vk::Extent2D>, next: vk::Extent2D) -> vk::Extent2D {
|
|
||||||
current.map_or(next, |current| vk::Extent2D {
|
|
||||||
width: current.width.min(next.width),
|
|
||||||
height: current.height.min(next.height),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A recorded operation declares its accesses before compilation and applies
|
/// A recorded operation declares its accesses before compilation and applies
|
||||||
/// itself to a Vulkan command buffer after resource binding.
|
/// itself to a Vulkan command buffer after resource binding.
|
||||||
pub trait Command: RecordedCommand {
|
pub trait Command: RecordedCommand {
|
||||||
|
|
@ -299,10 +59,6 @@ impl Command for CopyBuffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecordedCommand for CopyBuffer {
|
impl RecordedCommand for CopyBuffer {
|
||||||
fn kind(&self) -> CommandKind {
|
|
||||||
CommandKind::Transfer
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
||||||
let region = vk::BufferCopy2::default()
|
let region = vk::BufferCopy2::default()
|
||||||
.src_offset(self.src_range.offset)
|
.src_offset(self.src_range.offset)
|
||||||
|
|
@ -348,10 +104,6 @@ impl Command for ClearTexture {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecordedCommand for ClearTexture {
|
impl RecordedCommand for ClearTexture {
|
||||||
fn kind(&self) -> CommandKind {
|
|
||||||
CommandKind::Transfer
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
||||||
let value = vk::ClearColorValue {
|
let value = vk::ClearColorValue {
|
||||||
float32: self.color,
|
float32: self.color,
|
||||||
|
|
@ -467,14 +219,6 @@ impl Command for Draw {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecordedCommand for Draw {
|
impl RecordedCommand for Draw {
|
||||||
fn kind(&self) -> CommandKind {
|
|
||||||
CommandKind::Graphics
|
|
||||||
}
|
|
||||||
|
|
||||||
fn allowed_in_render_pass(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn preflight(&self) -> Result<(), RecordError> {
|
fn preflight(&self) -> Result<(), RecordError> {
|
||||||
validate_managed_descriptor_sets(&self.descriptor_sets).map(|_| ())
|
validate_managed_descriptor_sets(&self.descriptor_sets).map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
@ -591,160 +335,6 @@ impl RecordedCommand for Draw {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command for BeginRendering {
|
|
||||||
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
|
|
||||||
for attachment in &self.color {
|
|
||||||
let mut mask = vk::AccessFlags2::COLOR_ATTACHMENT_WRITE;
|
|
||||||
if attachment.load_op == vk::AttachmentLoadOp::LOAD {
|
|
||||||
mask |= vk::AccessFlags2::COLOR_ATTACHMENT_READ;
|
|
||||||
}
|
|
||||||
map.insert(
|
|
||||||
command_index,
|
|
||||||
attachment.texture.id(),
|
|
||||||
access(
|
|
||||||
vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT,
|
|
||||||
mask,
|
|
||||||
SubresourceScopeInner::Texture {
|
|
||||||
range: attachment.range,
|
|
||||||
layout: attachment.layout,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(attachment) = &self.depth {
|
|
||||||
let mut mask = vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_WRITE;
|
|
||||||
if attachment.depth_load_op == vk::AttachmentLoadOp::LOAD
|
|
||||||
|| attachment.stencil_load_op == vk::AttachmentLoadOp::LOAD
|
|
||||||
{
|
|
||||||
mask |= vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_READ;
|
|
||||||
}
|
|
||||||
map.insert(
|
|
||||||
command_index,
|
|
||||||
attachment.texture.id(),
|
|
||||||
access(
|
|
||||||
vk::PipelineStageFlags2::EARLY_FRAGMENT_TESTS
|
|
||||||
| vk::PipelineStageFlags2::LATE_FRAGMENT_TESTS,
|
|
||||||
mask,
|
|
||||||
SubresourceScopeInner::Texture {
|
|
||||||
range: attachment.range,
|
|
||||||
layout: attachment.layout,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RecordedCommand for BeginRendering {
|
|
||||||
fn kind(&self) -> CommandKind {
|
|
||||||
CommandKind::Begin
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_pass_desc(&self) -> Option<RenderPassDesc<'_>> {
|
|
||||||
Some(RenderPassDesc {
|
|
||||||
color: &self.color,
|
|
||||||
depth: self.depth.as_ref(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
|
||||||
RENDERING_SCRATCH.with(|scratch| {
|
|
||||||
let mut scratch = scratch.borrow_mut();
|
|
||||||
let attachments = &mut scratch.color_attachments;
|
|
||||||
attachments.clear();
|
|
||||||
|
|
||||||
let mut extent = None;
|
|
||||||
for attachment in &self.color {
|
|
||||||
let (view, attachment_extent) =
|
|
||||||
resolve_attachment_view(attachment.texture, &attachment.view, recorder)?;
|
|
||||||
extent = Some(rendering_extent(extent, attachment_extent));
|
|
||||||
attachments.push(
|
|
||||||
vk::RenderingAttachmentInfo::default()
|
|
||||||
.image_view(view)
|
|
||||||
.image_layout(attachment.layout)
|
|
||||||
.load_op(attachment.load_op)
|
|
||||||
.store_op(attachment.store_op)
|
|
||||||
.clear_value(attachment.clear_value),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut depth_info = None;
|
|
||||||
let mut stencil_info = None;
|
|
||||||
if let Some(attachment) = &self.depth {
|
|
||||||
let (view, attachment_extent) =
|
|
||||||
resolve_attachment_view(attachment.texture, &attachment.view, recorder)?;
|
|
||||||
extent = Some(rendering_extent(extent, attachment_extent));
|
|
||||||
let clear_value = vk::ClearValue {
|
|
||||||
depth_stencil: vk::ClearDepthStencilValue {
|
|
||||||
depth: attachment.clear_depth,
|
|
||||||
stencil: attachment.clear_stencil,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if attachment.range.aspect_mask.contains(vk::ImageAspectFlags::DEPTH) {
|
|
||||||
depth_info = Some(
|
|
||||||
vk::RenderingAttachmentInfo::default()
|
|
||||||
.image_view(view)
|
|
||||||
.image_layout(attachment.layout)
|
|
||||||
.load_op(attachment.depth_load_op)
|
|
||||||
.store_op(attachment.depth_store_op)
|
|
||||||
.clear_value(clear_value),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if attachment.range.aspect_mask.contains(vk::ImageAspectFlags::STENCIL) {
|
|
||||||
stencil_info = Some(
|
|
||||||
vk::RenderingAttachmentInfo::default()
|
|
||||||
.image_view(view)
|
|
||||||
.image_layout(attachment.layout)
|
|
||||||
.load_op(attachment.stencil_load_op)
|
|
||||||
.store_op(attachment.stencil_store_op)
|
|
||||||
.clear_value(clear_value),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let extent = extent.unwrap_or(vk::Extent2D {
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
});
|
|
||||||
let mut info = vk::RenderingInfo::default()
|
|
||||||
.render_area(vk::Rect2D {
|
|
||||||
offset: vk::Offset2D { x: 0, y: 0 },
|
|
||||||
extent,
|
|
||||||
})
|
|
||||||
.layer_count(1)
|
|
||||||
.color_attachments(attachments);
|
|
||||||
if let Some(depth) = depth_info.as_ref() {
|
|
||||||
info = info.depth_attachment(depth);
|
|
||||||
}
|
|
||||||
if let Some(stencil) = stencil_info.as_ref() {
|
|
||||||
info = info.stencil_attachment(stencil);
|
|
||||||
}
|
|
||||||
unsafe {
|
|
||||||
recorder
|
|
||||||
.device
|
|
||||||
.cmd_begin_rendering(recorder.command_buffer, &info)
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Command for EndRendering {
|
|
||||||
fn side_effects(&self, _command_index: CommandIndex, _map: &mut SideEffectMap) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RecordedCommand for EndRendering {
|
|
||||||
fn kind(&self) -> CommandKind {
|
|
||||||
CommandKind::End
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
|
||||||
unsafe { recorder.device.cmd_end_rendering(recorder.command_buffer) };
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_managed_descriptor_sets(
|
fn validate_managed_descriptor_sets(
|
||||||
sets: &[GraphDescriptorSet],
|
sets: &[GraphDescriptorSet],
|
||||||
) -> Result<Vec<vk::DescriptorSet>, RecordError> {
|
) -> Result<Vec<vk::DescriptorSet>, RecordError> {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,250 +0,0 @@
|
||||||
use ash::vk;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
CommandIndex, GraphError, SideEffectMap,
|
|
||||||
graph::{Kahn, RenderPassSpan},
|
|
||||||
recorder::BoxedCommand,
|
|
||||||
resources::{ResourceDesc, SubresourceScope, SubresourceScopeInner},
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SpanBuilder {
|
|
||||||
spans: Vec<RenderPassSpan>,
|
|
||||||
current: Option<CommandIndex>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SpanBuilder {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
spans: Vec::new(),
|
|
||||||
current: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn in_span(&self) -> bool {
|
|
||||||
self.current.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_begin(&self) -> Option<CommandIndex> {
|
|
||||||
self.current
|
|
||||||
}
|
|
||||||
|
|
||||||
fn begin(&mut self, index: CommandIndex) -> Result<(), GraphError> {
|
|
||||||
if self.current.is_some() {
|
|
||||||
Err(GraphError::NestedRenderPass)
|
|
||||||
} else {
|
|
||||||
self.current = Some(index);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn end(&mut self, end: CommandIndex) -> Result<(), GraphError> {
|
|
||||||
if let Some(begin) = self.current.take() {
|
|
||||||
self.spans.push(RenderPassSpan { begin, end });
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(GraphError::UnbalancedRenderPass)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finish(self) -> Result<Vec<RenderPassSpan>, GraphError> {
|
|
||||||
if self.current.is_some() {
|
|
||||||
Err(GraphError::UnbalancedRenderPass)
|
|
||||||
} else {
|
|
||||||
Ok(self.spans)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct Batch {
|
|
||||||
pub(crate) start: usize,
|
|
||||||
pub(crate) count: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct CommandOrderings {
|
|
||||||
pub(crate) execution_order: Vec<CommandIndex>,
|
|
||||||
pub(crate) batches: Vec<Batch>,
|
|
||||||
pub(crate) spans: Vec<RenderPassSpan>,
|
|
||||||
pub(crate) node_of_command: Vec<usize>,
|
|
||||||
pub(crate) node_representatives: Vec<CommandIndex>,
|
|
||||||
pub(crate) dependency_edges: Vec<Vec<usize>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn build_command_orderings(
|
|
||||||
resources: &[ResourceDesc],
|
|
||||||
commands: &[BoxedCommand],
|
|
||||||
side_effects: &mut SideEffectMap,
|
|
||||||
) -> Result<CommandOrderings, GraphError> {
|
|
||||||
let per_command_resource_access = side_effects.command_accesses();
|
|
||||||
let mut kahn = Kahn::new(commands.len());
|
|
||||||
let mut access_by_resource: Vec<Vec<(CommandIndex, Vec<SubresourceScope>)>> =
|
|
||||||
vec![Vec::new(); resources.len()];
|
|
||||||
|
|
||||||
let mut span_builder = SpanBuilder::new();
|
|
||||||
let mut node_of_command = vec![usize::MAX; commands.len()];
|
|
||||||
let mut node_representatives = Vec::new();
|
|
||||||
|
|
||||||
for (index, (command, accesses)) in commands
|
|
||||||
.iter()
|
|
||||||
.zip(per_command_resource_access)
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, c)| (CommandIndex::from(i), c))
|
|
||||||
{
|
|
||||||
let rep = span_builder.current_begin().unwrap_or(index);
|
|
||||||
kahn.add_node(rep.index());
|
|
||||||
|
|
||||||
// Node ids follow first encounter of each representative in command order.
|
|
||||||
let node = if node_of_command[rep.index()] == usize::MAX {
|
|
||||||
let node = node_representatives.len();
|
|
||||||
node_representatives.push(rep);
|
|
||||||
node
|
|
||||||
} else {
|
|
||||||
node_of_command[rep.index()]
|
|
||||||
};
|
|
||||||
node_of_command[index.index()] = node;
|
|
||||||
|
|
||||||
for access in accesses {
|
|
||||||
let Some(resource_access) = access_by_resource.get_mut(access.resource.index()) else {
|
|
||||||
// `validate_resources` reports invalid ids after dependency analysis.
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let priors = resource_access
|
|
||||||
.iter()
|
|
||||||
.take_while(|&&(c, _)| c < rep)
|
|
||||||
.filter(|(_, scopes)| scopes.iter().any(|scope| scope.conflicts(&access.scope)));
|
|
||||||
|
|
||||||
for &(prior, _) in priors {
|
|
||||||
kahn.add_edge(prior.index(), rep.index());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(&mut (last, ref mut scopes)) = resource_access.last_mut()
|
|
||||||
&& last == rep
|
|
||||||
{
|
|
||||||
scopes.push(access.scope);
|
|
||||||
} else {
|
|
||||||
resource_access.push((rep, vec![access.scope]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match command.kind() {
|
|
||||||
crate::CommandKind::Begin => {
|
|
||||||
if let Some(desc) = command.render_pass_desc() {
|
|
||||||
crate::graph::validate_render_pass_attachments(resources, desc)?;
|
|
||||||
}
|
|
||||||
span_builder.begin(index)?;
|
|
||||||
}
|
|
||||||
crate::CommandKind::Graphics => {
|
|
||||||
if !command.allowed_in_render_pass() {
|
|
||||||
return Err(GraphError::CommandNotAllowedInRenderPass(index));
|
|
||||||
}
|
|
||||||
if !span_builder.in_span() {
|
|
||||||
return Err(GraphError::DrawOutsideRenderPass(index));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
crate::CommandKind::Transfer
|
|
||||||
| crate::CommandKind::Compute
|
|
||||||
| crate::CommandKind::Other => {
|
|
||||||
if span_builder.in_span() && !command.allowed_in_render_pass() {
|
|
||||||
return Err(GraphError::CommandNotAllowedInRenderPass(index));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
crate::CommandKind::End => {
|
|
||||||
span_builder.end(index)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// spans are ordered by beginning command
|
|
||||||
let spans = span_builder.finish()?;
|
|
||||||
|
|
||||||
for span in &spans {
|
|
||||||
// Snapshot accesses intentionally predate lifetime-only attachment injection:
|
|
||||||
// these accesses extend lifetimes but must not add hazard edges.
|
|
||||||
if let Some(desc) = commands[span.begin.index()].render_pass_desc() {
|
|
||||||
for attachment in desc.color {
|
|
||||||
side_effects.insert_lifetime_only(
|
|
||||||
span.end,
|
|
||||||
attachment.texture.id(),
|
|
||||||
SubresourceScope {
|
|
||||||
stage: vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT,
|
|
||||||
access: vk::AccessFlags2::COLOR_ATTACHMENT_WRITE,
|
|
||||||
subresource: SubresourceScopeInner::Texture {
|
|
||||||
range: attachment.range,
|
|
||||||
layout: attachment.layout,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(attachment) = desc.depth {
|
|
||||||
side_effects.insert_lifetime_only(
|
|
||||||
span.end,
|
|
||||||
attachment.texture.id(),
|
|
||||||
SubresourceScope {
|
|
||||||
stage: vk::PipelineStageFlags2::EARLY_FRAGMENT_TESTS
|
|
||||||
| vk::PipelineStageFlags2::LATE_FRAGMENT_TESTS,
|
|
||||||
access: vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_WRITE,
|
|
||||||
subresource: SubresourceScopeInner::Texture {
|
|
||||||
range: attachment.range,
|
|
||||||
layout: attachment.layout,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut execution_order: Vec<CommandIndex> = Vec::with_capacity(commands.len());
|
|
||||||
let mut batches = Vec::<Batch>::new();
|
|
||||||
|
|
||||||
let mut span_iter = spans.iter().peekable();
|
|
||||||
|
|
||||||
while !kahn.is_complete() {
|
|
||||||
let first = execution_order.len();
|
|
||||||
|
|
||||||
for cmd in kahn.iter_batch().map(CommandIndex::from) {
|
|
||||||
use core::cmp::Ordering;
|
|
||||||
match span_iter.peek().map(|s| s.begin.cmp(&cmd)) {
|
|
||||||
Some(Ordering::Equal) => {
|
|
||||||
let span = span_iter.next().unwrap();
|
|
||||||
execution_order
|
|
||||||
.extend((span.begin.index()..=span.end.index()).map(CommandIndex::from));
|
|
||||||
}
|
|
||||||
_ => execution_order.push(cmd),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let count = execution_order.len() - first;
|
|
||||||
|
|
||||||
if count == 0 {
|
|
||||||
return Err(GraphError::DependencyCycle);
|
|
||||||
}
|
|
||||||
|
|
||||||
for command in &execution_order[first..] {
|
|
||||||
kahn.complete(command.index());
|
|
||||||
}
|
|
||||||
|
|
||||||
batches.push(Batch {
|
|
||||||
start: first,
|
|
||||||
count,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dependency_edges = vec![Vec::new(); node_representatives.len()];
|
|
||||||
for (node, representative) in node_representatives.iter().enumerate() {
|
|
||||||
let representative = representative.index();
|
|
||||||
let node_edges = &mut dependency_edges[node];
|
|
||||||
for &edge in &kahn.edges[representative] {
|
|
||||||
node_edges.push(node_of_command[edge]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(CommandOrderings {
|
|
||||||
execution_order,
|
|
||||||
batches,
|
|
||||||
spans,
|
|
||||||
node_of_command,
|
|
||||||
node_representatives,
|
|
||||||
dependency_edges,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -3,19 +3,10 @@
|
||||||
|
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
mod graph_compiler;
|
|
||||||
pub mod recorder;
|
pub mod recorder;
|
||||||
pub mod resources;
|
pub mod resources;
|
||||||
|
|
||||||
pub use commands::{
|
pub use graph::{CompiledGraph, GraphError, RenderGraph, ResourceInterference};
|
||||||
AttachmentView, BeginRendering, ColorAttachment, CommandKind, DepthStencilAttachment,
|
|
||||||
EndRendering, RenderPassDesc,
|
|
||||||
};
|
|
||||||
pub use graph::{
|
|
||||||
BarrierOrigin, CompiledGraph, GraphError, HappensBefore, MemoryBarrier, Placement, RenderGraph,
|
|
||||||
RenderPassSpan, ResourceFrontiers, ResourceInterference, ResourceLifetime, ResourceUse,
|
|
||||||
aliasing_permitted,
|
|
||||||
};
|
|
||||||
pub use recorder::{CommandIndex, CommandList, CommandRecorder, SideEffectMap};
|
pub use recorder::{CommandIndex, CommandList, CommandRecorder, SideEffectMap};
|
||||||
pub use resources::{
|
pub use resources::{
|
||||||
Buffer, BufferRange, BufferSlice, DescriptorResourceAccess, DescriptorResourceIdentity,
|
Buffer, BufferRange, BufferSlice, DescriptorResourceAccess, DescriptorResourceIdentity,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
use std::{
|
use std::collections::BTreeMap;
|
||||||
cell::RefCell,
|
|
||||||
collections::{BTreeMap, BTreeSet, HashMap},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
use renderer_resources::images::{ImageView, ImageViewDesc};
|
|
||||||
|
|
||||||
use crate::resources::{
|
use crate::resources::{
|
||||||
BufferRange, ResourceId, Subresource, SubresourceScope, SubresourceScopeInner,
|
BufferRange, ResourceId, Subresource, SubresourceScope, SubresourceScopeInner,
|
||||||
|
|
@ -43,7 +38,6 @@ pub struct ResourceAccess {
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct SideEffectMap {
|
pub struct SideEffectMap {
|
||||||
accesses: Vec<Vec<ResourceAccess>>,
|
accesses: Vec<Vec<ResourceAccess>>,
|
||||||
lifetime_only: BTreeSet<(usize, ResourceId)>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SideEffectMap {
|
impl SideEffectMap {
|
||||||
|
|
@ -87,23 +81,6 @@ impl SideEffectMap {
|
||||||
accesses.push(ResourceAccess { resource, scope });
|
accesses.push(ResourceAccess { resource, scope });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Record an access used only to extend a resource's lifetime through a command.
|
|
||||||
pub fn insert_lifetime_only(
|
|
||||||
&mut self,
|
|
||||||
command_index: CommandIndex,
|
|
||||||
resource: ResourceId,
|
|
||||||
scope: SubresourceScope,
|
|
||||||
) {
|
|
||||||
self.insert(command_index, resource, scope);
|
|
||||||
self.lifetime_only.insert((command_index.index(), resource));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether this command/resource access exists only for lifetime analysis.
|
|
||||||
pub fn is_lifetime_only(&self, command_index: CommandIndex, resource: ResourceId) -> bool {
|
|
||||||
self.lifetime_only
|
|
||||||
.contains(&(command_index.index(), resource))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn accesses(&self, command_index: CommandIndex) -> &[ResourceAccess] {
|
pub fn accesses(&self, command_index: CommandIndex) -> &[ResourceAccess] {
|
||||||
self.accesses
|
self.accesses
|
||||||
.get(command_index.0)
|
.get(command_index.0)
|
||||||
|
|
@ -115,12 +92,10 @@ impl SideEffectMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type BoxedCommand = Box<dyn RecordedCommand>;
|
|
||||||
|
|
||||||
/// A safe, type-erased temporary command buffer. Each command is owned until
|
/// A safe, type-erased temporary command buffer. Each command is owned until
|
||||||
/// compilation/recording, while its access declarations are stored separately.
|
/// compilation/recording, while its access declarations are stored separately.
|
||||||
pub struct CommandList {
|
pub struct CommandList {
|
||||||
pub(crate) commands: Vec<BoxedCommand>,
|
pub(crate) commands: Vec<Box<dyn RecordedCommand>>,
|
||||||
pub side_effects: SideEffectMap,
|
pub side_effects: SideEffectMap,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,18 +105,6 @@ pub trait RecordedCommand {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn kind(&self) -> crate::commands::CommandKind {
|
|
||||||
crate::commands::CommandKind::Other
|
|
||||||
}
|
|
||||||
|
|
||||||
fn allowed_in_render_pass(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_pass_desc(&self) -> Option<crate::commands::RenderPassDesc<'_>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError>;
|
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,7 +149,6 @@ impl Default for CommandList {
|
||||||
pub enum RecordError {
|
pub enum RecordError {
|
||||||
MissingBuffer(ResourceId),
|
MissingBuffer(ResourceId),
|
||||||
MissingImage(ResourceId),
|
MissingImage(ResourceId),
|
||||||
MissingView(ResourceId),
|
|
||||||
UnresolvedCreatedBuffer(ResourceId),
|
UnresolvedCreatedBuffer(ResourceId),
|
||||||
UnresolvedCreatedImage(ResourceId),
|
UnresolvedCreatedImage(ResourceId),
|
||||||
DescriptorSet(String),
|
DescriptorSet(String),
|
||||||
|
|
@ -199,18 +161,6 @@ pub struct CommandRecorder<'a> {
|
||||||
pub command_buffer: vk::CommandBuffer,
|
pub command_buffer: vk::CommandBuffer,
|
||||||
pub buffers: BTreeMap<ResourceId, vk::Buffer>,
|
pub buffers: BTreeMap<ResourceId, vk::Buffer>,
|
||||||
pub images: BTreeMap<ResourceId, vk::Image>,
|
pub images: BTreeMap<ResourceId, vk::Image>,
|
||||||
pub views: HashMap<(ResourceId, ImageViewDesc), Arc<ImageView>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct BarrierScratch {
|
|
||||||
memory: Vec<vk::MemoryBarrier2<'static>>,
|
|
||||||
buffers: Vec<vk::BufferMemoryBarrier2<'static>>,
|
|
||||||
images: Vec<vk::ImageMemoryBarrier2<'static>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
thread_local! {
|
|
||||||
static BARRIER_SCRATCH: RefCell<BarrierScratch> = RefCell::new(BarrierScratch::default());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CommandRecorder<'a> {
|
impl<'a> CommandRecorder<'a> {
|
||||||
|
|
@ -220,7 +170,6 @@ impl<'a> CommandRecorder<'a> {
|
||||||
command_buffer,
|
command_buffer,
|
||||||
buffers: BTreeMap::new(),
|
buffers: BTreeMap::new(),
|
||||||
images: BTreeMap::new(),
|
images: BTreeMap::new(),
|
||||||
views: HashMap::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,118 +187,61 @@ impl<'a> CommandRecorder<'a> {
|
||||||
.ok_or(RecordError::MissingImage(id))
|
.ok_or(RecordError::MissingImage(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a cached image view by graph resource and concrete view description.
|
|
||||||
pub fn view(
|
|
||||||
&self,
|
|
||||||
id: ResourceId,
|
|
||||||
desc: &ImageViewDesc,
|
|
||||||
) -> Result<Arc<ImageView>, RecordError> {
|
|
||||||
self.views
|
|
||||||
.get(&(id, image_view_desc_without_name(desc)))
|
|
||||||
.cloned()
|
|
||||||
.ok_or(RecordError::MissingView(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pipeline_barrier(&self, barrier: &super::graph::PipelineBarrier) {
|
pub fn pipeline_barrier(&self, barrier: &super::graph::PipelineBarrier) {
|
||||||
BARRIER_SCRATCH.with(|scratch| {
|
let buffer_barriers: Vec<_> = barrier
|
||||||
let mut scratch = scratch.borrow_mut();
|
.resources
|
||||||
scratch.memory.clear();
|
.iter()
|
||||||
scratch
|
.filter_map(|resource| match resource.range {
|
||||||
.memory
|
Subresource::Buffer(BufferRange { offset, size }) => {
|
||||||
.extend(barrier.memory_barriers.iter().map(|resource| {
|
self.buffers.get(&resource.id).map(|&buffer| {
|
||||||
vk::MemoryBarrier2::default()
|
let (src, dst) = if let Some(queue_transfer) = resource.queue_transfer {
|
||||||
|
(queue_transfer.src.index(), queue_transfer.dst.index())
|
||||||
|
} else {
|
||||||
|
(vk::QUEUE_FAMILY_IGNORED, vk::QUEUE_FAMILY_IGNORED)
|
||||||
|
};
|
||||||
|
|
||||||
|
vk::BufferMemoryBarrier2::default()
|
||||||
|
.src_stage_mask(resource.src_stage)
|
||||||
|
.src_access_mask(resource.src_access)
|
||||||
|
.dst_stage_mask(resource.dst_stage)
|
||||||
|
.dst_access_mask(resource.dst_access)
|
||||||
|
.src_queue_family_index(src)
|
||||||
|
.dst_queue_family_index(dst)
|
||||||
|
.buffer(buffer)
|
||||||
|
.offset(offset)
|
||||||
|
.size(size)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Subresource::Texture(_) => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let image_barriers: Vec<_> = barrier
|
||||||
|
.resources
|
||||||
|
.iter()
|
||||||
|
.filter_map(|resource| match resource.range {
|
||||||
|
Subresource::Texture(range) => self.images.get(&resource.id).map(|&image| {
|
||||||
|
vk::ImageMemoryBarrier2::default()
|
||||||
.src_stage_mask(resource.src_stage)
|
.src_stage_mask(resource.src_stage)
|
||||||
.src_access_mask(resource.src_access)
|
.src_access_mask(resource.src_access)
|
||||||
.dst_stage_mask(resource.dst_stage)
|
.dst_stage_mask(resource.dst_stage)
|
||||||
.dst_access_mask(resource.dst_access)
|
.dst_access_mask(resource.dst_access)
|
||||||
}));
|
.old_layout(resource.old_layout.unwrap_or(vk::ImageLayout::UNDEFINED))
|
||||||
|
.new_layout(resource.new_layout.unwrap_or(vk::ImageLayout::GENERAL))
|
||||||
|
.image(image)
|
||||||
|
.subresource_range(range)
|
||||||
|
}),
|
||||||
|
Subresource::Buffer(_) => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
scratch.buffers.clear();
|
let dependency = vk::DependencyInfo::default()
|
||||||
scratch
|
.buffer_memory_barriers(&buffer_barriers)
|
||||||
.buffers
|
.image_memory_barriers(&image_barriers);
|
||||||
.extend(
|
unsafe {
|
||||||
barrier
|
self.device
|
||||||
.resources
|
.cmd_pipeline_barrier2(self.command_buffer, &dependency);
|
||||||
.iter()
|
}
|
||||||
.filter_map(|resource| match resource.range {
|
|
||||||
Subresource::Buffer(BufferRange { offset, size }) => {
|
|
||||||
self.buffers.get(&resource.id).map(|&buffer| {
|
|
||||||
let (src, dst) =
|
|
||||||
if let Some(queue_transfer) = resource.queue_transfer {
|
|
||||||
(queue_transfer.src.index(), queue_transfer.dst.index())
|
|
||||||
} else {
|
|
||||||
(vk::QUEUE_FAMILY_IGNORED, vk::QUEUE_FAMILY_IGNORED)
|
|
||||||
};
|
|
||||||
|
|
||||||
vk::BufferMemoryBarrier2::default()
|
|
||||||
.src_stage_mask(resource.src_stage)
|
|
||||||
.src_access_mask(resource.src_access)
|
|
||||||
.dst_stage_mask(resource.dst_stage)
|
|
||||||
.dst_access_mask(resource.dst_access)
|
|
||||||
.src_queue_family_index(src)
|
|
||||||
.dst_queue_family_index(dst)
|
|
||||||
.buffer(buffer)
|
|
||||||
.offset(offset)
|
|
||||||
.size(size)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Subresource::Texture(_) => None,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
scratch.images.clear();
|
|
||||||
scratch
|
|
||||||
.images
|
|
||||||
.extend(
|
|
||||||
barrier
|
|
||||||
.resources
|
|
||||||
.iter()
|
|
||||||
.filter_map(|resource| match resource.range {
|
|
||||||
Subresource::Texture(range) => {
|
|
||||||
self.images.get(&resource.id).map(|&image| {
|
|
||||||
vk::ImageMemoryBarrier2::default()
|
|
||||||
.src_stage_mask(resource.src_stage)
|
|
||||||
.src_access_mask(resource.src_access)
|
|
||||||
.dst_stage_mask(resource.dst_stage)
|
|
||||||
.dst_access_mask(resource.dst_access)
|
|
||||||
.old_layout(
|
|
||||||
resource
|
|
||||||
.old_layout
|
|
||||||
.unwrap_or(vk::ImageLayout::UNDEFINED),
|
|
||||||
)
|
|
||||||
.new_layout(
|
|
||||||
resource.new_layout.unwrap_or(vk::ImageLayout::GENERAL),
|
|
||||||
)
|
|
||||||
.image(image)
|
|
||||||
.subresource_range(range)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Subresource::Buffer(_) => None,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
let dependency = vk::DependencyInfo::default()
|
|
||||||
.memory_barriers(&scratch.memory)
|
|
||||||
.buffer_memory_barriers(&scratch.buffers)
|
|
||||||
.image_memory_barriers(&scratch.images);
|
|
||||||
unsafe {
|
|
||||||
self.device
|
|
||||||
.cmd_pipeline_barrier2(self.command_buffer, &dependency);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn image_view_desc_without_name(desc: &ImageViewDesc) -> ImageViewDesc {
|
|
||||||
ImageViewDesc {
|
|
||||||
name: None,
|
|
||||||
flags: desc.flags,
|
|
||||||
kind: desc.kind,
|
|
||||||
format: desc.format,
|
|
||||||
components: desc.components,
|
|
||||||
aspect: desc.aspect,
|
|
||||||
mip_range: desc.mip_range,
|
|
||||||
layer_range: desc.layer_range,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -363,58 +255,6 @@ pub(crate) fn scope_range(scope: SubresourceScope) -> Subresource {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::graph::{MemoryBarrier, PipelineBarrier};
|
|
||||||
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
|
||||||
|
|
||||||
static MEMORY_BARRIER_COUNT: AtomicU32 = AtomicU32::new(0);
|
|
||||||
static MEMORY_BARRIER_SRC_STAGE: AtomicU64 = AtomicU64::new(0);
|
|
||||||
|
|
||||||
unsafe extern "system" fn capture_pipeline_barrier(
|
|
||||||
_device: vk::Device,
|
|
||||||
_command_buffer: vk::CommandBuffer,
|
|
||||||
dependency: *const vk::DependencyInfo<'_>,
|
|
||||||
) {
|
|
||||||
let dependency = unsafe { &*dependency };
|
|
||||||
MEMORY_BARRIER_COUNT.store(dependency.memory_barrier_count, Ordering::SeqCst);
|
|
||||||
let memory = unsafe { &*dependency.p_memory_barriers };
|
|
||||||
MEMORY_BARRIER_SRC_STAGE.store(memory.src_stage_mask.as_raw(), Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn pipeline_barrier_emits_memory_barriers_in_dependency_info() {
|
|
||||||
let device = unsafe {
|
|
||||||
ash::Device::load_with(
|
|
||||||
|name| {
|
|
||||||
if name.to_bytes() == b"vkCmdPipelineBarrier2" {
|
|
||||||
capture_pipeline_barrier as *const () as *const std::ffi::c_void
|
|
||||||
} else {
|
|
||||||
std::ptr::null()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
vk::Device::null(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let barrier = PipelineBarrier {
|
|
||||||
before_command: CommandIndex(0),
|
|
||||||
memory_barriers: vec![MemoryBarrier {
|
|
||||||
src_stage: vk::PipelineStageFlags2::COMPUTE_SHADER,
|
|
||||||
src_access: vk::AccessFlags2::SHADER_STORAGE_WRITE,
|
|
||||||
dst_stage: vk::PipelineStageFlags2::FRAGMENT_SHADER,
|
|
||||||
dst_access: vk::AccessFlags2::SHADER_READ,
|
|
||||||
}],
|
|
||||||
resources: tinyvec::TinyVec::new(),
|
|
||||||
};
|
|
||||||
MEMORY_BARRIER_COUNT.store(0, Ordering::SeqCst);
|
|
||||||
MEMORY_BARRIER_SRC_STAGE.store(0, Ordering::SeqCst);
|
|
||||||
|
|
||||||
CommandRecorder::new(&device, vk::CommandBuffer::null()).pipeline_barrier(&barrier);
|
|
||||||
|
|
||||||
assert_eq!(MEMORY_BARRIER_COUNT.load(Ordering::SeqCst), 1);
|
|
||||||
assert_eq!(
|
|
||||||
MEMORY_BARRIER_SRC_STAGE.load(Ordering::SeqCst),
|
|
||||||
vk::PipelineStageFlags2::COMPUTE_SHADER.as_raw()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_scope(
|
fn read_scope(
|
||||||
offset: u64,
|
offset: u64,
|
||||||
|
|
@ -435,7 +275,7 @@ mod tests {
|
||||||
let command = map.begin_command();
|
let command = map.begin_command();
|
||||||
map.insert(
|
map.insert(
|
||||||
command,
|
command,
|
||||||
ResourceId::from_u32(0),
|
ResourceId(0),
|
||||||
read_scope(
|
read_scope(
|
||||||
0,
|
0,
|
||||||
64,
|
64,
|
||||||
|
|
@ -445,7 +285,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
map.insert(
|
map.insert(
|
||||||
command,
|
command,
|
||||||
ResourceId::from_u32(0),
|
ResourceId(0),
|
||||||
read_scope(
|
read_scope(
|
||||||
16,
|
16,
|
||||||
16,
|
16,
|
||||||
|
|
@ -476,7 +316,7 @@ mod tests {
|
||||||
let command = reverse.begin_command();
|
let command = reverse.begin_command();
|
||||||
reverse.insert(
|
reverse.insert(
|
||||||
command,
|
command,
|
||||||
ResourceId::from_u32(1),
|
ResourceId(1),
|
||||||
read_scope(
|
read_scope(
|
||||||
16,
|
16,
|
||||||
16,
|
16,
|
||||||
|
|
@ -486,7 +326,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
reverse.insert(
|
reverse.insert(
|
||||||
command,
|
command,
|
||||||
ResourceId::from_u32(1),
|
ResourceId(1),
|
||||||
read_scope(
|
read_scope(
|
||||||
0,
|
0,
|
||||||
64,
|
64,
|
||||||
|
|
@ -520,7 +360,7 @@ mod tests {
|
||||||
let command = map.begin_command();
|
let command = map.begin_command();
|
||||||
map.insert(
|
map.insert(
|
||||||
command,
|
command,
|
||||||
ResourceId::from_u32(0),
|
ResourceId(0),
|
||||||
read_scope(
|
read_scope(
|
||||||
0,
|
0,
|
||||||
16,
|
16,
|
||||||
|
|
@ -530,7 +370,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
map.insert(
|
map.insert(
|
||||||
command,
|
command,
|
||||||
ResourceId::from_u32(0),
|
ResourceId(0),
|
||||||
read_scope(
|
read_scope(
|
||||||
8,
|
8,
|
||||||
16,
|
16,
|
||||||
|
|
@ -548,7 +388,7 @@ mod tests {
|
||||||
let command = map.begin_command();
|
let command = map.begin_command();
|
||||||
map.insert(
|
map.insert(
|
||||||
command,
|
command,
|
||||||
ResourceId::from_u32(0),
|
ResourceId(0),
|
||||||
read_scope(
|
read_scope(
|
||||||
0,
|
0,
|
||||||
64,
|
64,
|
||||||
|
|
@ -558,7 +398,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
map.insert(
|
map.insert(
|
||||||
command,
|
command,
|
||||||
ResourceId::from_u32(0),
|
ResourceId(0),
|
||||||
read_scope(
|
read_scope(
|
||||||
16,
|
16,
|
||||||
16,
|
16,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use std::{borrow::Cow, sync::Arc};
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
use renderer_device::device::MemoryLocation;
|
|
||||||
use renderer_resources::{
|
use renderer_resources::{
|
||||||
buffers::Buffer as BufferResource,
|
buffers::Buffer as BufferResource,
|
||||||
images::{Image as ImageResource, ImageSubresourceExt},
|
images::{Image as ImageResource, ImageSubresourceExt},
|
||||||
|
|
@ -14,7 +13,7 @@ use renderer_resources::{
|
||||||
pub struct GraphBufferDesc {
|
pub struct GraphBufferDesc {
|
||||||
pub size: u64,
|
pub size: u64,
|
||||||
pub name: Option<Cow<'static, str>>,
|
pub name: Option<Cow<'static, str>>,
|
||||||
pub mem_location: MemoryLocation,
|
pub mem_location: gpu_allocator::MemoryLocation,
|
||||||
/// Additional Vulkan usages not visible in the graph's recorded commands.
|
/// Additional Vulkan usages not visible in the graph's recorded commands.
|
||||||
pub extra_usage: vk::BufferUsageFlags,
|
pub extra_usage: vk::BufferUsageFlags,
|
||||||
}
|
}
|
||||||
|
|
@ -25,7 +24,7 @@ impl Default for GraphBufferDesc {
|
||||||
size: 0,
|
size: 0,
|
||||||
name: None,
|
name: None,
|
||||||
// Transient graph resources default to device-local memory.
|
// Transient graph resources default to device-local memory.
|
||||||
mem_location: MemoryLocation::GpuOnly,
|
mem_location: gpu_allocator::MemoryLocation::GpuOnly,
|
||||||
extra_usage: vk::BufferUsageFlags::empty(),
|
extra_usage: vk::BufferUsageFlags::empty(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -44,7 +43,7 @@ pub struct GraphTextureDesc {
|
||||||
pub tiling: vk::ImageTiling,
|
pub tiling: vk::ImageTiling,
|
||||||
pub layout: vk::ImageLayout,
|
pub layout: vk::ImageLayout,
|
||||||
pub name: Option<Cow<'static, str>>,
|
pub name: Option<Cow<'static, str>>,
|
||||||
pub mem_location: MemoryLocation,
|
pub mem_location: gpu_allocator::MemoryLocation,
|
||||||
/// Additional Vulkan usages not visible in the graph's recorded commands.
|
/// Additional Vulkan usages not visible in the graph's recorded commands.
|
||||||
pub extra_usage: vk::ImageUsageFlags,
|
pub extra_usage: vk::ImageUsageFlags,
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +66,7 @@ impl Default for GraphTextureDesc {
|
||||||
tiling: vk::ImageTiling::OPTIMAL,
|
tiling: vk::ImageTiling::OPTIMAL,
|
||||||
layout: vk::ImageLayout::UNDEFINED,
|
layout: vk::ImageLayout::UNDEFINED,
|
||||||
name: None,
|
name: None,
|
||||||
mem_location: MemoryLocation::GpuOnly,
|
mem_location: gpu_allocator::MemoryLocation::GpuOnly,
|
||||||
extra_usage: vk::ImageUsageFlags::empty(),
|
extra_usage: vk::ImageUsageFlags::empty(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -75,14 +74,10 @@ impl Default for GraphTextureDesc {
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
pub struct ResourceId(u32);
|
pub struct ResourceId(pub u64);
|
||||||
|
|
||||||
impl ResourceId {
|
impl ResourceId {
|
||||||
pub fn from_u32(index: u32) -> Self {
|
pub(crate) fn index(self) -> usize {
|
||||||
Self(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn index(self) -> usize {
|
|
||||||
self.0 as usize
|
self.0 as usize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,552 +0,0 @@
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use ash::vk;
|
|
||||||
use renderer_device::{
|
|
||||||
device::{MemoryLocation, QueueFlags},
|
|
||||||
testing::TestVulkan,
|
|
||||||
};
|
|
||||||
use renderer_rendergraph::{
|
|
||||||
BeginRendering, ColorAttachment, EndRendering, RenderGraph,
|
|
||||||
commands::CopyBuffer,
|
|
||||||
commands::{ClearTexture, Command},
|
|
||||||
recorder::{CommandIndex, CommandRecorder, RecordError, RecordedCommand, SideEffectMap},
|
|
||||||
resources::{
|
|
||||||
BufferRange, GraphBufferDesc, GraphTextureDesc, ResourceId, ResourceState,
|
|
||||||
SubresourceScope, SubresourceScopeInner,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use renderer_resources::{
|
|
||||||
buffers::{Buffer, BufferDesc},
|
|
||||||
images::ImageViewDesc,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ScopedNoop(Vec<(ResourceId, SubresourceScope)>);
|
|
||||||
|
|
||||||
impl Command for ScopedNoop {
|
|
||||||
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
|
|
||||||
for &(resource, scope) in &self.0 {
|
|
||||||
map.insert(command_index, resource, scope);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RecordedCommand for ScopedNoop {
|
|
||||||
fn apply(&mut self, _recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PassNoop(Vec<(ResourceId, SubresourceScope)>);
|
|
||||||
|
|
||||||
impl Command for PassNoop {
|
|
||||||
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
|
|
||||||
for &(resource, scope) in &self.0 {
|
|
||||||
map.insert(command_index, resource, scope);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RecordedCommand for PassNoop {
|
|
||||||
fn allowed_in_render_pass(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply(&mut self, _recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn buffer_scope() -> SubresourceScope {
|
|
||||||
SubresourceScope {
|
|
||||||
stage: vk::PipelineStageFlags2::COPY,
|
|
||||||
access: vk::AccessFlags2::TRANSFER_WRITE,
|
|
||||||
subresource: SubresourceScopeInner::Buffer(BufferRange {
|
|
||||||
offset: 0,
|
|
||||||
size: 256,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn image_scope() -> SubresourceScope {
|
|
||||||
SubresourceScope {
|
|
||||||
stage: vk::PipelineStageFlags2::COMPUTE_SHADER,
|
|
||||||
access: vk::AccessFlags2::SHADER_STORAGE_WRITE,
|
|
||||||
subresource: SubresourceScopeInner::Texture {
|
|
||||||
range: vk::ImageSubresourceRange {
|
|
||||||
aspect_mask: vk::ImageAspectFlags::COLOR,
|
|
||||||
base_mip_level: 0,
|
|
||||||
level_count: 1,
|
|
||||||
base_array_layer: 0,
|
|
||||||
layer_count: 1,
|
|
||||||
},
|
|
||||||
layout: vk::ImageLayout::GENERAL,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pass_buffer_read_scope() -> SubresourceScope {
|
|
||||||
SubresourceScope {
|
|
||||||
stage: vk::PipelineStageFlags2::VERTEX_SHADER,
|
|
||||||
access: vk::AccessFlags2::SHADER_READ,
|
|
||||||
subresource: SubresourceScopeInner::Buffer(BufferRange {
|
|
||||||
offset: 0,
|
|
||||||
size: 256,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pass_texture_read_scope() -> SubresourceScope {
|
|
||||||
SubresourceScope {
|
|
||||||
stage: vk::PipelineStageFlags2::FRAGMENT_SHADER,
|
|
||||||
access: vk::AccessFlags2::SHADER_SAMPLED_READ,
|
|
||||||
subresource: SubresourceScopeInner::Texture {
|
|
||||||
range: vk::ImageSubresourceRange {
|
|
||||||
aspect_mask: vk::ImageAspectFlags::COLOR,
|
|
||||||
base_mip_level: 0,
|
|
||||||
level_count: 1,
|
|
||||||
base_array_layer: 0,
|
|
||||||
layer_count: 1,
|
|
||||||
},
|
|
||||||
layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn supports_render_pass_test(test: &TestVulkan) -> bool {
|
|
||||||
let features = test.device().enabled_features().core13;
|
|
||||||
if features.synchronization2 != vk::TRUE {
|
|
||||||
eprintln!("Skipping graph render pass: synchronization2 is unsupported");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if features.dynamic_rendering != vk::TRUE {
|
|
||||||
eprintln!("Skipping graph render pass: dynamic_rendering is unsupported");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn submit_graph(test: &TestVulkan, graph: &mut renderer_rendergraph::CompiledGraph) {
|
|
||||||
let device = test.device();
|
|
||||||
let pool_info = vk::CommandPoolCreateInfo::default()
|
|
||||||
.queue_family_index(device.queues.graphics().family.index.index());
|
|
||||||
let pool = unsafe { device.dev().create_command_pool(&pool_info, None) }
|
|
||||||
.expect("create alias test command pool");
|
|
||||||
let allocation_info = vk::CommandBufferAllocateInfo::default()
|
|
||||||
.command_pool(pool)
|
|
||||||
.level(vk::CommandBufferLevel::PRIMARY)
|
|
||||||
.command_buffer_count(1);
|
|
||||||
let command_buffer = unsafe { device.dev().allocate_command_buffers(&allocation_info) }
|
|
||||||
.expect("allocate alias test command buffer")[0];
|
|
||||||
unsafe {
|
|
||||||
device
|
|
||||||
.dev()
|
|
||||||
.begin_command_buffer(command_buffer, &vk::CommandBufferBeginInfo::default())
|
|
||||||
.expect("begin alias test command buffer");
|
|
||||||
}
|
|
||||||
let mut recorder = CommandRecorder::new(device.dev(), command_buffer);
|
|
||||||
graph
|
|
||||||
.record_commands(&mut recorder)
|
|
||||||
.expect("record alias graph");
|
|
||||||
unsafe {
|
|
||||||
device
|
|
||||||
.dev()
|
|
||||||
.end_command_buffer(command_buffer)
|
|
||||||
.expect("end alias test command buffer");
|
|
||||||
let command_buffers = [command_buffer];
|
|
||||||
let submit = vk::SubmitInfo::default().command_buffers(&command_buffers);
|
|
||||||
device
|
|
||||||
.dev()
|
|
||||||
.queue_submit(device.queues.graphics().raw, &[submit], vk::Fence::null())
|
|
||||||
.expect("submit alias graph");
|
|
||||||
}
|
|
||||||
device.wait_idle().expect("wait for alias graph idle");
|
|
||||||
unsafe { device.dev().destroy_command_pool(pool, None) };
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn imported_buffer_copy_records_under_validation() {
|
|
||||||
let Some(test) = TestVulkan::acquire() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let device = test.device();
|
|
||||||
if device.enabled_features().core13.synchronization2 != vk::TRUE {
|
|
||||||
eprintln!("Skipping graph recording: synchronization2 is unsupported");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let make_buffer = || {
|
|
||||||
Arc::new(
|
|
||||||
Buffer::new(
|
|
||||||
device.clone(),
|
|
||||||
BufferDesc {
|
|
||||||
size: 256,
|
|
||||||
usage: vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST,
|
|
||||||
queue_families: QueueFlags::empty(),
|
|
||||||
mem_location: MemoryLocation::CpuToGpu,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("create graph test buffer"),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let source = make_buffer();
|
|
||||||
let destination = make_buffer();
|
|
||||||
let mut graph = RenderGraph::new();
|
|
||||||
let source_id = graph
|
|
||||||
.import_buffer(source, ResourceState::default())
|
|
||||||
.expect("import source buffer");
|
|
||||||
let destination_id = graph
|
|
||||||
.import_buffer(destination, ResourceState::default())
|
|
||||||
.expect("import destination buffer");
|
|
||||||
graph.push(CopyBuffer {
|
|
||||||
src: source_id,
|
|
||||||
src_range: BufferRange {
|
|
||||||
offset: 0,
|
|
||||||
size: 256,
|
|
||||||
},
|
|
||||||
dst: destination_id,
|
|
||||||
dst_range: BufferRange {
|
|
||||||
offset: 0,
|
|
||||||
size: 256,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
let mut graph = graph.compile().expect("compile imported buffer graph");
|
|
||||||
|
|
||||||
let pool_info = vk::CommandPoolCreateInfo::default()
|
|
||||||
.queue_family_index(device.queues.graphics().family.index.index());
|
|
||||||
let pool = unsafe { device.dev().create_command_pool(&pool_info, None) }
|
|
||||||
.expect("create graph command pool");
|
|
||||||
let allocation_info = vk::CommandBufferAllocateInfo::default()
|
|
||||||
.command_pool(pool)
|
|
||||||
.level(vk::CommandBufferLevel::PRIMARY)
|
|
||||||
.command_buffer_count(1);
|
|
||||||
let command_buffer = unsafe { device.dev().allocate_command_buffers(&allocation_info) }
|
|
||||||
.expect("allocate graph command buffer")[0];
|
|
||||||
unsafe {
|
|
||||||
device
|
|
||||||
.dev()
|
|
||||||
.begin_command_buffer(command_buffer, &vk::CommandBufferBeginInfo::default())
|
|
||||||
.expect("begin graph command buffer");
|
|
||||||
}
|
|
||||||
let mut recorder = CommandRecorder::new(device.dev(), command_buffer);
|
|
||||||
graph
|
|
||||||
.record_commands(&mut recorder)
|
|
||||||
.expect("record imported buffer graph");
|
|
||||||
unsafe {
|
|
||||||
device
|
|
||||||
.dev()
|
|
||||||
.end_command_buffer(command_buffer)
|
|
||||||
.expect("end graph command buffer");
|
|
||||||
device.dev().destroy_command_pool(pool, None);
|
|
||||||
}
|
|
||||||
device.wait_idle().expect("wait for graph device idle");
|
|
||||||
test.assert_no_validation_errors();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolved_buffers_alias_and_submit_with_scoped_barrier() {
|
|
||||||
let Some(test) = TestVulkan::acquire() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let device = test.device();
|
|
||||||
if device.enabled_features().core13.synchronization2 != vk::TRUE {
|
|
||||||
eprintln!("Skipping graph aliasing: synchronization2 is unsupported");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let bridge = Arc::new(
|
|
||||||
Buffer::new(
|
|
||||||
device.clone(),
|
|
||||||
BufferDesc {
|
|
||||||
size: 256,
|
|
||||||
usage: vk::BufferUsageFlags::TRANSFER_DST,
|
|
||||||
mem_location: MemoryLocation::CpuToGpu,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("create dependency bridge buffer"),
|
|
||||||
);
|
|
||||||
let mut graph = RenderGraph::new();
|
|
||||||
let first = graph.create_buffer(GraphBufferDesc {
|
|
||||||
size: 256,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
let second = graph.create_buffer(GraphBufferDesc {
|
|
||||||
size: 256,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
let bridge = graph
|
|
||||||
.import_buffer(bridge, ResourceState::default())
|
|
||||||
.expect("import dependency bridge");
|
|
||||||
graph.push(ScopedNoop(vec![
|
|
||||||
(first.id(), buffer_scope()),
|
|
||||||
(bridge.id(), buffer_scope()),
|
|
||||||
]));
|
|
||||||
graph.push(ScopedNoop(vec![
|
|
||||||
(bridge.id(), buffer_scope()),
|
|
||||||
(second.id(), buffer_scope()),
|
|
||||||
]));
|
|
||||||
|
|
||||||
let mut compiled = graph.compile().expect("compile buffer alias graph");
|
|
||||||
compiled
|
|
||||||
.resolve(device)
|
|
||||||
.expect("resolve buffer alias graph");
|
|
||||||
compiled
|
|
||||||
.resolve(device)
|
|
||||||
.expect("resolving an already-resolved graph is idempotent");
|
|
||||||
let first_placement = compiled.resource_lifetimes()[&first.id()].placement;
|
|
||||||
let second_placement = compiled.resource_lifetimes()[&second.id()].placement;
|
|
||||||
assert_eq!(first_placement.pool, second_placement.pool);
|
|
||||||
assert_eq!(first_placement.offset, second_placement.offset);
|
|
||||||
assert!(first_placement.aliased && second_placement.aliased);
|
|
||||||
let alias_barrier = compiled
|
|
||||||
.barriers()
|
|
||||||
.iter()
|
|
||||||
.find(|barrier| barrier.before_command == CommandIndex(1))
|
|
||||||
.expect("incoming buffer's first use has an alias barrier");
|
|
||||||
assert!(alias_barrier.memory_barriers.is_empty());
|
|
||||||
let incoming = alias_barrier
|
|
||||||
.resources
|
|
||||||
.iter()
|
|
||||||
.find(|resource| resource.id == second.id())
|
|
||||||
.expect("alias barrier is scoped to the incoming buffer");
|
|
||||||
assert_eq!(incoming.src_stage, vk::PipelineStageFlags2::COPY);
|
|
||||||
assert_eq!(incoming.src_access, vk::AccessFlags2::TRANSFER_WRITE);
|
|
||||||
assert_eq!(incoming.dst_stage, vk::PipelineStageFlags2::COPY);
|
|
||||||
assert_eq!(incoming.dst_access, vk::AccessFlags2::TRANSFER_WRITE);
|
|
||||||
|
|
||||||
submit_graph(&test, &mut compiled);
|
|
||||||
test.assert_no_validation_errors();
|
|
||||||
drop(compiled);
|
|
||||||
device
|
|
||||||
.wait_idle()
|
|
||||||
.expect("wait before shared allocation teardown");
|
|
||||||
test.assert_no_validation_errors();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolved_images_alias_and_submit_with_undefined_transition() {
|
|
||||||
let Some(test) = TestVulkan::acquire() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let device = test.device();
|
|
||||||
if device.enabled_features().core13.synchronization2 != vk::TRUE {
|
|
||||||
eprintln!("Skipping graph aliasing: synchronization2 is unsupported");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let bridge = Arc::new(
|
|
||||||
Buffer::new(
|
|
||||||
device.clone(),
|
|
||||||
BufferDesc {
|
|
||||||
size: 256,
|
|
||||||
usage: vk::BufferUsageFlags::TRANSFER_DST,
|
|
||||||
mem_location: MemoryLocation::CpuToGpu,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("create dependency bridge buffer"),
|
|
||||||
);
|
|
||||||
let mut graph = RenderGraph::new();
|
|
||||||
let first = graph.create_texture(GraphTextureDesc {
|
|
||||||
format: vk::Format::R8G8B8A8_UNORM,
|
|
||||||
extent: vk::Extent3D {
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
depth: 1,
|
|
||||||
},
|
|
||||||
extra_usage: vk::ImageUsageFlags::STORAGE,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
let second = graph.create_texture(GraphTextureDesc {
|
|
||||||
format: vk::Format::R8G8B8A8_UNORM,
|
|
||||||
extent: vk::Extent3D {
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
depth: 1,
|
|
||||||
},
|
|
||||||
extra_usage: vk::ImageUsageFlags::STORAGE,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
let bridge = graph
|
|
||||||
.import_buffer(bridge, ResourceState::default())
|
|
||||||
.expect("import dependency bridge");
|
|
||||||
graph.push(ScopedNoop(vec![
|
|
||||||
(first.id(), image_scope()),
|
|
||||||
(bridge.id(), buffer_scope()),
|
|
||||||
]));
|
|
||||||
graph.push(ScopedNoop(vec![
|
|
||||||
(bridge.id(), buffer_scope()),
|
|
||||||
(second.id(), image_scope()),
|
|
||||||
]));
|
|
||||||
|
|
||||||
let mut compiled = graph.compile().expect("compile image alias graph");
|
|
||||||
compiled.resolve(device).expect("resolve image alias graph");
|
|
||||||
compiled
|
|
||||||
.resolve(device)
|
|
||||||
.expect("resolving an already-resolved graph is idempotent");
|
|
||||||
let first_placement = compiled.resource_lifetimes()[&first.id()].placement;
|
|
||||||
let second_placement = compiled.resource_lifetimes()[&second.id()].placement;
|
|
||||||
assert_eq!(first_placement.pool, second_placement.pool);
|
|
||||||
assert_eq!(first_placement.offset, second_placement.offset);
|
|
||||||
assert!(first_placement.aliased && second_placement.aliased);
|
|
||||||
assert!(compiled.barriers().iter().any(|barrier| {
|
|
||||||
barrier.before_command == CommandIndex(1)
|
|
||||||
&& barrier.resources.iter().any(|resource| {
|
|
||||||
resource.id == second.id()
|
|
||||||
&& resource.old_layout == Some(vk::ImageLayout::UNDEFINED)
|
|
||||||
&& resource.src_access == vk::AccessFlags2::SHADER_STORAGE_WRITE
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
|
|
||||||
submit_graph(&test, &mut compiled);
|
|
||||||
test.assert_no_validation_errors();
|
|
||||||
drop(compiled);
|
|
||||||
device
|
|
||||||
.wait_idle()
|
|
||||||
.expect("wait before shared allocation teardown");
|
|
||||||
test.assert_no_validation_errors();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn created_render_pass_records_submits_and_validates() {
|
|
||||||
let Some(test) = TestVulkan::acquire() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if !supports_render_pass_test(&test) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let device = test.device();
|
|
||||||
let make_buffer = || {
|
|
||||||
Arc::new(
|
|
||||||
Buffer::new(
|
|
||||||
device.clone(),
|
|
||||||
BufferDesc {
|
|
||||||
size: 256,
|
|
||||||
usage: vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST,
|
|
||||||
queue_families: QueueFlags::empty(),
|
|
||||||
mem_location: MemoryLocation::CpuToGpu,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("create render-pass test buffer"),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let producer_source = make_buffer();
|
|
||||||
let producer_destination = make_buffer();
|
|
||||||
let unrelated_destination = make_buffer();
|
|
||||||
|
|
||||||
let mut graph = RenderGraph::new();
|
|
||||||
let target = graph.create_texture(GraphTextureDesc {
|
|
||||||
format: vk::Format::R8G8B8A8_UNORM,
|
|
||||||
extent: vk::Extent3D {
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
depth: 1,
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
let producer_source = graph
|
|
||||||
.import_buffer(producer_source, ResourceState::default())
|
|
||||||
.expect("import producer source");
|
|
||||||
let producer_destination = graph
|
|
||||||
.import_buffer(producer_destination, ResourceState::default())
|
|
||||||
.expect("import producer destination");
|
|
||||||
let unrelated_destination = graph
|
|
||||||
.import_buffer(unrelated_destination, ResourceState::default())
|
|
||||||
.expect("import unrelated destination");
|
|
||||||
let buffer_range = BufferRange {
|
|
||||||
offset: 0,
|
|
||||||
size: 256,
|
|
||||||
};
|
|
||||||
let image_range = vk::ImageSubresourceRange {
|
|
||||||
aspect_mask: vk::ImageAspectFlags::COLOR,
|
|
||||||
base_mip_level: 0,
|
|
||||||
level_count: 1,
|
|
||||||
base_array_layer: 0,
|
|
||||||
layer_count: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
graph.push(ClearTexture {
|
|
||||||
texture: target,
|
|
||||||
range: image_range,
|
|
||||||
layout: vk::ImageLayout::TRANSFER_DST_OPTIMAL,
|
|
||||||
color: [0.1, 0.2, 0.3, 1.0],
|
|
||||||
});
|
|
||||||
let producer = graph.push(CopyBuffer {
|
|
||||||
src: producer_source,
|
|
||||||
src_range: buffer_range,
|
|
||||||
dst: producer_destination,
|
|
||||||
dst_range: buffer_range,
|
|
||||||
});
|
|
||||||
let begin = graph.push(BeginRendering {
|
|
||||||
color: vec![ColorAttachment::created(
|
|
||||||
target,
|
|
||||||
ImageViewDesc::color_2d(),
|
|
||||||
vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
|
|
||||||
vk::AttachmentLoadOp::LOAD,
|
|
||||||
vk::AttachmentStoreOp::STORE,
|
|
||||||
vk::ClearValue::default(),
|
|
||||||
)],
|
|
||||||
depth: None,
|
|
||||||
});
|
|
||||||
let interior = graph.push(PassNoop(vec![(
|
|
||||||
producer_destination.id(),
|
|
||||||
pass_buffer_read_scope(),
|
|
||||||
)]));
|
|
||||||
let end = graph.push(EndRendering);
|
|
||||||
let post_end = graph.push(ScopedNoop(vec![(target.id(), pass_texture_read_scope())]));
|
|
||||||
let unrelated = graph.push(CopyBuffer {
|
|
||||||
src: producer_destination,
|
|
||||||
src_range: buffer_range,
|
|
||||||
dst: unrelated_destination,
|
|
||||||
dst_range: buffer_range,
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut compiled = graph.compile().expect("compile render-pass graph");
|
|
||||||
let execution_order = compiled.execution_order();
|
|
||||||
let position = |command| {
|
|
||||||
execution_order
|
|
||||||
.iter()
|
|
||||||
.position(|&item| item == command)
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
assert!(position(begin) < position(interior));
|
|
||||||
assert!(position(interior) < position(end));
|
|
||||||
assert!(position(unrelated) > position(end));
|
|
||||||
let begin_barrier = compiled
|
|
||||||
.barriers()
|
|
||||||
.iter()
|
|
||||||
.find(|barrier| barrier.before_command == begin)
|
|
||||||
.expect("incoming barriers are hoisted before BeginRendering");
|
|
||||||
assert!(begin_barrier.resources.iter().any(|resource| {
|
|
||||||
resource.id == target.id()
|
|
||||||
&& resource.old_layout == Some(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
|
|
||||||
&& resource.new_layout == Some(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
|
||||||
}));
|
|
||||||
assert!(begin_barrier.resources.iter().any(|resource| {
|
|
||||||
resource.id == producer_destination.id()
|
|
||||||
&& resource.origin == renderer_rendergraph::BarrierOrigin::Command(producer)
|
|
||||||
}));
|
|
||||||
assert!(compiled.barriers().iter().any(|barrier| {
|
|
||||||
barrier.before_command == post_end
|
|
||||||
&& barrier.resources.iter().any(|resource| {
|
|
||||||
resource.id == target.id()
|
|
||||||
&& resource.old_layout == Some(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
|
||||||
&& resource.new_layout == Some(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
|
|
||||||
})
|
|
||||||
}));
|
|
||||||
|
|
||||||
compiled
|
|
||||||
.resolve(device)
|
|
||||||
.expect("resolve created attachment view");
|
|
||||||
assert!(
|
|
||||||
compiled
|
|
||||||
.created_view(target.id(), &ImageViewDesc::color_2d())
|
|
||||||
.is_some()
|
|
||||||
);
|
|
||||||
submit_graph(&test, &mut compiled);
|
|
||||||
test.assert_no_validation_errors();
|
|
||||||
drop(compiled);
|
|
||||||
device
|
|
||||||
.wait_idle()
|
|
||||||
.expect("wait before render target teardown");
|
|
||||||
test.assert_no_validation_errors();
|
|
||||||
}
|
|
||||||
|
|
@ -4,21 +4,19 @@
|
||||||
`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`.
|
`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
|
## Module map
|
||||||
- `lib.rs`: re-exports `Device`, `Error`, `Result`, `device`, and `make_extension` from `renderer_device`; declares public buffer, image, memory, pipeline, utility, and descriptor modules.
|
- `lib.rs`: re-exports `Device`, `Error`, `Result`, `device`, and `make_extension` from `renderer_device`; declares the public resource modules, including descriptor metadata and validation.
|
||||||
- `buffers.rs`: `BufferDesc` and `Buffer`; allocated and unallocated Vulkan buffer creation, memory requirement query/binding, raw access, and host mapping.
|
- `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/unallocated images from borrowed swapchain images.
|
- `images.rs`: `ImageDesc`, `Image`, `ImageViewDesc`, `ImageView`, subresource ranges, extents, offsets, and queue ownership. `ImageInner` distinguishes allocated images from borrowed swapchain images.
|
||||||
- `memory.rs`: re-exports device `MemoryRequirements`; exposes memory validation errors, `MemoryMember`, and `MemoryGroup` for aligned shared buffer/image allocations.
|
|
||||||
- `pipeline.rs`: shader-stage and graphics/compute pipeline descriptions; `DescriptorPool`, `DescriptorSetLayout`, `PipelineLayout`, `Sampler`, `ShaderModule`, and `Pipeline`.
|
- `pipeline.rs`: shader-stage and graphics/compute pipeline descriptions; `DescriptorPool`, `DescriptorSetLayout`, `PipelineLayout`, `Sampler`, `ShaderModule`, and `Pipeline`.
|
||||||
- `descriptors.rs`: managed descriptor binding/resource metadata, validation plans, errors, and pool-generation helpers; it remains independent of render-graph types.
|
- `descriptors.rs`: managed descriptor binding/resource metadata, validation plans, errors, and pool-generation helpers; it remains independent of render-graph types.
|
||||||
- `util.rs`: `eq_f32`, `ord_f32`, `hash_f32`, `F32`, and the crate-private `weak_vec` module / `WeakVec` used by image view tracking.
|
- `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
|
## 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.
|
- These modules are public from `renderer_resources`; the crate root re-exports the core device types and `device` module from `renderer_device` for resource construction.
|
||||||
- `BufferDesc` configures Vulkan buffer flags, size, usage, queue families, `renderer_device::device::MemoryLocation`, allocation strategy, and optional debug name. `Buffer::new` requests a VMA allocation through `DeviceInner::allocate_for_buffer` and binds it through `bind_buffer_memory`; `Buffer::new_unallocated` creates only the Vulkan handle. Its `memory_requirements` includes dedicated-allocation hints, and `bind_allocation(Arc<DeviceObject<GpuAllocation>>, offset)` validates then binds shared memory. `map` and `map_mut` return mapped slices when available.
|
- `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 VMA allocation block size. CPU-mapped locations select host-visible, host-coherent memory.
|
- `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 strategy/location. `Image::new` uses the device's VMA allocation and image-binding helpers.
|
- `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_unallocated` creates an unbound image. `memory_requirements` includes dedicated-allocation hints, and `bind_allocation(Arc<DeviceObject<GpuAllocation>>, offset)` validates then binds shared memory. `Image::new_with_allocation` accepts an explicit `renderer_device::device::Allocation` and validates size and memory-type compatibility.
|
- `Image::new` creates an allocated image. `Image::new_with_allocation` accepts an explicit `renderer_device::device::Allocation` and validates size and memory-type compatibility.
|
||||||
- `MemoryGroup::new(memory_type_index, property_flags, size, alignment)` fixes the shared pool's memory type and capacity. `add(MemoryMember::Buffer(&mut buffer))` / `add(MemoryMember::Image(&mut image))` queries requirements, checks that the selected type is allowed for every member, and packs each at an aligned offset; `allocate(&device)` creates aliasable memory, then consuming `bind_all()` transfers allocation references into the resources. The group does not retain its own allocation after binding.
|
|
||||||
- `Image::from_swapchain_raw(image, device, format, extent, usage)` builds an image wrapper for an image owned by a swapchain; renderer calls it from `Swapchain::acquire_image`.
|
- `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>`.
|
- `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.
|
- `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.
|
||||||
|
|
@ -48,8 +46,6 @@
|
||||||
- `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` 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.
|
- `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.
|
- Allocated images and buffers use `DeviceObject` for Vulkan handles and allocation ownership. Keep resource destruction ordered before freeing bound memory.
|
||||||
- Unallocated resources use `Allocation::Unmanaged` until bound. Shared bind APIs are one-shot; they validate offset alignment, allowed memory type, and allocation bounds before calling the device layer. Keep `DeviceObject<GpuAllocation>` on the resource after a successful bind so Vulkan handles are destroyed before their allocation reference.
|
|
||||||
- `MemoryGroup` only accepts members whose `memory_type_bits` include its selected type (the intersection across all members); it rejects dedicated-required resources. Group offsets use aligned bump packing, and allocation ownership transfers into each bound member at `bind_all`.
|
|
||||||
- `Buffer` equality compares the raw buffer handle; `Image` equality compares the raw image handle. Descriptor equality is separate from resource object identity.
|
- `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.
|
- `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.
|
- `ImageView` retains `Arc<Image>` so the source image outlives the view. `Image::create_view` returns `ManuallyDrop<ImageView>`; callers must transfer/drop it deliberately.
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,8 @@ testing = []
|
||||||
[dependencies]
|
[dependencies]
|
||||||
renderer_device = { path = "../renderer_device" }
|
renderer_device = { path = "../renderer_device" }
|
||||||
ash = { workspace = true }
|
ash = { workspace = true }
|
||||||
|
gpu-allocator = { workspace = true }
|
||||||
parking_lot = { workspace = true }
|
parking_lot = { workspace = true }
|
||||||
bytemuck = { workspace = true }
|
bytemuck = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" }
|
ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
renderer_device = { path = "../renderer_device", features = ["testing"] }
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
use std::{borrow::Cow, sync::Arc};
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
|
use gpu_allocator::vulkan::AllocationScheme;
|
||||||
use renderer_device::queue::QueueFamilyIndices;
|
use renderer_device::queue::QueueFamilyIndices;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Device,
|
Device,
|
||||||
device::{
|
device::{Allocation, AllocationStrategy, DeviceObject, QueueFlags},
|
||||||
Allocation, AllocationDesc, AllocationStrategy, DeviceObject, MemoryLocation, QueueFlags,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -18,7 +17,7 @@ pub struct BufferDesc {
|
||||||
pub usage: vk::BufferUsageFlags,
|
pub usage: vk::BufferUsageFlags,
|
||||||
pub queue_families: QueueFlags,
|
pub queue_families: QueueFlags,
|
||||||
|
|
||||||
pub mem_location: MemoryLocation,
|
pub mem_location: gpu_allocator::MemoryLocation,
|
||||||
pub alloc_scheme: AllocationStrategy,
|
pub alloc_scheme: AllocationStrategy,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,7 +65,7 @@ impl Default for BufferDesc {
|
||||||
size: Default::default(),
|
size: Default::default(),
|
||||||
usage: Default::default(),
|
usage: Default::default(),
|
||||||
queue_families: QueueFlags::empty(),
|
queue_families: QueueFlags::empty(),
|
||||||
mem_location: MemoryLocation::Unknown,
|
mem_location: gpu_allocator::MemoryLocation::Unknown,
|
||||||
alloc_scheme: AllocationStrategy::default(),
|
alloc_scheme: AllocationStrategy::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,122 +102,46 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(device: Device, desc: BufferDesc) -> crate::Result<Self> {
|
pub fn new(device: Device, desc: BufferDesc) -> crate::Result<Self> {
|
||||||
let buffer = Self::new_raw(device.clone(), &desc)?;
|
let (buffer, requirements) = Self::new_raw(device.clone(), &desc)?;
|
||||||
let alloc = device.allocate_for_buffer(
|
|
||||||
*buffer,
|
|
||||||
AllocationDesc {
|
|
||||||
name: desc.name.as_deref().unwrap_or_default(),
|
|
||||||
location: desc.mem_location,
|
|
||||||
strategy: desc.alloc_scheme,
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let buffer = Self {
|
let alloc =
|
||||||
buffer: BufferHandle::Owned(buffer),
|
device
|
||||||
desc,
|
.alloc2
|
||||||
alloc: Allocation::Owned(DeviceObject::new(device.clone(), alloc)),
|
.lock()
|
||||||
};
|
.allocate(&gpu_allocator::vulkan::AllocationCreateDesc {
|
||||||
device.bind_buffer_memory(
|
name: desc.name.as_deref().unwrap_or_default(),
|
||||||
buffer.alloc.allocation().expect("owned buffer allocation"),
|
requirements,
|
||||||
buffer.raw(),
|
location: desc.mem_location,
|
||||||
)?;
|
linear: true,
|
||||||
|
allocation_scheme: match desc.alloc_scheme {
|
||||||
|
AllocationStrategy::AllocatorManaged => {
|
||||||
|
AllocationScheme::GpuAllocatorManaged
|
||||||
|
}
|
||||||
|
AllocationStrategy::Dedicated => AllocationScheme::DedicatedBuffer(buffer),
|
||||||
|
},
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(buffer)
|
unsafe {
|
||||||
}
|
device
|
||||||
|
.raw
|
||||||
/// Creates a Vulkan buffer without allocating or binding memory.
|
.bind_buffer_memory(buffer, alloc.memory(), alloc.offset())?;
|
||||||
pub fn new_unallocated(device: Device, desc: BufferDesc) -> crate::Result<Self> {
|
}
|
||||||
let buffer = Self::new_raw(device, &desc)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
buffer: BufferHandle::Owned(buffer),
|
buffer: BufferHandle::Owned(DeviceObject::new_debug_named(
|
||||||
|
device.clone(),
|
||||||
|
buffer,
|
||||||
|
desc.name.clone(),
|
||||||
|
)),
|
||||||
desc,
|
desc,
|
||||||
alloc: Allocation::Unmanaged {
|
alloc: Allocation::Owned(DeviceObject::new(device, alloc)),
|
||||||
requirements: std::sync::OnceLock::new(),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the Vulkan memory requirements, including dedicated-allocation hints.
|
fn new_raw(
|
||||||
pub fn memory_requirements(&self) -> crate::memory::Result<crate::memory::MemoryRequirements> {
|
device: Device,
|
||||||
let Some(buffer) = self.owned() else {
|
desc: &BufferDesc,
|
||||||
return Err(crate::memory::Error::RequirementQuery(
|
) -> crate::Result<(vk::Buffer, vk::MemoryRequirements)> {
|
||||||
crate::Error::Unspecified,
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
let requirements_cache = match &self.alloc {
|
|
||||||
Allocation::Unmanaged { requirements } => Some(requirements),
|
|
||||||
Allocation::Owned(_) | Allocation::Shared(_) => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(requirements) = requirements_cache.and_then(std::sync::OnceLock::get) {
|
|
||||||
return Ok(*requirements);
|
|
||||||
}
|
|
||||||
|
|
||||||
let requirements = buffer
|
|
||||||
.device()
|
|
||||||
.buffer_memory_requirements(**buffer)
|
|
||||||
.map_err(crate::memory::Error::RequirementQuery)?;
|
|
||||||
|
|
||||||
if let Some(cache) = requirements_cache {
|
|
||||||
let _ = cache.set(requirements);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(requirements)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Binds this buffer into an aliasable allocation at an allocation-local offset.
|
|
||||||
///
|
|
||||||
/// Requirements are queried once and cached in the resource. The Vulkan bind remains the
|
|
||||||
/// authoritative validation.
|
|
||||||
pub fn bind_allocation(
|
|
||||||
&mut self,
|
|
||||||
allocation: Arc<DeviceObject<crate::device::GpuAllocation>>,
|
|
||||||
offset: u64,
|
|
||||||
) -> crate::memory::Result<()> {
|
|
||||||
if !matches!(self.alloc, Allocation::Unmanaged { .. }) {
|
|
||||||
return Err(crate::memory::Error::AlreadyBound);
|
|
||||||
}
|
|
||||||
|
|
||||||
let requirements = self.memory_requirements()?;
|
|
||||||
crate::memory::validate_binding(&requirements, allocation.as_ref(), offset)?;
|
|
||||||
let device = self.owned().expect("owned buffer").device();
|
|
||||||
|
|
||||||
self.alloc = device
|
|
||||||
.bind_buffer_at_offset(self.raw(), &requirements, allocation, offset)
|
|
||||||
.map_err(crate::memory::Error::Bind)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn validate_unallocated(&self) -> crate::memory::Result<()> {
|
|
||||||
if matches!(self.alloc, Allocation::Unmanaged { .. }) {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(crate::memory::Error::AlreadyBound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn validate_device(&self, device: &Device) -> crate::memory::Result<()> {
|
|
||||||
if self.owned().expect("unallocated buffer is owned").device() == device {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(crate::memory::Error::Bind(
|
|
||||||
crate::Error::AllocationDeviceMismatch,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn set_shared_allocation(
|
|
||||||
&mut self,
|
|
||||||
allocation: Arc<DeviceObject<crate::device::GpuAllocation>>,
|
|
||||||
) {
|
|
||||||
debug_assert!(matches!(self.alloc, Allocation::Unmanaged { .. }));
|
|
||||||
self.alloc = Allocation::Shared(allocation);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_raw(device: Device, desc: &BufferDesc) -> crate::Result<DeviceObject<vk::Buffer>> {
|
|
||||||
let queue_families = device.queues.family_indices(desc.queue_families);
|
let queue_families = device.queues.family_indices(desc.queue_families);
|
||||||
|
|
||||||
let sharing_mode = if queue_families.len() > 1 {
|
let sharing_mode = if queue_families.len() > 1 {
|
||||||
|
|
@ -233,11 +156,9 @@ impl Buffer {
|
||||||
.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)? };
|
||||||
Ok(DeviceObject::new_debug_named(
|
let mem_reqs = unsafe { device.dev().get_buffer_memory_requirements(buffer) };
|
||||||
device,
|
|
||||||
buffer,
|
Ok((buffer, mem_reqs))
|
||||||
desc.name.clone(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn map(&mut self) -> Option<&[u8]> {
|
pub fn map(&mut self) -> Option<&[u8]> {
|
||||||
|
|
@ -284,313 +205,11 @@ impl Buffer {
|
||||||
size,
|
size,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
alloc: Allocation::Unmanaged {
|
alloc: Allocation::Unmanaged,
|
||||||
requirements: std::sync::OnceLock::new(),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod allocation_validation_tests {
|
|
||||||
use ash::vk;
|
|
||||||
use renderer_device::testing::TestVulkan;
|
|
||||||
|
|
||||||
use super::{Buffer, BufferDesc};
|
|
||||||
use crate::{
|
|
||||||
device::MemoryLocation,
|
|
||||||
images::{Image, ImageDesc},
|
|
||||||
};
|
|
||||||
use renderer_device::device::AllocationStrategy;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn vma_mapping_and_dedicated_resources_under_validation() {
|
|
||||||
let test = TestVulkan::expect();
|
|
||||||
let device = test.device().clone();
|
|
||||||
|
|
||||||
fn assert_send_sync<T: Send + Sync>() {}
|
|
||||||
assert_send_sync::<Buffer>();
|
|
||||||
assert_send_sync::<Image>();
|
|
||||||
|
|
||||||
let mut mapped_buffer = Buffer::new(
|
|
||||||
device.clone(),
|
|
||||||
BufferDesc {
|
|
||||||
size: 64,
|
|
||||||
usage: vk::BufferUsageFlags::TRANSFER_SRC,
|
|
||||||
mem_location: MemoryLocation::CpuToGpu,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("create CpuToGpu buffer");
|
|
||||||
let allocation = mapped_buffer
|
|
||||||
.alloc
|
|
||||||
.allocation()
|
|
||||||
.expect("CpuToGpu buffer allocation");
|
|
||||||
let memory_type = test
|
|
||||||
.physical_device()
|
|
||||||
.properties
|
|
||||||
.memory_properties
|
|
||||||
.memory_types[allocation.memory_type_index() as usize];
|
|
||||||
assert!(memory_type.property_flags.contains(
|
|
||||||
vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT
|
|
||||||
));
|
|
||||||
|
|
||||||
let expected = (0..64).map(|byte| byte as u8).collect::<Vec<_>>();
|
|
||||||
mapped_buffer
|
|
||||||
.map_mut()
|
|
||||||
.expect("CpuToGpu buffer is mapped")
|
|
||||||
.copy_from_slice(&expected);
|
|
||||||
assert_eq!(
|
|
||||||
mapped_buffer.map().expect("CpuToGpu buffer remains mapped"),
|
|
||||||
expected
|
|
||||||
);
|
|
||||||
|
|
||||||
let dedicated_buffer = Buffer::new(
|
|
||||||
device.clone(),
|
|
||||||
BufferDesc {
|
|
||||||
size: 256,
|
|
||||||
usage: vk::BufferUsageFlags::TRANSFER_DST,
|
|
||||||
alloc_scheme: AllocationStrategy::Dedicated,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("create dedicated buffer");
|
|
||||||
assert!(
|
|
||||||
dedicated_buffer
|
|
||||||
.alloc
|
|
||||||
.allocation()
|
|
||||||
.expect("dedicated buffer allocation")
|
|
||||||
.is_dedicated(),
|
|
||||||
"VMA should allocate the dedicated buffer from its own memory block"
|
|
||||||
);
|
|
||||||
|
|
||||||
let dedicated_image = Image::new(
|
|
||||||
device,
|
|
||||||
ImageDesc {
|
|
||||||
format: vk::Format::R8G8B8A8_UNORM,
|
|
||||||
extent: vk::Extent3D {
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
depth: 1,
|
|
||||||
},
|
|
||||||
usage: vk::ImageUsageFlags::SAMPLED,
|
|
||||||
alloc_scheme: AllocationStrategy::Dedicated,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("create dedicated image");
|
|
||||||
assert!(
|
|
||||||
dedicated_image
|
|
||||||
.allocation()
|
|
||||||
.expect("dedicated image allocation")
|
|
||||||
.allocation()
|
|
||||||
.expect("dedicated image VMA allocation")
|
|
||||||
.is_dedicated(),
|
|
||||||
"VMA should allocate the dedicated image from its own memory block"
|
|
||||||
);
|
|
||||||
|
|
||||||
drop(dedicated_image);
|
|
||||||
drop(dedicated_buffer);
|
|
||||||
drop(mapped_buffer);
|
|
||||||
test.device().wait_idle().expect("wait for device idle");
|
|
||||||
test.assert_no_validation_errors();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(test, feature = "testing"))]
|
|
||||||
mod unallocated_memory_tests {
|
|
||||||
use ash::vk;
|
|
||||||
use renderer_device::testing::TestVulkan;
|
|
||||||
|
|
||||||
use super::{Buffer, BufferDesc};
|
|
||||||
use crate::{
|
|
||||||
device::MemoryLocation,
|
|
||||||
images::{Image, ImageDesc},
|
|
||||||
memory::{Error, MemoryGroup, MemoryMember},
|
|
||||||
};
|
|
||||||
|
|
||||||
fn buffer_desc() -> BufferDesc {
|
|
||||||
BufferDesc {
|
|
||||||
size: 256,
|
|
||||||
usage: vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST,
|
|
||||||
mem_location: MemoryLocation::GpuOnly,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unallocated_resources_query_requirements_and_bind() {
|
|
||||||
let test = TestVulkan::expect();
|
|
||||||
let device = test.device().clone();
|
|
||||||
let mut buffer = Buffer::new_unallocated(device.clone(), buffer_desc())
|
|
||||||
.expect("create unallocated buffer");
|
|
||||||
let buffer_requirements = buffer.memory_requirements().expect("buffer requirements");
|
|
||||||
assert!(buffer_requirements.size >= buffer.len());
|
|
||||||
assert!(matches!(
|
|
||||||
&buffer.alloc,
|
|
||||||
crate::device::Allocation::Unmanaged { .. }
|
|
||||||
));
|
|
||||||
|
|
||||||
let mut image = Image::new_unallocated(
|
|
||||||
device.clone(),
|
|
||||||
ImageDesc {
|
|
||||||
format: vk::Format::R8G8B8A8_UNORM,
|
|
||||||
extent: vk::Extent3D {
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
depth: 1,
|
|
||||||
},
|
|
||||||
usage: vk::ImageUsageFlags::TRANSFER_DST,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("create unallocated image");
|
|
||||||
let image_requirements = image.memory_requirements().expect("image requirements");
|
|
||||||
assert!(image_requirements.size > 0);
|
|
||||||
assert!(
|
|
||||||
image
|
|
||||||
.allocation()
|
|
||||||
.is_some_and(|allocation| allocation.allocation().is_none())
|
|
||||||
);
|
|
||||||
|
|
||||||
let buffer_memory = device
|
|
||||||
.allocate_memory(&buffer_requirements, vk::MemoryPropertyFlags::empty())
|
|
||||||
.expect("allocate buffer memory");
|
|
||||||
buffer
|
|
||||||
.bind_allocation(buffer_memory, 0)
|
|
||||||
.expect("bind buffer at offset zero");
|
|
||||||
assert!(matches!(
|
|
||||||
&buffer.alloc,
|
|
||||||
crate::device::Allocation::Shared(_)
|
|
||||||
));
|
|
||||||
|
|
||||||
let image_memory = device
|
|
||||||
.allocate_memory(&image_requirements, vk::MemoryPropertyFlags::empty())
|
|
||||||
.expect("allocate image memory");
|
|
||||||
image
|
|
||||||
.bind_allocation(image_memory, 0)
|
|
||||||
.expect("bind image at offset zero");
|
|
||||||
assert!(matches!(
|
|
||||||
image.allocation(),
|
|
||||||
Some(crate::device::Allocation::Shared(_))
|
|
||||||
));
|
|
||||||
drop(image);
|
|
||||||
drop(buffer);
|
|
||||||
test.device().wait_idle().expect("wait for device idle");
|
|
||||||
test.assert_no_validation_errors();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn bind_rejects_invalid_offsets_and_rebinding() {
|
|
||||||
let test = TestVulkan::expect();
|
|
||||||
let device = test.device().clone();
|
|
||||||
|
|
||||||
let mut buffer = Buffer::new_unallocated(device.clone(), buffer_desc())
|
|
||||||
.expect("create buffer for offset checks");
|
|
||||||
let requirements = buffer.memory_requirements().expect("buffer requirements");
|
|
||||||
let allocation = device
|
|
||||||
.allocate_memory(&requirements, vk::MemoryPropertyFlags::empty())
|
|
||||||
.expect("allocate offset-check memory");
|
|
||||||
if requirements.alignment > 1 {
|
|
||||||
assert!(matches!(
|
|
||||||
buffer.bind_allocation(allocation.clone(), 1),
|
|
||||||
Err(Error::MisalignedOffset { .. })
|
|
||||||
));
|
|
||||||
}
|
|
||||||
assert!(matches!(
|
|
||||||
buffer.bind_allocation(allocation.clone(), requirements.size),
|
|
||||||
Err(Error::OutOfBounds { .. })
|
|
||||||
));
|
|
||||||
buffer
|
|
||||||
.bind_allocation(allocation.clone(), 0)
|
|
||||||
.expect("first buffer bind");
|
|
||||||
assert!(matches!(
|
|
||||||
buffer.bind_allocation(allocation, 0),
|
|
||||||
Err(Error::AlreadyBound)
|
|
||||||
));
|
|
||||||
|
|
||||||
let allocated = device
|
|
||||||
.allocate_memory(&requirements, vk::MemoryPropertyFlags::empty())
|
|
||||||
.expect("allocate for synthetic type-bits validation");
|
|
||||||
let impossible = crate::memory::MemoryRequirements {
|
|
||||||
memory_type_bits: 0,
|
|
||||||
..requirements
|
|
||||||
};
|
|
||||||
assert!(matches!(
|
|
||||||
crate::memory::validate_binding(&impossible, allocated.as_ref(), 0),
|
|
||||||
Err(Error::IncompatibleMemoryType { .. })
|
|
||||||
));
|
|
||||||
|
|
||||||
drop(buffer);
|
|
||||||
test.device().wait_idle().expect("wait for device idle");
|
|
||||||
test.assert_no_validation_errors();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn memory_group_packs_and_binds_two_buffers() {
|
|
||||||
let test = TestVulkan::expect();
|
|
||||||
let device = test.device().clone();
|
|
||||||
let mut first =
|
|
||||||
Buffer::new_unallocated(device.clone(), buffer_desc()).expect("create first buffer");
|
|
||||||
let mut second =
|
|
||||||
Buffer::new_unallocated(device.clone(), buffer_desc()).expect("create second buffer");
|
|
||||||
let first_requirements = first.memory_requirements().expect("first requirements");
|
|
||||||
let second_requirements = second.memory_requirements().expect("second requirements");
|
|
||||||
let common_bits =
|
|
||||||
first_requirements.memory_type_bits & second_requirements.memory_type_bits;
|
|
||||||
assert_ne!(common_bits, 0, "the test adapter needs a common type");
|
|
||||||
let memory_type_index = common_bits.trailing_zeros();
|
|
||||||
let alignment = first_requirements
|
|
||||||
.alignment
|
|
||||||
.max(second_requirements.alignment);
|
|
||||||
let first_end = first_requirements.size;
|
|
||||||
let expected_second_offset = first_end
|
|
||||||
.checked_add(alignment - 1)
|
|
||||||
.expect("offset arithmetic")
|
|
||||||
/ alignment
|
|
||||||
* alignment;
|
|
||||||
let size = expected_second_offset + second_requirements.size;
|
|
||||||
let mut group = MemoryGroup::new(
|
|
||||||
memory_type_index,
|
|
||||||
vk::MemoryPropertyFlags::empty(),
|
|
||||||
size,
|
|
||||||
alignment,
|
|
||||||
)
|
|
||||||
.expect("create memory group");
|
|
||||||
let first_offset = group
|
|
||||||
.add(MemoryMember::Buffer(&mut first))
|
|
||||||
.expect("pack first buffer");
|
|
||||||
let second_offset = group
|
|
||||||
.add(MemoryMember::Buffer(&mut second))
|
|
||||||
.expect("pack second buffer");
|
|
||||||
assert_eq!(first_offset, 0);
|
|
||||||
assert_eq!(second_offset, expected_second_offset);
|
|
||||||
assert_eq!(second_offset % alignment, 0);
|
|
||||||
group.allocate(&device).expect("allocate shared memory");
|
|
||||||
group.bind_all().expect("bind group members");
|
|
||||||
assert!(matches!(&first.alloc, crate::device::Allocation::Shared(_)));
|
|
||||||
assert!(matches!(
|
|
||||||
&second.alloc,
|
|
||||||
crate::device::Allocation::Shared(_)
|
|
||||||
));
|
|
||||||
assert_eq!(
|
|
||||||
first
|
|
||||||
.alloc
|
|
||||||
.allocation()
|
|
||||||
.map(crate::device::GpuAllocation::memory),
|
|
||||||
second
|
|
||||||
.alloc
|
|
||||||
.allocation()
|
|
||||||
.map(crate::device::GpuAllocation::memory),
|
|
||||||
"both buffers retain the shared Vulkan memory block"
|
|
||||||
);
|
|
||||||
drop(first);
|
|
||||||
drop(second);
|
|
||||||
test.device().wait_idle().expect("wait for device idle");
|
|
||||||
test.assert_no_validation_errors();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Byte range selected by a buffer view.
|
/// Byte range selected by a buffer view.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct BufferRange {
|
pub struct BufferRange {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
|
mem::ManuallyDrop,
|
||||||
ops::{Add, Sub},
|
ops::{Add, Sub},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
device::{
|
device::{Allocation, AllocationStrategy, DeviceObject, QueueFlags},
|
||||||
Allocation, AllocationDesc, AllocationStrategy, DeviceObject, MemoryLocation, QueueFlags,
|
|
||||||
},
|
|
||||||
util::WeakVec,
|
util::WeakVec,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::Device;
|
use super::Device;
|
||||||
use ash::vk::{self, Handle};
|
use ash::vk::{self, Handle};
|
||||||
use ash_formats::FormatExt;
|
use ash_formats::FormatExt;
|
||||||
|
use gpu_allocator::vulkan::{AllocationCreateDesc, AllocationScheme};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use renderer_device::queue::QueueFamilyIndices;
|
use renderer_device::queue::QueueFamilyIndices;
|
||||||
|
|
||||||
|
|
@ -32,7 +32,7 @@ pub struct ImageDesc {
|
||||||
pub queue_families: QueueFlags,
|
pub queue_families: QueueFlags,
|
||||||
pub layout: vk::ImageLayout,
|
pub layout: vk::ImageLayout,
|
||||||
|
|
||||||
pub mem_location: MemoryLocation,
|
pub mem_location: gpu_allocator::MemoryLocation,
|
||||||
pub alloc_scheme: AllocationStrategy,
|
pub alloc_scheme: AllocationStrategy,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,7 +123,7 @@ impl Default for ImageDesc {
|
||||||
usage: Default::default(),
|
usage: Default::default(),
|
||||||
queue_families: QueueFlags::empty(),
|
queue_families: QueueFlags::empty(),
|
||||||
layout: vk::ImageLayout::UNDEFINED,
|
layout: vk::ImageLayout::UNDEFINED,
|
||||||
mem_location: MemoryLocation::Unknown,
|
mem_location: gpu_allocator::MemoryLocation::Unknown,
|
||||||
alloc_scheme: AllocationStrategy::AllocatorManaged,
|
alloc_scheme: AllocationStrategy::AllocatorManaged,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -187,15 +187,17 @@ pub struct Image {
|
||||||
|
|
||||||
impl Image {
|
impl Image {
|
||||||
pub fn new(device: Device, desc: ImageDesc) -> crate::Result<Self> {
|
pub fn new(device: Device, desc: ImageDesc) -> crate::Result<Self> {
|
||||||
let image = Self::new_raw(device.clone(), &desc)?;
|
let (image, requirements) = Self::new_raw(device.clone(), &desc)?;
|
||||||
let alloc = device.allocate_for_image(
|
let alloc = device.alloc2.lock().allocate(&AllocationCreateDesc {
|
||||||
*image,
|
name: desc.name.as_deref().unwrap_or(""),
|
||||||
AllocationDesc {
|
requirements,
|
||||||
name: desc.name.as_deref().unwrap_or(""),
|
location: desc.mem_location,
|
||||||
location: desc.mem_location,
|
linear: desc.tiling == vk::ImageTiling::LINEAR,
|
||||||
strategy: desc.alloc_scheme,
|
allocation_scheme: match desc.alloc_scheme {
|
||||||
|
AllocationStrategy::AllocatorManaged => AllocationScheme::GpuAllocatorManaged,
|
||||||
|
AllocationStrategy::Dedicated => AllocationScheme::DedicatedImage(image),
|
||||||
},
|
},
|
||||||
)?;
|
})?;
|
||||||
|
|
||||||
Self::new_with_allocation_unchecked(
|
Self::new_with_allocation_unchecked(
|
||||||
device.clone(),
|
device.clone(),
|
||||||
|
|
@ -205,138 +207,37 @@ impl Image {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a Vulkan image without allocating or binding memory.
|
fn new_with_allocation_unchecked(
|
||||||
pub fn new_unallocated(device: Device, desc: ImageDesc) -> crate::Result<Self> {
|
device: Device,
|
||||||
let image = Self::new_raw(device, &desc)?;
|
image: vk::Image,
|
||||||
|
allocation: Allocation,
|
||||||
|
desc: ImageDesc,
|
||||||
|
) -> crate::Result<Self> {
|
||||||
|
// bind memory
|
||||||
|
if let Some(alloc) = allocation.allocation() {
|
||||||
|
unsafe {
|
||||||
|
device
|
||||||
|
.raw
|
||||||
|
.bind_image_memory(image, alloc.memory(), alloc.offset())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
image: ImageInner::Allocated(
|
image: ImageInner::Allocated(
|
||||||
image,
|
DeviceObject::new_debug_named(device, image, desc.name.clone()),
|
||||||
Allocation::Unmanaged {
|
allocation,
|
||||||
requirements: std::sync::OnceLock::new(),
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
desc,
|
desc,
|
||||||
views: Default::default(),
|
views: Default::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the Vulkan memory requirements, including dedicated-allocation hints.
|
|
||||||
pub fn memory_requirements(&self) -> crate::memory::Result<crate::memory::MemoryRequirements> {
|
|
||||||
let ImageInner::Allocated(image, allocation) = &self.image else {
|
|
||||||
return Err(crate::memory::Error::RequirementQuery(
|
|
||||||
crate::Error::Unspecified,
|
|
||||||
));
|
|
||||||
};
|
|
||||||
let requirements_cache = match allocation {
|
|
||||||
Allocation::Unmanaged { requirements } => Some(requirements),
|
|
||||||
Allocation::Owned(_) | Allocation::Shared(_) => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(requirements) = requirements_cache.and_then(std::sync::OnceLock::get) {
|
|
||||||
return Ok(*requirements);
|
|
||||||
}
|
|
||||||
|
|
||||||
let requirements = image
|
|
||||||
.device()
|
|
||||||
.image_memory_requirements(**image)
|
|
||||||
.map_err(crate::memory::Error::RequirementQuery)?;
|
|
||||||
|
|
||||||
if let Some(cache) = requirements_cache {
|
|
||||||
let _ = cache.set(requirements);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(requirements)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Binds this image into an aliasable allocation at an allocation-local offset.
|
|
||||||
///
|
|
||||||
/// Requirements are queried once and cached in the resource. The Vulkan bind remains the
|
|
||||||
/// authoritative validation.
|
|
||||||
pub fn bind_allocation(
|
|
||||||
&mut self,
|
|
||||||
allocation: Arc<DeviceObject<crate::device::GpuAllocation>>,
|
|
||||||
offset: u64,
|
|
||||||
) -> crate::memory::Result<()> {
|
|
||||||
let ImageInner::Allocated(_, current_allocation) = &self.image else {
|
|
||||||
return Err(crate::memory::Error::AlreadyBound);
|
|
||||||
};
|
|
||||||
if !matches!(current_allocation, Allocation::Unmanaged { .. }) {
|
|
||||||
return Err(crate::memory::Error::AlreadyBound);
|
|
||||||
}
|
|
||||||
|
|
||||||
let requirements = self.memory_requirements()?;
|
|
||||||
crate::memory::validate_binding(&requirements, allocation.as_ref(), offset)?;
|
|
||||||
let ImageInner::Allocated(image, current_allocation) = &mut self.image else {
|
|
||||||
unreachable!("image kind checked above")
|
|
||||||
};
|
|
||||||
*current_allocation = image
|
|
||||||
.device()
|
|
||||||
.bind_image_at_offset(**image, &requirements, allocation, offset)
|
|
||||||
.map_err(crate::memory::Error::Bind)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn validate_unallocated(&self) -> crate::memory::Result<()> {
|
|
||||||
match &self.image {
|
|
||||||
ImageInner::Allocated(_, Allocation::Unmanaged { .. }) => Ok(()),
|
|
||||||
ImageInner::Allocated(_, _) | ImageInner::Swapchain(_, _) => {
|
|
||||||
Err(crate::memory::Error::AlreadyBound)
|
|
||||||
}
|
|
||||||
#[cfg(feature = "testing")]
|
|
||||||
ImageInner::Testing(_) => Err(crate::memory::Error::AlreadyBound),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn validate_device(&self, device: &Device) -> crate::memory::Result<()> {
|
|
||||||
if self.image.device() == device {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(crate::memory::Error::Bind(
|
|
||||||
crate::Error::AllocationDeviceMismatch,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn set_shared_allocation(
|
|
||||||
&mut self,
|
|
||||||
allocation: Arc<DeviceObject<crate::device::GpuAllocation>>,
|
|
||||||
) {
|
|
||||||
let ImageInner::Allocated(_, current_allocation) = &mut self.image else {
|
|
||||||
unreachable!("group members are owned images")
|
|
||||||
};
|
|
||||||
debug_assert!(matches!(current_allocation, Allocation::Unmanaged { .. }));
|
|
||||||
*current_allocation = Allocation::Shared(allocation);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_with_allocation_unchecked(
|
|
||||||
device: Device,
|
|
||||||
image: DeviceObject<vk::Image>,
|
|
||||||
allocation: Allocation,
|
|
||||||
desc: ImageDesc,
|
|
||||||
) -> crate::Result<Self> {
|
|
||||||
let image = Self {
|
|
||||||
image: ImageInner::Allocated(image, allocation),
|
|
||||||
desc,
|
|
||||||
views: Default::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Bind after ownership is established so failures destroy the image before freeing memory.
|
|
||||||
if let ImageInner::Allocated(raw_image, allocation) = &image.image {
|
|
||||||
if let Some(alloc) = allocation.allocation() {
|
|
||||||
device.bind_image_memory(alloc, **raw_image)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(image)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_with_allocation(
|
pub fn new_with_allocation(
|
||||||
device: Device,
|
device: Device,
|
||||||
allocation: Allocation,
|
allocation: Allocation,
|
||||||
desc: ImageDesc,
|
desc: ImageDesc,
|
||||||
) -> crate::Result<Self> {
|
) -> crate::Result<Self> {
|
||||||
let image = Self::new_raw(device.clone(), &desc)?;
|
let (image, requirements) = Self::new_raw(device.clone(), &desc)?;
|
||||||
let requirements = unsafe { device.dev().get_image_memory_requirements(*image) };
|
|
||||||
|
|
||||||
// validate allocation
|
// validate allocation
|
||||||
let alloc_size = allocation
|
let alloc_size = allocation
|
||||||
|
|
@ -385,7 +286,7 @@ impl Image {
|
||||||
usage,
|
usage,
|
||||||
queue_families: QueueFlags::PRESENT,
|
queue_families: QueueFlags::PRESENT,
|
||||||
layout: vk::ImageLayout::UNDEFINED,
|
layout: vk::ImageLayout::UNDEFINED,
|
||||||
mem_location: MemoryLocation::GpuOnly,
|
mem_location: gpu_allocator::MemoryLocation::GpuOnly,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
views: Default::default(),
|
views: Default::default(),
|
||||||
|
|
@ -406,7 +307,10 @@ impl Image {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_raw(device: Device, desc: &ImageDesc) -> crate::Result<DeviceObject<vk::Image>> {
|
fn new_raw(
|
||||||
|
device: Device,
|
||||||
|
desc: &ImageDesc,
|
||||||
|
) -> crate::Result<(vk::Image, vk::MemoryRequirements)> {
|
||||||
tracing::trace!("new image with desc={desc:?}");
|
tracing::trace!("new image with desc={desc:?}");
|
||||||
let ImageDesc {
|
let ImageDesc {
|
||||||
flags,
|
flags,
|
||||||
|
|
@ -468,11 +372,9 @@ impl Image {
|
||||||
|
|
||||||
let image = unsafe { device.raw.create_image(&info, None)? };
|
let image = unsafe { device.raw.create_image(&info, None)? };
|
||||||
|
|
||||||
Ok(DeviceObject::new_debug_named(
|
let requirements = unsafe { device.raw.get_image_memory_requirements(image) };
|
||||||
device,
|
|
||||||
image,
|
Ok((image, requirements))
|
||||||
desc.name.clone(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn raw(&self) -> vk::Image {
|
pub fn raw(&self) -> vk::Image {
|
||||||
|
|
@ -554,7 +456,10 @@ impl Image {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
pub fn create_view(self: &Arc<Self>, mut desc: ImageViewDesc) -> crate::Result<ImageView> {
|
pub fn create_view(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
mut desc: ImageViewDesc,
|
||||||
|
) -> crate::Result<ManuallyDrop<ImageView>> {
|
||||||
// validate
|
// validate
|
||||||
if !view_kind_compatible(self.desc.kind, desc.kind) {
|
if !view_kind_compatible(self.desc.kind, desc.kind) {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
|
|
@ -614,7 +519,7 @@ impl Image {
|
||||||
let device = self.image.device();
|
let device = self.image.device();
|
||||||
let view = unsafe { device.raw.create_image_view(&create_info, None)? };
|
let view = unsafe { device.raw.create_image_view(&create_info, None)? };
|
||||||
|
|
||||||
Ok(ImageView {
|
Ok(ManuallyDrop::new(ImageView {
|
||||||
view: ImageViewHandle::Owned(DeviceObject::new_debug_named(
|
view: ImageViewHandle::Owned(DeviceObject::new_debug_named(
|
||||||
device.clone(),
|
device.clone(),
|
||||||
view,
|
view,
|
||||||
|
|
@ -622,7 +527,7 @@ impl Image {
|
||||||
)),
|
)),
|
||||||
desc,
|
desc,
|
||||||
image: self.clone(),
|
image: self.clone(),
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1185,6 +1090,26 @@ fn image_view_subresource_range(desc: &ImageViewDesc) -> vk::ImageSubresourceRan
|
||||||
.layer_count(desc.layer_range.len())
|
.layer_count(desc.layer_range.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod descriptor_range_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalized_range_accessor_resolves_remaining_sentinels() {
|
||||||
|
let mut desc = ImageViewDesc::color_2d()
|
||||||
|
.with_mip_range(1..)
|
||||||
|
.with_layer_range(2..);
|
||||||
|
desc.mip_range.set_max_end(6);
|
||||||
|
desc.layer_range.set_max_end(9);
|
||||||
|
let range = image_view_subresource_range(&desc);
|
||||||
|
assert_eq!(range.aspect_mask, vk::ImageAspectFlags::COLOR);
|
||||||
|
assert_eq!(range.base_mip_level, 1);
|
||||||
|
assert_eq!(range.level_count, 5);
|
||||||
|
assert_eq!(range.base_array_layer, 2);
|
||||||
|
assert_eq!(range.layer_count, 7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct QueueOwnership {
|
pub struct QueueOwnership {
|
||||||
pub src: u32,
|
pub src: u32,
|
||||||
pub dst: u32,
|
pub dst: u32,
|
||||||
|
|
@ -1312,23 +1237,3 @@ impl ImageSubresourceExt for vk::ImageSubresource {
|
||||||
MipRange::single(self.mip_level)
|
MipRange::single(self.mip_level)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod descriptor_range_tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn normalized_range_accessor_resolves_remaining_sentinels() {
|
|
||||||
let mut desc = ImageViewDesc::color_2d()
|
|
||||||
.with_mip_range(1..)
|
|
||||||
.with_layer_range(2..);
|
|
||||||
desc.mip_range.set_max_end(6);
|
|
||||||
desc.layer_range.set_max_end(9);
|
|
||||||
let range = image_view_subresource_range(&desc);
|
|
||||||
assert_eq!(range.aspect_mask, vk::ImageAspectFlags::COLOR);
|
|
||||||
assert_eq!(range.base_mip_level, 1);
|
|
||||||
assert_eq!(range.level_count, 5);
|
|
||||||
assert_eq!(range.base_array_layer, 2);
|
|
||||||
assert_eq!(range.layer_count, 7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,5 @@ pub use renderer_device::{Device, Error, Result, device, make_extension};
|
||||||
pub mod buffers;
|
pub mod buffers;
|
||||||
pub mod descriptors;
|
pub mod descriptors;
|
||||||
pub mod images;
|
pub mod images;
|
||||||
pub mod memory;
|
|
||||||
pub mod pipeline;
|
pub mod pipeline;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
|
||||||
|
|
@ -1,411 +0,0 @@
|
||||||
//! Shared aliasable memory allocation helpers for buffers and images.
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use ash::vk;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
Device,
|
|
||||||
buffers::Buffer,
|
|
||||||
device::{DeviceObject, GpuAllocation, MemoryBinding},
|
|
||||||
images::Image,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Vulkan memory requirements returned by the device layer.
|
|
||||||
pub use crate::device::MemoryRequirements;
|
|
||||||
|
|
||||||
/// Errors from resource requirement queries, validation, and shared binding.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
RequirementQuery(crate::Error),
|
|
||||||
Bind(crate::Error),
|
|
||||||
Allocation(crate::Error),
|
|
||||||
MisalignedOffset {
|
|
||||||
offset: u64,
|
|
||||||
alignment: u64,
|
|
||||||
},
|
|
||||||
IncompatibleMemoryType {
|
|
||||||
memory_type_index: u32,
|
|
||||||
memory_type_bits: u32,
|
|
||||||
},
|
|
||||||
OutOfBounds {
|
|
||||||
offset: u64,
|
|
||||||
end: u64,
|
|
||||||
allocation_size: u64,
|
|
||||||
},
|
|
||||||
AlreadyBound,
|
|
||||||
AlreadyAllocated,
|
|
||||||
RequiresDedicatedAllocation,
|
|
||||||
GroupMemoryTypeMismatch {
|
|
||||||
memory_type_index: u32,
|
|
||||||
},
|
|
||||||
GroupCapacityExceeded {
|
|
||||||
requested_end: u64,
|
|
||||||
capacity: u64,
|
|
||||||
},
|
|
||||||
EmptyGroup,
|
|
||||||
NotAllocated,
|
|
||||||
InvalidGroupRequirements,
|
|
||||||
OffsetOverflow,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl core::fmt::Display for Error {
|
|
||||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::RequirementQuery(error) => {
|
|
||||||
write!(f, "failed to query resource memory requirements: {error}")
|
|
||||||
}
|
|
||||||
Self::Bind(error) => write!(f, "failed to bind resource memory: {error}"),
|
|
||||||
Self::Allocation(error) => write!(f, "failed to allocate group memory: {error}"),
|
|
||||||
Self::MisalignedOffset { offset, alignment } => write!(
|
|
||||||
f,
|
|
||||||
"allocation offset {offset} is not aligned to {alignment}"
|
|
||||||
),
|
|
||||||
Self::IncompatibleMemoryType {
|
|
||||||
memory_type_index,
|
|
||||||
memory_type_bits,
|
|
||||||
} => write!(
|
|
||||||
f,
|
|
||||||
"allocation memory type {memory_type_index} is not allowed by mask {memory_type_bits:#010x}"
|
|
||||||
),
|
|
||||||
Self::OutOfBounds {
|
|
||||||
offset,
|
|
||||||
end,
|
|
||||||
allocation_size,
|
|
||||||
} => write!(
|
|
||||||
f,
|
|
||||||
"resource range [{offset}, {end}) does not fit allocation size {allocation_size}"
|
|
||||||
),
|
|
||||||
Self::AlreadyBound => f.write_str("resource is already bound to an allocation"),
|
|
||||||
Self::AlreadyAllocated => f.write_str("memory group has already been allocated"),
|
|
||||||
Self::RequiresDedicatedAllocation => f.write_str(
|
|
||||||
"resources requiring dedicated allocations cannot be added to a memory group",
|
|
||||||
),
|
|
||||||
Self::GroupMemoryTypeMismatch { memory_type_index } => write!(
|
|
||||||
f,
|
|
||||||
"resource memory requirements do not include memory type {memory_type_index}"
|
|
||||||
),
|
|
||||||
Self::GroupCapacityExceeded {
|
|
||||||
requested_end,
|
|
||||||
capacity,
|
|
||||||
} => write!(
|
|
||||||
f,
|
|
||||||
"memory group capacity exceeded: requested end {requested_end}, capacity {capacity}"
|
|
||||||
),
|
|
||||||
Self::EmptyGroup => f.write_str("memory group has no members"),
|
|
||||||
Self::NotAllocated => f.write_str("memory group has not been allocated"),
|
|
||||||
Self::InvalidGroupRequirements => {
|
|
||||||
f.write_str("memory group size or alignment is invalid")
|
|
||||||
}
|
|
||||||
Self::OffsetOverflow => f.write_str("memory group offset arithmetic overflowed"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for Error {
|
|
||||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
|
||||||
match self {
|
|
||||||
Self::RequirementQuery(error) | Self::Bind(error) | Self::Allocation(error) => {
|
|
||||||
Some(error)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Result<T> = core::result::Result<T, Error>;
|
|
||||||
|
|
||||||
pub(crate) fn validate_binding(
|
|
||||||
requirements: &MemoryRequirements,
|
|
||||||
allocation: &DeviceObject<GpuAllocation>,
|
|
||||||
offset: u64,
|
|
||||||
) -> Result<()> {
|
|
||||||
let alignment = requirements.alignment;
|
|
||||||
if alignment == 0 || !offset.is_multiple_of(alignment) {
|
|
||||||
return Err(Error::MisalignedOffset { offset, alignment });
|
|
||||||
}
|
|
||||||
|
|
||||||
let memory_type_index = allocation.memory_type_index();
|
|
||||||
if requirements.memory_type_bits & (1 << memory_type_index) == 0 {
|
|
||||||
return Err(Error::IncompatibleMemoryType {
|
|
||||||
memory_type_index,
|
|
||||||
memory_type_bits: requirements.memory_type_bits,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let end = offset
|
|
||||||
.checked_add(requirements.size)
|
|
||||||
.ok_or(Error::OutOfBounds {
|
|
||||||
offset,
|
|
||||||
end: u64::MAX,
|
|
||||||
allocation_size: allocation.size(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if end > allocation.size() {
|
|
||||||
return Err(Error::OutOfBounds {
|
|
||||||
offset,
|
|
||||||
end,
|
|
||||||
allocation_size: allocation.size(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Member<'a> {
|
|
||||||
Buffer {
|
|
||||||
resource: &'a mut Buffer,
|
|
||||||
offset: u64,
|
|
||||||
},
|
|
||||||
Image {
|
|
||||||
resource: &'a mut Image,
|
|
||||||
offset: u64,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Packs resources into a single aliasable allocation with a fixed memory type.
|
|
||||||
///
|
|
||||||
/// Members are exclusively borrowed until `bind_all` consumes the group. Their
|
|
||||||
/// allocation ownership is then transferred to each resource; the group retains
|
|
||||||
/// no allocation handle after binding.
|
|
||||||
pub struct MemoryGroup<'a> {
|
|
||||||
memory_type_index: u32,
|
|
||||||
property_flags: vk::MemoryPropertyFlags,
|
|
||||||
size: u64,
|
|
||||||
alignment: u64,
|
|
||||||
next_offset: u64,
|
|
||||||
members: Vec<Member<'a>>,
|
|
||||||
allocation: Option<Arc<DeviceObject<GpuAllocation>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> MemoryGroup<'a> {
|
|
||||||
/// Creates an empty group for a fixed memory type and allocation capacity.
|
|
||||||
pub fn new(
|
|
||||||
memory_type_index: u32,
|
|
||||||
property_flags: vk::MemoryPropertyFlags,
|
|
||||||
size: u64,
|
|
||||||
alignment: u64,
|
|
||||||
) -> Result<Self> {
|
|
||||||
if memory_type_index >= u32::BITS || size == 0 || alignment == 0 {
|
|
||||||
return Err(Error::InvalidGroupRequirements);
|
|
||||||
}
|
|
||||||
Ok(Self {
|
|
||||||
memory_type_index,
|
|
||||||
property_flags,
|
|
||||||
size,
|
|
||||||
alignment,
|
|
||||||
next_offset: 0,
|
|
||||||
members: Vec::new(),
|
|
||||||
allocation: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a buffer, placing it at the next suitably aligned offset.
|
|
||||||
pub fn add_buffer(&mut self, resource: &'a mut Buffer) -> Result<u64> {
|
|
||||||
if self.allocation.is_some() {
|
|
||||||
return Err(Error::AlreadyAllocated);
|
|
||||||
}
|
|
||||||
|
|
||||||
let requirements = resource.memory_requirements()?;
|
|
||||||
self.add_member_type_check(&requirements)?;
|
|
||||||
let offset = self.next_member_offset(&requirements)?;
|
|
||||||
|
|
||||||
let next_offset = offset
|
|
||||||
.checked_add(requirements.size)
|
|
||||||
.ok_or(Error::OffsetOverflow)?;
|
|
||||||
|
|
||||||
self.members.push(Member::Buffer { resource, offset });
|
|
||||||
|
|
||||||
self.next_offset = next_offset;
|
|
||||||
Ok(offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds either resource kind through a uniform entry point.
|
|
||||||
pub fn add(&mut self, member: MemoryMember<'a>) -> Result<u64> {
|
|
||||||
match member {
|
|
||||||
MemoryMember::Buffer(resource) => self.add_buffer(resource),
|
|
||||||
MemoryMember::Image(resource) => self.add_image(resource),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds an image, placing it at the next suitably aligned offset.
|
|
||||||
pub fn add_image(&mut self, resource: &'a mut Image) -> Result<u64> {
|
|
||||||
if self.allocation.is_some() {
|
|
||||||
return Err(Error::AlreadyAllocated);
|
|
||||||
}
|
|
||||||
|
|
||||||
let requirements = resource.memory_requirements()?;
|
|
||||||
self.add_member_type_check(&requirements)?;
|
|
||||||
let offset = self.next_member_offset(&requirements)?;
|
|
||||||
|
|
||||||
let next_offset = offset
|
|
||||||
.checked_add(requirements.size)
|
|
||||||
.ok_or(Error::OffsetOverflow)?;
|
|
||||||
|
|
||||||
self.members.push(Member::Image { resource, offset });
|
|
||||||
|
|
||||||
self.next_offset = next_offset;
|
|
||||||
Ok(offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allocates the group's shared backing memory.
|
|
||||||
pub fn allocate(&mut self, device: &Device) -> Result<()> {
|
|
||||||
if self.members.is_empty() {
|
|
||||||
return Err(Error::EmptyGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.allocation.is_some() {
|
|
||||||
return Err(Error::AlreadyAllocated);
|
|
||||||
}
|
|
||||||
|
|
||||||
let requirements = MemoryRequirements {
|
|
||||||
size: self.size,
|
|
||||||
alignment: self.alignment,
|
|
||||||
memory_type_bits: 1 << self.memory_type_index,
|
|
||||||
requires_dedicated_allocation: false,
|
|
||||||
prefers_dedicated_allocation: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let allocation = device
|
|
||||||
.allocate_memory(&requirements, self.property_flags)
|
|
||||||
.map_err(Error::Allocation)?;
|
|
||||||
|
|
||||||
for member in &self.members {
|
|
||||||
let (requirements, offset) = match member {
|
|
||||||
Member::Buffer { resource, offset } => (resource.memory_requirements()?, *offset),
|
|
||||||
Member::Image { resource, offset } => (resource.memory_requirements()?, *offset),
|
|
||||||
};
|
|
||||||
validate_binding(&requirements, allocation.as_ref(), offset)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.allocation = Some(allocation);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Binds every member and transfers the allocation reference into each resource.
|
|
||||||
pub fn bind_all(mut self) -> Result<()> {
|
|
||||||
let allocation = self.allocation.take().ok_or(Error::NotAllocated)?;
|
|
||||||
|
|
||||||
// Validate resources first to preserve the same resource-level error mapping as
|
|
||||||
// individual binding, then prepare the ordered device batch from cached requirements.
|
|
||||||
let mut bindings = Vec::with_capacity(self.members.len());
|
|
||||||
for member in &self.members {
|
|
||||||
match member {
|
|
||||||
Member::Buffer { resource, offset } => {
|
|
||||||
resource.validate_unallocated()?;
|
|
||||||
let requirements = resource.memory_requirements()?;
|
|
||||||
validate_binding(&requirements, allocation.as_ref(), *offset)?;
|
|
||||||
resource.validate_device(allocation.device())?;
|
|
||||||
bindings.push(MemoryBinding::Buffer {
|
|
||||||
buffer: resource.raw(),
|
|
||||||
offset: *offset,
|
|
||||||
requirements,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Member::Image { resource, offset } => {
|
|
||||||
resource.validate_unallocated()?;
|
|
||||||
let requirements = resource.memory_requirements()?;
|
|
||||||
validate_binding(&requirements, allocation.as_ref(), *offset)?;
|
|
||||||
resource.validate_device(allocation.device())?;
|
|
||||||
bindings.push(MemoryBinding::Image {
|
|
||||||
image: resource.raw(),
|
|
||||||
offset: *offset,
|
|
||||||
requirements,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(bind_error) = allocation
|
|
||||||
.device()
|
|
||||||
.bind_group_at_offset(&allocation, &bindings)
|
|
||||||
{
|
|
||||||
// A Vulkan bind failure leaves its earlier prefix bound; retain its allocation just
|
|
||||||
// as repeated `bind_allocation` calls would have done.
|
|
||||||
for (index, member) in self.members.into_iter().enumerate() {
|
|
||||||
if index >= bind_error.bound_count {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
match member {
|
|
||||||
Member::Buffer { resource, .. } => {
|
|
||||||
resource.set_shared_allocation(allocation.clone());
|
|
||||||
}
|
|
||||||
Member::Image { resource, .. } => {
|
|
||||||
resource.set_shared_allocation(allocation.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Err(Error::Bind(bind_error.error));
|
|
||||||
}
|
|
||||||
|
|
||||||
for member in self.members {
|
|
||||||
match member {
|
|
||||||
Member::Buffer { resource, .. } => {
|
|
||||||
resource.set_shared_allocation(allocation.clone());
|
|
||||||
}
|
|
||||||
Member::Image { resource, .. } => {
|
|
||||||
resource.set_shared_allocation(allocation.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// `allocation` drops here. Only successfully bound resources retain it.
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_member_type_check(&self, requirements: &MemoryRequirements) -> Result<()> {
|
|
||||||
if requirements.requires_dedicated_allocation {
|
|
||||||
return Err(Error::RequiresDedicatedAllocation);
|
|
||||||
}
|
|
||||||
if requirements.memory_type_bits & (1 << self.memory_type_index) == 0 {
|
|
||||||
return Err(Error::GroupMemoryTypeMismatch {
|
|
||||||
memory_type_index: self.memory_type_index,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_member_offset(&self, requirements: &MemoryRequirements) -> Result<u64> {
|
|
||||||
if requirements.alignment == 0 {
|
|
||||||
return Err(Error::InvalidGroupRequirements);
|
|
||||||
}
|
|
||||||
let alignment = checked_lcm(requirements.alignment, self.alignment)?;
|
|
||||||
let offset = (self
|
|
||||||
.next_offset
|
|
||||||
.checked_add(alignment - 1)
|
|
||||||
.ok_or(Error::OffsetOverflow)?
|
|
||||||
/ alignment)
|
|
||||||
.checked_mul(alignment)
|
|
||||||
.ok_or(Error::OffsetOverflow)?;
|
|
||||||
let end = offset
|
|
||||||
.checked_add(requirements.size)
|
|
||||||
.ok_or(Error::OffsetOverflow)?;
|
|
||||||
if end > self.size {
|
|
||||||
return Err(Error::GroupCapacityExceeded {
|
|
||||||
requested_end: end,
|
|
||||||
capacity: self.size,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(offset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn checked_lcm(lhs: u64, rhs: u64) -> Result<u64> {
|
|
||||||
let gcd = {
|
|
||||||
let (mut lhs, mut rhs) = (lhs, rhs);
|
|
||||||
while rhs != 0 {
|
|
||||||
(lhs, rhs) = (rhs, lhs % rhs);
|
|
||||||
}
|
|
||||||
lhs
|
|
||||||
};
|
|
||||||
lhs.checked_div(gcd)
|
|
||||||
.and_then(|lhs| lhs.checked_mul(rhs))
|
|
||||||
.ok_or(Error::OffsetOverflow)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A resource accepted by [`MemoryGroup::add`].
|
|
||||||
pub enum MemoryMember<'a> {
|
|
||||||
Buffer(&'a mut Buffer),
|
|
||||||
Image(&'a mut Image),
|
|
||||||
}
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use ash::vk;
|
|
||||||
use renderer_device::{
|
|
||||||
device::{MemoryLocation, QueueFlags},
|
|
||||||
testing::TestVulkan,
|
|
||||||
};
|
|
||||||
use renderer_resources::{
|
|
||||||
buffers::{Buffer, BufferDesc},
|
|
||||||
pipeline::{
|
|
||||||
BufferDescriptor, DescriptorPool, DescriptorPoolDesc, DescriptorSetAllocDesc,
|
|
||||||
DescriptorSetLayout, DescriptorSetLayoutBindingDesc, DescriptorSetLayoutDesc,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn managed_set_write_and_pool_reset_under_validation() {
|
|
||||||
let Some(test) = TestVulkan::acquire() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let device = test.device().clone();
|
|
||||||
let buffer = Arc::new(
|
|
||||||
Buffer::new(
|
|
||||||
device.clone(),
|
|
||||||
BufferDesc {
|
|
||||||
size: 256,
|
|
||||||
usage: vk::BufferUsageFlags::UNIFORM_BUFFER,
|
|
||||||
queue_families: QueueFlags::empty(),
|
|
||||||
mem_location: MemoryLocation::CpuToGpu,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("create descriptor buffer"),
|
|
||||||
);
|
|
||||||
let bindings = [DescriptorSetLayoutBindingDesc {
|
|
||||||
binding: 0,
|
|
||||||
count: 1,
|
|
||||||
kind: vk::DescriptorType::UNIFORM_BUFFER,
|
|
||||||
stage: vk::ShaderStageFlags::VERTEX,
|
|
||||||
flags: None,
|
|
||||||
}];
|
|
||||||
let layout = DescriptorSetLayout::new(
|
|
||||||
device.clone(),
|
|
||||||
DescriptorSetLayoutDesc {
|
|
||||||
bindings: &bindings,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("create descriptor set layout");
|
|
||||||
let sizes = [vk::DescriptorPoolSize::default()
|
|
||||||
.ty(vk::DescriptorType::UNIFORM_BUFFER)
|
|
||||||
.descriptor_count(1)];
|
|
||||||
let pool = DescriptorPool::new(
|
|
||||||
device,
|
|
||||||
DescriptorPoolDesc {
|
|
||||||
sizes: &sizes,
|
|
||||||
max_sets: 1,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("create descriptor pool");
|
|
||||||
let mut set = pool
|
|
||||||
.allocate_managed(DescriptorSetAllocDesc {
|
|
||||||
name: None,
|
|
||||||
layout: &layout,
|
|
||||||
})
|
|
||||||
.expect("allocate managed descriptor set");
|
|
||||||
let mut writer = set.writer().expect("create descriptor writer");
|
|
||||||
writer
|
|
||||||
.write_buffer(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
&[BufferDescriptor {
|
|
||||||
buffer: &buffer,
|
|
||||||
offset: 0,
|
|
||||||
range: 256,
|
|
||||||
}],
|
|
||||||
)
|
|
||||||
.expect("stage buffer descriptor");
|
|
||||||
writer.commit().expect("commit buffer descriptor");
|
|
||||||
|
|
||||||
test.device().wait_idle().expect("wait for device idle");
|
|
||||||
drop(set);
|
|
||||||
// SAFETY: no command uses this set, the device is idle, and this test excludes concurrent use.
|
|
||||||
unsafe { pool.reset() }.expect("reset descriptor pool");
|
|
||||||
drop(pool);
|
|
||||||
drop(layout);
|
|
||||||
drop(buffer);
|
|
||||||
test.assert_no_validation_errors();
|
|
||||||
}
|
|
||||||
|
|
@ -1,326 +0,0 @@
|
||||||
# Migration Plan: gpu-allocator → VMA via vk-mem
|
|
||||||
|
|
||||||
Status: proposed — rev 2 (Luna review fixes applied)
|
|
||||||
Date: 2026-10-26
|
|
||||||
Scope: workspace-wide memory-allocator swap
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Replace the `gpu-allocator` crate with VMA (Vulkan Memory Allocator) via the
|
|
||||||
`vk-mem` bindings crate, preserving current allocation behavior (buffer/image
|
|
||||||
create/bind/destroy, mapping, dedicated allocations) and the existing real-Vulkan
|
|
||||||
tests.
|
|
||||||
|
|
||||||
## Architecture decision
|
|
||||||
|
|
||||||
### Requirements
|
|
||||||
|
|
||||||
- Hard: remove `gpu-allocator`; use `vk-mem`/VMA; preserve the dependency
|
|
||||||
direction `bevy_vulkan_render` → `renderer` → `renderer_resources` →
|
|
||||||
`renderer_device`; keep buffer/image create/bind/destroy and mapping behavior;
|
|
||||||
keep real-Vulkan tests working.
|
|
||||||
- Nice: dedicated allocations, allocation naming/stats, defragmentation (not
|
|
||||||
required now).
|
|
||||||
|
|
||||||
### Constraints
|
|
||||||
|
|
||||||
- Nightly toolchain, edition 2024.
|
|
||||||
- `renderer_resources` must not depend on `renderer`.
|
|
||||||
- `renderer_device` owns device/allocator state.
|
|
||||||
- `renderer_resources`/`renderer` should not expose `vk_mem` types.
|
|
||||||
- The `testing` feature gates real-device tests.
|
|
||||||
- No per-frame allocation is acceptable on current paths.
|
|
||||||
- `renderer_rendergraph` is also a workspace member and directly uses
|
|
||||||
`gpu_allocator::MemoryLocation`, so it must be included (it is not mentioned in
|
|
||||||
the workspace guide).
|
|
||||||
|
|
||||||
### Current architecture
|
|
||||||
|
|
||||||
- Deps: root `Cargo.toml:45` git `gpu-allocator` (line 44 has a commented
|
|
||||||
`# vk-mem = "0.5.0"`); `renderer_device/Cargo.toml:12`,
|
|
||||||
`renderer_resources/Cargo.toml:12`, `renderer/Cargo.toml:27`,
|
|
||||||
`renderer_rendergraph/Cargo.toml:8`. `Cargo.lock:3423-3435` is the git package;
|
|
||||||
`Cargo.lock:3409-3420` is the registry copy pulled by `wgpu-hal` via the Bevy
|
|
||||||
dev-dependency.
|
|
||||||
- Allocator state: `crates/renderer_device/src/device.rs:151`
|
|
||||||
`pub alloc2: Mutex<gpu_allocator::vulkan::Allocator>`, initialized at
|
|
||||||
`:441-456` (`AllocatorCreateDesc`, `AllocationSizes::new(8MB,64MB)` with 256MB
|
|
||||||
maxima). `GpuAllocation` is the private alias at `:111`; `AllocationStrategy`
|
|
||||||
at `:114-124`; `Allocation::{Owned,Shared,Unmanaged}` at `:126-147`. Free path
|
|
||||||
is `ExternallyManagedObject<Device> for GpuAllocation` at `:1074-1077`, which
|
|
||||||
calls `alloc2.lock().free(...)`.
|
|
||||||
- Errors: `crates/renderer_device/src/lib.rs:81`
|
|
||||||
`GpuAllocatorError(#[from] gpu_allocator::AllocationError)`.
|
|
||||||
- Buffer flow: `crates/renderer_resources/src/buffers.rs:4` imports
|
|
||||||
`AllocationScheme`; `:20,68` `MemoryLocation`; `:108-122` allocates from
|
|
||||||
`alloc2`; `:128-130` binds with raw `vkBindBufferMemory`; `:137` wraps in
|
|
||||||
`Allocation::Owned(DeviceObject<GpuAllocation>)`; `:164-181` maps via
|
|
||||||
`mapped_slice`/`mapped_slice_mut`.
|
|
||||||
- Image flow: `crates/renderer_resources/src/images.rs:16` imports VMA-named
|
|
||||||
`AllocationCreateDesc/Scheme`; `:35,126,289` `MemoryLocation`; `:191-205`
|
|
||||||
allocates and wraps; `:217-223` binds; `:244-260` validates size/memory-type
|
|
||||||
for `new_with_allocation`; `:418-428` exposes allocation.
|
|
||||||
- Dedicated logic: `buffers.rs:116-121` uses
|
|
||||||
`AllocationScheme::DedicatedBuffer(buffer)`; `images.rs:196-199` uses
|
|
||||||
`DedicatedImage(image)`; `renderer_rendergraph/src/graph.rs:1352-1357` chooses
|
|
||||||
dedicated at 64 MiB.
|
|
||||||
- Direct test allocations: `crates/renderer_device/src/testing.rs:312-319` and
|
|
||||||
`:351-357` allocate/free directly through `device.alloc2`.
|
|
||||||
- Other type consumers: `renderer/src/rendering/mod.rs:8,56,77,87,97`;
|
|
||||||
`renderer/src/egui.rs:8,74-75,140,157,510,531,538,545,555`;
|
|
||||||
`renderer/src/render_graph/graph_builder.rs:51,60,69,87,168-183`;
|
|
||||||
`renderer_rendergraph/src/resources.rs:16,27,46,69`; tests
|
|
||||||
`renderer_resources/tests/managed_set_validation.rs:26` and
|
|
||||||
`renderer_rendergraph/tests/graph_validation.rs:31`.
|
|
||||||
- No stats/reporting or defragmentation call sites exist today. `Buffer`/`Image`
|
|
||||||
drop field order: buffer/image handle first, then allocation
|
|
||||||
(`buffers.rs:74-78`, `images.rs:144-148`).
|
|
||||||
|
|
||||||
### Alternatives considered
|
|
||||||
|
|
||||||
1. **Swap allocator inside `renderer_device`, add a private wrapper and a
|
|
||||||
workspace-owned `MemoryLocation` (chosen)**: least invasive, upper crates stay
|
|
||||||
VMA-free.
|
|
||||||
2. Re-export `vk_mem` types as public resource API: least code, but leaks VMA
|
|
||||||
into `renderer_resources`/`renderer` and locks the workspace API to `vk-mem`.
|
|
||||||
3. Use `vk_mem::create_buffer/create_image` directly in `renderer_resources`:
|
|
||||||
fewer Vulkan calls and idiomatic VMA, but larger ownership refactor and
|
|
||||||
couples the resource crate to `vk_mem`.
|
|
||||||
4. Add a memory-backend trait: easy A/B testing, but unnecessary indirection and
|
|
||||||
lifetime complexity for a one-time swap.
|
|
||||||
|
|
||||||
### Decision rationale (feasibility verified)
|
|
||||||
|
|
||||||
- `vk-mem 0.5.0` bundles **VMA 3.3.0** and depends on **ash 0.38** (workspace ash
|
|
||||||
is `0.38.0+1.3.281`), so there is one ash and no duplicate-ash problem.
|
|
||||||
- `vk_mem::Allocator` is `Send + Sync` and internally synchronized unless
|
|
||||||
`EXTERNALLY_SYNCHRONIZED` is set; keep the `Mutex`.
|
|
||||||
- `vk_mem::Allocation` is `Copy, Send, Sync` and has no inherent
|
|
||||||
`size/memory/offset` accessors, so the wrapper must retain `AllocationInfo`.
|
|
||||||
|
|
||||||
## Target design
|
|
||||||
|
|
||||||
- `renderer_device::device::MemoryLocation { Unknown, GpuOnly, CpuToGpu,
|
|
||||||
GpuToCpu }` with `Default = Unknown`, `Copy/Eq/Hash`; use it as the public
|
|
||||||
`mem_location` type.
|
|
||||||
- `pub struct GpuAllocation { raw: vk_mem::Allocation, memory: vk::DeviceMemory,
|
|
||||||
offset: u64, size: u64, memory_type_index: u32, mapped: Option<SendSyncPtr> }`.
|
|
||||||
`SendSyncPtr` is a private `NonNull<u8>` wrapper with unsafe `Send/Sync`,
|
|
||||||
mirroring `gpu_allocator`'s pointer wrapper. Implement `Debug`,
|
|
||||||
`memory()/offset()/size()/memory_type_index()/mapped_slice()/mapped_slice_mut()`.
|
|
||||||
- `pub struct AllocationDesc<'a> { name: &'a str, location: MemoryLocation,
|
|
||||||
strategy: AllocationStrategy }` and
|
|
||||||
`DeviceInner::{allocate_for_buffer, allocate_for_image, bind_buffer_memory,
|
|
||||||
bind_image_memory}`. Note the call shapes: `allocate_memory_for_buffer/image`
|
|
||||||
live on the `vk_mem::Alloc` trait (import `vk_mem::Alloc as _`) and are
|
|
||||||
`unsafe` in `vk-mem 0.5.0`; `bind_buffer_memory`/`bind_image_memory` are
|
|
||||||
inherent (also `unsafe`) `Allocator` methods, not trait methods.
|
|
||||||
- `DeviceInner.alloc2: Mutex<vk_mem::Allocator>` remains the single allocator
|
|
||||||
owner. Keep `Allocation::{Owned(DeviceObject<GpuAllocation>),
|
|
||||||
Shared(Arc<...>), Unmanaged}` unchanged in shape.
|
|
||||||
- `renderer_resources`/`renderer`/`renderer_rendergraph` use only
|
|
||||||
`renderer_device::device::MemoryLocation`, `Allocation`, and the new `Device`
|
|
||||||
helpers; no `vk_mem` imports outside `renderer_device`.
|
|
||||||
|
|
||||||
Suggested interface:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
impl DeviceInner {
|
|
||||||
pub fn allocate_for_buffer(&self, buffer: vk::Buffer, desc: AllocationDesc<'_>) -> Result<GpuAllocation>;
|
|
||||||
pub fn allocate_for_image(&self, image: vk::Image, desc: AllocationDesc<'_>) -> Result<GpuAllocation>;
|
|
||||||
pub fn bind_buffer_memory(&self, alloc: &GpuAllocation, buffer: vk::Buffer) -> Result<()>;
|
|
||||||
pub fn bind_image_memory(&self, alloc: &GpuAllocation, image: vk::Image) -> Result<()>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dependency changes
|
|
||||||
|
|
||||||
- Root `Cargo.toml`: add `vk-mem = "0.5.0"` to `[workspace.dependencies]` (the
|
|
||||||
commented line at `:44` anticipates this); remove the git `gpu-allocator` entry
|
|
||||||
in phase 3.
|
|
||||||
- `renderer_device/Cargo.toml`: add `vk-mem = { workspace = true }`; remove
|
|
||||||
`gpu-allocator` in phase 2 (after the swap compiles).
|
|
||||||
- `renderer_resources/Cargo.toml`, `renderer/Cargo.toml`,
|
|
||||||
`renderer_rendergraph/Cargo.toml`: remove `gpu-allocator` in phase 3.
|
|
||||||
- Ash alignment: `vk-mem 0.5.0` pins ash 0.38; an ash upgrade requires a
|
|
||||||
matching `vk-mem` release or fork.
|
|
||||||
|
|
||||||
## API mapping
|
|
||||||
|
|
||||||
| gpu-allocator | VMA / `vk-mem` |
|
|
||||||
|---|---|
|
|
||||||
| `AllocatorCreateDesc` instance/device/physical_device | `AllocatorCreateInfo::new(&instance, &device, physical_device)` |
|
|
||||||
| `debug_settings`, allocation sizes | VMA compile-time debug macros; `preferred_large_heap_block_size` only (loss of min/max block tuning) |
|
|
||||||
| `buffer_device_address` | `AllocatorCreateFlags::BUFFER_DEVICE_ADDRESS` (off here) |
|
|
||||||
| `MemoryLocation::{Unknown,GpuOnly,CpuToGpu,GpuToCpu}` | `MemoryUsage::{Unknown,GpuOnly,CpuToGpu,GpuToCpu}` |
|
|
||||||
| `AllocationScheme::GpuAllocatorManaged` | default allocation (`AllocationCreateFlags::empty()`) |
|
|
||||||
| `DedicatedBuffer(buffer)` / `DedicatedImage(image)` | `DEDICATED_MEMORY` + `allocate_memory_for_buffer/image` (passes `VkMemoryDedicatedAllocateInfo` when API version ≥ 1.1) |
|
|
||||||
| `AllocationCreateDesc.linear` | implicit: use `allocate_memory_for_buffer/image`, not generic allocation |
|
|
||||||
| `Allocation.memory()/offset()/size()/memory_type_index()` | `AllocationInfo.device_memory/offset/size/memory_type` captured at allocation |
|
|
||||||
| `mapped_slice()/mapped_slice_mut()` | `AllocationInfo.mapped_data` from `MAPPED` allocation, or `Allocator::map_memory` |
|
|
||||||
| `Allocator::free(allocation)` | `Allocator::free_memory(&mut allocation)` |
|
|
||||||
| `bind_buffer_memory`/`bind_image_memory` | `Allocator::{bind_buffer_memory, bind_image_memory}` |
|
|
||||||
| stats report | `calculate_statistics()` / `get_heap_budgets()`; no allocation names in `vk-mem 0.5.0` |
|
|
||||||
| debug names | unchanged: `DeviceObject::new_debug_named` sets Vulkan object names; VMA allocation names are not exposed |
|
|
||||||
|
|
||||||
## Migration phases
|
|
||||||
|
|
||||||
### Phase 0 — baseline
|
|
||||||
|
|
||||||
Run `cargo check --workspace`, `cargo test -p renderer_device --features
|
|
||||||
testing`, inspect `git status`. No edits.
|
|
||||||
|
|
||||||
### Phase 1 — dependency/type scaffolding (non-breaking)
|
|
||||||
|
|
||||||
Add `vk-mem = "0.5.0"` to root `[workspace.dependencies]`; add
|
|
||||||
`vk-mem = { workspace = true }` to `renderer_device/Cargo.toml`; add
|
|
||||||
`MemoryLocation`, `GpuAllocation` skeleton, and `AllocationDesc` in
|
|
||||||
`crates/renderer_device/src/device.rs` while leaving the old allocator
|
|
||||||
untouched.
|
|
||||||
|
|
||||||
Verify: `cargo check -p renderer_device`.
|
|
||||||
|
|
||||||
### Phase 2 — atomic allocator swap (one compile unit)
|
|
||||||
|
|
||||||
In `crates/renderer_device/src/device.rs` replace `alloc2`/creation/
|
|
||||||
`GpuAllocation` alias/free impl with VMA; remove `GpuAllocatorError` from
|
|
||||||
`crates/renderer_device/src/lib.rs:81`; rewrite `testing.rs:312-319,351-357`;
|
|
||||||
in `crates/renderer_resources/src/buffers.rs` and `images.rs` replace
|
|
||||||
`alloc2.allocate` + raw bind with `allocate_for_buffer/image` + `bind_*`, adding
|
|
||||||
handle guards in the same pass (raw handle wrapped in `DeviceObject` immediately
|
|
||||||
after creation, allocation ownership established before binding — see
|
|
||||||
implementation notes), but temporarily keep the public fields as
|
|
||||||
`gpu_allocator::MemoryLocation` and convert at the call site.
|
|
||||||
|
|
||||||
Verify: `cargo check --workspace` and `cargo test -p renderer_device --features
|
|
||||||
testing`.
|
|
||||||
|
|
||||||
### Phase 3 — public type/dependency cleanup
|
|
||||||
|
|
||||||
Change `BufferDesc.mem_location`/`ImageDesc.mem_location` to
|
|
||||||
`renderer_device::device::MemoryLocation`; update `renderer/src/rendering/mod.rs`,
|
|
||||||
`renderer/src/egui.rs`, `renderer/src/render_graph/graph_builder.rs`,
|
|
||||||
`renderer_rendergraph/src/resources.rs`, and both tests
|
|
||||||
(`renderer_resources/tests/managed_set_validation.rs`,
|
|
||||||
`renderer_rendergraph/tests/graph_validation.rs`); remove `gpu-allocator` from
|
|
||||||
`renderer_resources`, `renderer`, `renderer_rendergraph`, and root manifests.
|
|
||||||
|
|
||||||
Verify: `cargo check --workspace`; `rg -n 'gpu_allocator|gpu-allocator' --glob
|
|
||||||
'!Cargo.lock'` should be empty.
|
|
||||||
|
|
||||||
### Phase 4 — hardening/docs
|
|
||||||
|
|
||||||
- Ensure `GpuAllocation` is `Send + Sync`.
|
|
||||||
- Add handle guards: wrap raw buffer/image handles in `DeviceObject` right after
|
|
||||||
creation and establish allocation ownership before binding, so allocation and
|
|
||||||
bind failures never leak (see implementation notes).
|
|
||||||
- Add a validation test for `CpuToGpu` mapping that asserts the allocated memory
|
|
||||||
type is `HOST_VISIBLE | HOST_COHERENT` and round-trips a CPU write through
|
|
||||||
`map_mut`, plus dedicated buffers/images.
|
|
||||||
- Update `crates/renderer_device/AGENTS.md`,
|
|
||||||
`crates/renderer_resources/AGENTS.md`, `crates/renderer/AGENTS.md`, and the
|
|
||||||
root workspace guide to say VMA/`vk-mem`.
|
|
||||||
- Optionally rename `alloc2` to `allocator`.
|
|
||||||
|
|
||||||
Verify: full checklist below.
|
|
||||||
|
|
||||||
## Implementation notes
|
|
||||||
|
|
||||||
- Allocator creation: `unsafe { vk_mem::Allocator::new(create_info) }?`; set
|
|
||||||
`preferred_large_heap_block_size` to ~64 MiB (VMA's default is 256 MiB).
|
|
||||||
`vulkan_api_version` must not exceed the instance's requested version or the
|
|
||||||
selected physical device's supported version: record the instance request
|
|
||||||
(add `pub api_version: u32` to `InstanceInner` when building `ApplicationInfo`
|
|
||||||
in `instance.rs:126-135`) and clamp it with the physical device's
|
|
||||||
`limits.api_version` at device selection, passing the effective minimum (a
|
|
||||||
safe floor is 1.1 when both sides support it, else 1.0). Dedicated-allocation
|
|
||||||
structs require API ≥ 1.1; on Vulkan 1.0 devices require
|
|
||||||
`VK_KHR_dedicated_allocation` (+ `VK_KHR_get_memory_requirements2`) at device
|
|
||||||
creation or skip `DEDICATED_MEMORY` there.
|
|
||||||
- Do not enable `EXTERNALLY_SYNCHRONIZED`; keep the `Mutex`.
|
|
||||||
- Allocation flags: `MAPPED` for `CpuToGpu`/`GpuToCpu`, plus `DEDICATED_MEMORY`
|
|
||||||
for `AllocationStrategy::Dedicated`.
|
|
||||||
- **Coherence policy (required)**: VMA's `CpuToGpu`/`GpuToCpu` usages guarantee
|
|
||||||
host-visible memory, not host-coherent memory, but the current mapping API
|
|
||||||
(`mapped_slice`/`mapped_slice_mut`) has no flush/invalidate step. Require
|
|
||||||
coherent memory: compute the memory-type bitmask of types with
|
|
||||||
`HOST_VISIBLE | HOST_COHERENT` from the physical device's memory properties
|
|
||||||
and set `AllocationCreateInfo.memory_type_bits` for CPU-mapped allocations;
|
|
||||||
fail the allocation if no such type exists (stricter than gpu-allocator,
|
|
||||||
which silently falls back to non-coherent types). If a non-coherent-only
|
|
||||||
device must be supported later, switch to explicit
|
|
||||||
`Allocator::flush_allocation`/`invalidate_allocation` around map windows and
|
|
||||||
change the API contract accordingly — do not ship a silent fallback.
|
|
||||||
- **Handle guards before allocation**: the buffer/image flows create raw Vulkan
|
|
||||||
handles before allocating (`buffers.rs:108-130`, `images.rs:191-223`); wrap
|
|
||||||
each raw handle in `DeviceObject` immediately after creation and establish
|
|
||||||
`Allocation` ownership (`DeviceObject<GpuAllocation>`) before binding, so
|
|
||||||
every failure path — allocation failure or bind failure — releases the handle
|
|
||||||
and the allocation in the correct order (destroy handle first, then
|
|
||||||
`free_memory`). No raw handle may escape a fallible span unowned.
|
|
||||||
- Free with `free_memory` after the buffer/image handle is destroyed; do not
|
|
||||||
also call `destroy_buffer/destroy_image` for the same allocation.
|
|
||||||
|
|
||||||
## Validation checklist
|
|
||||||
|
|
||||||
- `cargo check -p renderer_device`, `-p renderer_resources`, `-p renderer`,
|
|
||||||
`-p renderer_rendergraph`, then `cargo check --workspace`.
|
|
||||||
- `cargo test -p renderer_device` and `cargo test -p renderer_device --features
|
|
||||||
testing` (real Vulkan/validation; skips if unavailable).
|
|
||||||
- `cargo test -p renderer_resources` and `cargo test -p renderer_rendergraph`
|
|
||||||
(both exercise `TestVulkan` through their dev-dependency features).
|
|
||||||
- `cargo tree -d | rg '^ash'` should show one ash. The lockfile has two
|
|
||||||
`gpu-allocator 0.28.0` packages (registry + git), so `cargo tree -i
|
|
||||||
gpu-allocator` is ambiguous; use source-qualified package IDs:
|
|
||||||
`cargo tree -i 'registry+https://github.com/rust-lang/crates.io-index#gpu-allocator@0.28.0'`
|
|
||||||
should list only `wgpu-hal` via the Bevy dev-dependency, and
|
|
||||||
`cargo tree -i 'https://github.com/janis-bhm/gpu-allocator#gpu-allocator@0.28.0'`
|
|
||||||
should be empty after phase 3.
|
|
||||||
`rg -n 'gpu_allocator|gpu-allocator' --glob '!Cargo.lock'` should return
|
|
||||||
nothing.
|
|
||||||
- Re-run the baseline Vulkan smoke tests and, if a display is available,
|
|
||||||
`cargo build --examples`.
|
|
||||||
|
|
||||||
## Risks & gotchas
|
|
||||||
|
|
||||||
- **FFI/lifetimes**: `vk_mem::Allocator` is a raw-pointer owner; it must outlive
|
|
||||||
every `Allocation`. The existing `DeviceInner` field order (`alloc2` before
|
|
||||||
`_drop`) is correct; do not move the allocator after `_drop`. `GpuAllocation`
|
|
||||||
must not be `Copy` and must free exactly once through
|
|
||||||
`DeviceObject`/`ExternallyManagedObject`.
|
|
||||||
- **Thread safety**: `AllocationInfo.mapped_data` is `*mut c_void`, which is not
|
|
||||||
`Send/Sync`; storing it directly breaks `Arc<Buffer>` thread-safety. Use a
|
|
||||||
private `SendSyncPtr` or unsafe `Send/Sync` wrapper and add a compile-time
|
|
||||||
`assert_send_sync::<Buffer>()`/`Image` test.
|
|
||||||
- **Mapping differs**: `gpu-allocator` mapped host-visible blocks
|
|
||||||
opportunistically; VMA maps only with `MAPPED`. `CpuToGpu`/`GpuToCpu` keep
|
|
||||||
working; `GpuOnly` mapping on integrated GPUs may now return `None`.
|
|
||||||
**Coherence**: VMA only guarantees host-visible (not host-coherent) memory
|
|
||||||
for CPU usages; this plan requires coherent types via `memory_type_bits`
|
|
||||||
(see implementation notes) so the existing flush-free `map`/`map_mut` usage
|
|
||||||
stays correct. Non-coherent support would need explicit
|
|
||||||
`flush_allocation`/`invalidate_allocation`; current code does not
|
|
||||||
flush/invalidate either.
|
|
||||||
- **Dedicated allocation**: use `allocate_memory_for_buffer/image` with
|
|
||||||
`DEDICATED_MEMORY`; generic `allocate_memory` would create a separate block
|
|
||||||
without dedicated buffer/image info. `VkMemoryDedicatedAllocateInfo` is only
|
|
||||||
emitted when the runtime API version is ≥ 1.1 (or the KHR dedicated
|
|
||||||
extensions are enabled); pass the clamped effective version per the
|
|
||||||
implementation notes and require the extensions on Vulkan 1.0 devices.
|
|
||||||
- **OOM handling**: `gpu_allocator::AllocationError` variants disappear; VMA
|
|
||||||
returns `vk::Result` (`ERROR_OUT_OF_DEVICE_MEMORY`,
|
|
||||||
`ERROR_FEATURE_NOT_PRESENT`, etc.), already handled by `Error::Result`. No
|
|
||||||
call site pattern-matches the old variant.
|
|
||||||
- **Build environment**: `vk-mem 0.5.0` compiles VMA C++ (`wrapper.cpp`, C++17,
|
|
||||||
links stdc++ on Linux); ensure CI has a C++ compiler. It pins ash 0.38; an ash
|
|
||||||
upgrade requires a matching `vk-mem` release/fork. The crate is less actively
|
|
||||||
maintained than `gpu-allocator`.
|
|
||||||
- **Lost features**: no `vk_mem` allocation naming setter
|
|
||||||
(`vmaSetAllocationName`) and no `build_stats_string` in 0.5.0; VMA
|
|
||||||
stats/heap budgets remain available. Defragmentation is intentionally out of
|
|
||||||
scope; if enabled later, cached `memory/offset/mapped_data` in the wrapper
|
|
||||||
must be re-queried.
|
|
||||||
- **Pre-existing hazard**: `Image::new_with_allocation` still creates the raw
|
|
||||||
image before validation (`images.rs:235-260`); the handle-guard requirement
|
|
||||||
(implementation notes) covers this by owning the raw image in `DeviceObject`
|
|
||||||
from creation onward.
|
|
||||||
|
|
@ -1,575 +0,0 @@
|
||||||
# Plan: Render-Pass Support (`BeginRendering` / `EndRendering`) in `renderer_rendergraph`
|
|
||||||
|
|
||||||
Status: proposed — rev 5 (rev-1 correctness, rev-2 performance, rev-3 adversarial, and rev-4 re-review applied; rev-4 verdict was REJECT, findings resolved here)
|
|
||||||
Date: 2026-10-27
|
|
||||||
Scope: `crates/renderer_rendergraph`
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Add dynamic-rendering render-pass support to the rendergraph: `BeginRendering`
|
|
||||||
and `EndRendering` commands that bracket a span of graph commands, with the
|
|
||||||
compiler guaranteeing that every command recorded between the two is emitted
|
|
||||||
inside the rendering instance — i.e. commands that must be in a rendergraph
|
|
||||||
pass are ordered by the begin/end rendering commands, and no foreign command
|
|
||||||
may be scheduled between them.
|
|
||||||
|
|
||||||
## Current state (verified against code)
|
|
||||||
|
|
||||||
- Commands implement `Command` (declares `side_effects` → `SubresourceScope`
|
|
||||||
accesses) + `RecordedCommand` (`preflight`/`apply`), and are pushed via
|
|
||||||
`RenderGraph::push` into `CommandList` (`commands.rs`, `recorder.rs`).
|
|
||||||
- `GraphCompiler::dependencies()` (`graph.rs:820-863`) builds hazard edges
|
|
||||||
purely from resource conflicts; `topo_batches_and_dependencies()`
|
|
||||||
(`graph.rs:870-889`) Kahn-sorts commands into `batches`;
|
|
||||||
`record_commands` (`graph.rs:2500-2554`) flattens batches, emitting barriers
|
|
||||||
(keyed `before_command` via `grouped_barrier_indices`) immediately before
|
|
||||||
each command.
|
|
||||||
- There is no notion of a render pass. Nothing prevents the flattener from
|
|
||||||
emitting an unrelated command between two commands the user recorded
|
|
||||||
adjacently: Kahn batching interleaves hazard-free commands freely.
|
|
||||||
- `CommandRecorder` (`recorder.rs:159-164`) maps `ResourceId → vk::Buffer` /
|
|
||||||
`vk::Image` only; there is no image-view plumbing. Descriptor imports read
|
|
||||||
`Arc<ImageView>` bindings at import time (`graph.rs:507-513`) but retain
|
|
||||||
only the underlying image (`StagedImportResource::Texture(view.image().clone())`,
|
|
||||||
`graph.rs:524-532`) — attachment views are new plumbing, not an existing
|
|
||||||
pattern to reuse.
|
|
||||||
- `CommandList` stores type-erased `Box<dyn RecordedCommand>` (`recorder.rs:97-109`);
|
|
||||||
the `Command` trait exposes only `side_effects` (`commands.rs:14-16`), so
|
|
||||||
there is currently no way to classify a command or introspect its render-pass
|
|
||||||
descriptor.
|
|
||||||
- The graph documents a single-queue submission contract (`graph.rs:8-10`);
|
|
||||||
ordinary barriers set `queue_transfer: None` (`graph.rs:972-982, 1001-1012`).
|
|
||||||
- Compile-time cost profile (recomputed independently): `dependencies()` is
|
|
||||||
`O(Σ_r u_r²)` pair checks (plus a per-pair scope cross-product factor);
|
|
||||||
`HappensBefore::from_dag` allocates one `FixedBitSet` row per node
|
|
||||||
(`graph.rs:213-217`, `O(N²/64)` memory) and unions per edge
|
|
||||||
(`O(E·N/64)` time). `ResourceInterference::from_resource_uses` **already**
|
|
||||||
computes terminal/initial frontier sets once per resource
|
|
||||||
(`graph.rs:1348-1356`); the remaining recomputations are in
|
|
||||||
`first_fit_placement` (`graph.rs:1642-1649`), the `overlapping` loop in
|
|
||||||
`plan_placements` (`graph.rs:2384-2399`), and `build_alias_barriers`
|
|
||||||
(`graph.rs:1088, 1109-1117`). Contraction does not remove those; see D8.
|
|
||||||
- The crate is not yet referenced by `renderer`/`bevy_vulkan_render`; the
|
|
||||||
current per-frame path is the legacy `renderer::render_graph`. If this
|
|
||||||
crate replaces it, `compile()` becomes per-frame, so compile cost matters.
|
|
||||||
- `dynamic_rendering` and `synchronization2` are requested/enabled
|
|
||||||
(`crates/renderer/src/lib.rs:352-357`, `crates/renderer_device/src/testing.rs:166-172`).
|
|
||||||
- **Pre-existing defect (reproduced):** `build_barriers` records only the last
|
|
||||||
*conflicting* producer's layout (`graph.rs:951-982`) and never chains
|
|
||||||
`old_layout` through earlier barriers. Two same-layout readers of one
|
|
||||||
producer yield two image barriers both claiming `oldLayout = producerLayout`;
|
|
||||||
the second is stale (Vulkan requires `oldLayout` be the current layout or
|
|
||||||
`UNDEFINED`). Fixing this generally (per-subresource current-layout map) is
|
|
||||||
part of Task 3 — the plan's own tests hit it.
|
|
||||||
- `Image::create_view` returns `Result<ManuallyDrop<ImageView>>`
|
|
||||||
(`renderer_resources/src/images.rs:558-621`); storing it directly leaks the
|
|
||||||
`vk::ImageView` and the `Arc<Image>` (the existing renderer calls `.raw()`
|
|
||||||
on the `ManuallyDrop`, `crates/renderer/src/rendering/mod.rs:332`, which
|
|
||||||
leaks today). New code must use `ManuallyDrop::into_inner`.
|
|
||||||
- `ImageView` holds `Arc<Image>` (`images.rs:1123-1127`), so a view cache
|
|
||||||
keeps its image alive; drop-order interaction with `CompiledGraph` is benign
|
|
||||||
provided the `ManuallyDrop` is unwrapped.
|
|
||||||
|
|
||||||
## Design decisions
|
|
||||||
|
|
||||||
### D1 — Dynamic rendering, not `VkRenderPass`
|
|
||||||
|
|
||||||
New commands `BeginRendering` / `EndRendering` wrapping `vkCmdBeginRendering`
|
|
||||||
/ `vkCmdEndRendering` (Vulkan 1.3 `dynamic_rendering`). Attachments are graph
|
|
||||||
`Texture` handles plus a view specification (see D9 for the created-texture
|
|
||||||
problem).
|
|
||||||
|
|
||||||
### D2 — The pass span is an atomic scheduling unit, contracted *before* hazard-edge construction
|
|
||||||
|
|
||||||
The compiler contracts the `[begin, end]` command span into a single DAG node
|
|
||||||
("pass node"). This is the mechanism that satisfies the ordering contract:
|
|
||||||
|
|
||||||
- No foreign command can ever be scheduled inside the span (Kahn batching
|
|
||||||
would otherwise interleave hazard-free commands into it).
|
|
||||||
- The whole span is ordered relative to everything else by the begin/end
|
|
||||||
hazard edges.
|
|
||||||
|
|
||||||
Contraction happens **before** `dependencies()` builds hazard edges. This
|
|
||||||
removes the `O(Σ u_r²)` interior-draw edge construction (edges that would be
|
|
||||||
discarded as self-edges anyway) and shrinks the `O(N²/64)` closure memory/time
|
|
||||||
by roughly `(N_c/N)²`.
|
|
||||||
|
|
||||||
The contraction mapping used by every consumer:
|
|
||||||
|
|
||||||
- **Type-safe node indexing:** use `NodeId(NonZeroU32)` newtypes and
|
|
||||||
`Option<NodeId>` rather than raw `u32` sentinels mixed into `CommandIndex`
|
|
||||||
spaces. To minimize churn, keep `batches: Vec<Vec<CommandIndex>>` with the
|
|
||||||
`BeginRendering` command index as the documented pass-node representative;
|
|
||||||
`spans: Vec<Span>` on `CompiledGraph` records `{ begin, end, interior }`
|
|
||||||
for expansion. Document the semantic change to the public `batches()` /
|
|
||||||
`dependency_edges()` accessors. Closure rows are indexed `0..N_c`.
|
|
||||||
- **HappensBefore is span-aware internally:** `HappensBefore` (public,
|
|
||||||
`lib.rs:10`) stores the command→node map and span slices and translates in
|
|
||||||
`contains`/`happens_before` (`graph.rs:244-251`), so command-space queries
|
|
||||||
keep working and interior indices never silently return `false` from a
|
|
||||||
node-space row lookup.
|
|
||||||
- **Intra-span record order is NOT a happens-before fact.** Interior commands
|
|
||||||
map to the pass node and same-node uses are treated as **unordered** by the
|
|
||||||
closure used for aliasing/interference. This is a memory-correctness
|
|
||||||
requirement, not a style choice: if intra-span record order were visible to
|
|
||||||
`resources_alias_in_order`, a created attachment `A` (single use at `Begin`)
|
|
||||||
and a created storage image `B` used only by an interior command would be
|
|
||||||
judged ordered, `first_fit_placement` could place `B` over `A`'s memory, and
|
|
||||||
`build_alias_barriers` would either emit no barrier or an illegal in-pass
|
|
||||||
`UNDEFINED → layout` transition — discarding a live render target mid-pass.
|
|
||||||
Intra-span record order is needed only by `build_barriers` (which iterates
|
|
||||||
execution order directly) and by recording; it must not leak into the
|
|
||||||
aliasing/interference closure.
|
|
||||||
- **Attachment liveness is modeled through `End`.** `EndRendering` carries
|
|
||||||
synthetic accesses (the attachment layout, `COLOR_ATTACHMENT_WRITE` /
|
|
||||||
`DEPTH_STENCIL_ATTACHMENT_WRITE` at the attachment stage) used **only** for
|
|
||||||
lifetime/alias analysis — excluded from barrier generation (the begin's
|
|
||||||
merged scope already covers synchronization) and from usage derivation.
|
|
||||||
This makes `terminal_uses(attachment) == End`, so an attachment is never
|
|
||||||
judged dead before the store.
|
|
||||||
- **Barrier analysis:** `build_barriers` walks scheduled command indices and
|
|
||||||
reads only `accesses[command_index]` (`graph.rs:930-934`). Barrier analysis
|
|
||||||
runs over the *uncontracted* access list in **execution order** (see D6),
|
|
||||||
with each interior command's scheduling position mapped to the pass node.
|
|
||||||
This preserves, e.g., the barrier for an outside producer consumed only by
|
|
||||||
an interior `Draw`.
|
|
||||||
- **Lifetimes:** `bind_lifetimes` indexes `batch_for_command` for every
|
|
||||||
original access (`graph.rs:1258-1275`); interior commands map to the pass
|
|
||||||
node's batch index so they are never left at `usize::MAX`. Placement uses
|
|
||||||
command-level interference, not batch intervals, so placement quality is
|
|
||||||
unaffected; external consumers of `resource_lifetimes()` get coarser
|
|
||||||
intervals for interior commands (documented).
|
|
||||||
- **Self-edges:** intra-span hazard pairs are skipped during edge
|
|
||||||
construction (they become intra-span execution-order constraints handled by
|
|
||||||
`build_barriers`, not DAG edges). Note `dependencies()` only adds
|
|
||||||
prior→current edges (`graph.rs:825-861`), so the DAG is acyclic regardless;
|
|
||||||
only a literal self-edge would trip `Kahn`, and skipping intra-span pairs
|
|
||||||
prevents exactly that.
|
|
||||||
|
|
||||||
### D3 — Access declaration: one merged scope per attachment; interior accesses preserved
|
|
||||||
|
|
||||||
- `BeginRendering` declares **one access per attachment** with
|
|
||||||
`access = COLOR_ATTACHMENT_WRITE | (LOAD ? COLOR_ATTACHMENT_READ : 0)` at
|
|
||||||
the same stage (`COLOR_ATTACHMENT_OUTPUT` / `EARLY|LATE_FRAGMENT_TESTS`),
|
|
||||||
subresource range from the view (concrete range rules in D9). Declaring
|
|
||||||
read and write as two separate accesses is a blocker:
|
|
||||||
`SideEffectMap::insert` deliberately keeps opposite-polarity scopes separate
|
|
||||||
(`recorder.rs:55-82`), and `build_barriers` then emits **two**
|
|
||||||
`ImageMemoryBarrier2` entries for the same image/range/layout in one
|
|
||||||
`vkCmdPipelineBarrier2` (reproduced). A single merged scope is handled
|
|
||||||
correctly by `SideEffectMap`, `conflicts`, `derive_image_usage`
|
|
||||||
(`graph.rs:2696-2709`), and the barrier builder.
|
|
||||||
- `EndRendering`'s synthetic accesses (D2) are tagged so barrier generation
|
|
||||||
and usage derivation skip them.
|
|
||||||
- Interior command accesses are *not* replaced by the begin declarations;
|
|
||||||
they remain in the access list (see D2 barrier analysis) so the compiler
|
|
||||||
can emit barriers for outside producers consumed inside the pass.
|
|
||||||
- Because the span is contracted, any later consumer is ordered after the
|
|
||||||
*entire pass node*, i.e. after `EndRendering` — matching store-visibility
|
|
||||||
semantics (verified: the consumer's barrier takes
|
|
||||||
`COLOR_ATTACHMENT_OUTPUT`/`COLOR_ATTACHMENT_WRITE` as src, and hoisting a
|
|
||||||
barrier before `BeginRendering` with those dst stages is sufficient
|
|
||||||
visibility for LOAD reads and in-pass attachment reads).
|
|
||||||
- **Intra-span hazard policy (narrowed):**
|
|
||||||
- **Allowed inside a span:** any barrier with `old_layout == new_layout` —
|
|
||||||
memory barriers, buffer barriers, *and* image barriers with no layout
|
|
||||||
change (a storage-image read/write hazard at `GENERAL` needs an
|
|
||||||
`ImageMemoryBarrier2`; buffer barriers do not synchronize image
|
|
||||||
accesses). These are legal inside a rendering instance.
|
|
||||||
- **Rejected at compile time** (`GraphError::UnsupportedIntraPassDependency`):
|
|
||||||
intra-span hazards requiring an image layout transition (`old != new`).
|
|
||||||
The rejection operates on the **generated barrier plan** (D5), not on an
|
|
||||||
independent declared-layout scan.
|
|
||||||
- **Feedback loops:** defined as any interior access (read *or* write)
|
|
||||||
whose `(aspect, mip, layer)` set intersects a `Begin` attachment
|
|
||||||
subresource, excluding the begin's own load operation. A storage-image
|
|
||||||
write to an attachment subresource is equally a feedback loop; an
|
|
||||||
interior read of a disjoint mip/layer is legal. Genuine feedback loops
|
|
||||||
require splitting.
|
|
||||||
- In-pass barrier emission is part of Task 4, not a follow-up: the recorder
|
|
||||||
emits the barrier between the two interior commands in execution order.
|
|
||||||
- **Aliasing vs. interior first uses is a placement constraint, not a
|
|
||||||
validation message.** For image resources, add an interference edge (or an
|
|
||||||
explicit `aliasing_allowed` predicate consulted by `plan_placements` /
|
|
||||||
`first_fit_placement`) whenever a required alias transition would land at a
|
|
||||||
command that maps to a pass node — covering interior *and* boundary
|
|
||||||
(`Begin`/`End`) positions. There is no "reject aliasing" hook after
|
|
||||||
`plan_placements` commits offsets, so this must be enforced before
|
|
||||||
placement. Attachments themselves are safe: with D2's `End` modeling,
|
|
||||||
their first use is `Begin` and their terminal use is `End`, both boundary
|
|
||||||
commands of the pass node itself.
|
|
||||||
|
|
||||||
### D4 — Barrier placement: hoist cross-boundary barriers to BeginRendering, with per-subresource layout handling
|
|
||||||
|
|
||||||
Barriers whose `before_command` targets an interior command but whose origin
|
|
||||||
lies outside the span are re-keyed to the span's `BeginRendering` command,
|
|
||||||
after batching when spans are known. Constraints:
|
|
||||||
|
|
||||||
- **Barrier origin must be explicit.** `BarrierResource` (`graph.rs:84-95`)
|
|
||||||
records only masks/layout — no source command and no initial-vs-command
|
|
||||||
origin — so a post-pass hoisting pass cannot distinguish an outside-producer
|
|
||||||
barrier from an initial-state transition. Add
|
|
||||||
`src_command: Option<CommandIndex>` or a
|
|
||||||
`BarrierOrigin::{Initial, Command(CommandIndex)}` to `BarrierResource`.
|
|
||||||
**Initial-state transitions for interior first uses are hoisted to
|
|
||||||
`Begin`** and are not covered by the intra-span rejection (a transient
|
|
||||||
storage image first used inside a pass is the common case, not an edge
|
|
||||||
case).
|
|
||||||
- **Coalesce hoisted image transitions per `(resource, exact range,
|
|
||||||
target_layout)` into one barrier** with a unioned `dst_stage/dst_access`
|
|
||||||
(safe: the dst scope is a wait target; unioning only makes all consumers
|
|
||||||
wait). Layout chaining is defined **per `(aspect, mip, layer)`**, not per
|
|
||||||
barrier: for overlapping-but-unequal ranges, transition only the
|
|
||||||
subresources still in the producer layout; for disjoint ranges, emit
|
|
||||||
independent `producer → layout` transitions. The earlier "chain different
|
|
||||||
target layouts" rule is removed: for same-pass overlapping ranges it is
|
|
||||||
unreachable (D3 rejects the required in-pass transition), and for disjoint
|
|
||||||
ranges chaining `producer → A → B` is simply wrong (the second range is
|
|
||||||
still in the producer layout).
|
|
||||||
- The general `build_barriers` fix (per-subresource current-layout map, not
|
|
||||||
"last conflicting access's layout") makes non-hoisted multi-reader cases
|
|
||||||
correct too; D4's rules then only govern the hoisted subset.
|
|
||||||
- After hoisting, rebuild `barrier_indices_by_command` and
|
|
||||||
`ordinary_barrier_count` (`O(B)`), and apply the same coalescing in
|
|
||||||
`rebuild_barriers` (`graph.rs:2092-2106`) for alias barriers.
|
|
||||||
**Sequencing:** hoisting runs before `build_alias_barriers`, and hoisted
|
|
||||||
barriers live strictly within the ordinary prefix so `rebuild_barriers`'
|
|
||||||
truncation to `ordinary_barrier_count` (`graph.rs:2098`) cannot drop or
|
|
||||||
duplicate them; `rebuild_barriers` re-runs hoisting after the alias rebuild.
|
|
||||||
- Consumers after the pass keep their own barrier keys; expansion emits them
|
|
||||||
after `EndRendering` naturally. No barrier targets `EndRendering` itself
|
|
||||||
(its synthetic accesses are excluded from barrier generation).
|
|
||||||
- In-pass barriers (per D3) are emitted between the two interior commands in
|
|
||||||
execution order; they are not hoisted.
|
|
||||||
|
|
||||||
### D5 — Compile-time validation on the generated barrier plan, all linear
|
|
||||||
|
|
||||||
New `GraphError` variants for:
|
|
||||||
|
|
||||||
- unbalanced begin/end; nested/overlapping spans (dynamic rendering cannot
|
|
||||||
nest); `EndRendering` before its `BeginRendering` in record order;
|
|
||||||
- graphics commands outside any span (`GraphError::DrawOutsideRenderPass`),
|
|
||||||
and **transfer/clear commands inside a span** (`CopyBuffer`,
|
|
||||||
`ClearTexture`, and any custom command not marked render-pass-legal —
|
|
||||||
`vkCmdCopyBuffer2`/`vkCmdClearColorImage` are illegal inside a rendering
|
|
||||||
instance). Classification uses a `fn kind(&self) -> CommandKind` method on
|
|
||||||
**`RecordedCommand`** (not `Command` — a method on `Command` is
|
|
||||||
unreachable after `Box<dyn RecordedCommand>` erasure), returning
|
|
||||||
`CommandKind::{Begin, End, Graphics, Transfer, Compute, Other}` plus a
|
|
||||||
`fn allowed_in_render_pass(&self) -> bool` (default false; `Draw` → true;
|
|
||||||
test/custom no-op commands may override so Task 5's in-pass barrier tests
|
|
||||||
can exist). **Note:** five existing unit tests compile graphs containing
|
|
||||||
`Draw` with no pass (pushes at `graph.rs:3563, 3749, 3815, 4329, 4469`)
|
|
||||||
and must be updated (wrap in begin/end or make the restriction opt-in) —
|
|
||||||
added to Task 5.
|
|
||||||
- unsupported intra-span dependencies (per D3). **The check operates on the
|
|
||||||
generated barrier plan after hoisting/chaining, not on an independent
|
|
||||||
declared-layout scan:** after hoisting, any barrier whose `before_command`
|
|
||||||
is interior and whose `old_layout != new_layout` is either hoistable
|
|
||||||
(initial/outside origin) or must be rejected. This is exact, `O(B)`, and
|
|
||||||
removes the fragile per-subresource validation machinery whose
|
|
||||||
`previous`-selection (`graph.rs:951-982`) can diverge from declared
|
|
||||||
layouts (e.g. producer `P` at layout `P0`, outside reader `R1` transitions
|
|
||||||
to `A`, interior reader `R2` reads at `A`: a declared-layout scan sees
|
|
||||||
`A → A` and accepts, while the un-fixed builder would emit `P0 → A` at
|
|
||||||
`R2` inside the pass).
|
|
||||||
- attachment consistency: each attachment view must refer to the same image
|
|
||||||
as its graph `Texture` — compare **raw handles** (`Image: PartialEq`
|
|
||||||
compares raw handles, `images.rs:484-488`; `Arc::ptr_eq` would
|
|
||||||
false-positive), consistent with import dedup by raw handle
|
|
||||||
(`graph.rs:389-399`). Applies to the imported case at compile time and to
|
|
||||||
the created case after view creation (D9). The recorder binds graph images
|
|
||||||
by `ResourceId` (`graph.rs:2513-2529`); this check closes that gap.
|
|
||||||
- attachment layout legality: the caller-specified layout must be a legal
|
|
||||||
rendering layout (`COLOR_ATTACHMENT_OPTIMAL`, `GENERAL`, etc.). The
|
|
||||||
previously proposed "layout must match compiler-produced state" rule is
|
|
||||||
vacuous — the barrier's `new_layout` is derived from the begin's own
|
|
||||||
declared access, so no independent state exists.
|
|
||||||
- duplicate attachment subresources: two attachments covering the same
|
|
||||||
subresource would be silently merged by `SideEffectMap::insert`
|
|
||||||
(`recorder.rs:55-82`), making them invisible from `command_accesses`;
|
|
||||||
reject via the introspection hook (below), using a per-subresource set
|
|
||||||
(`O(covered subresources)`, not pairwise `O(a²)`).
|
|
||||||
- queue ownership: passes cannot cross queues or require ownership
|
|
||||||
transfers; the single-queue submission contract (`graph.rs:8-10`) applies
|
|
||||||
unchanged, and any access requiring a queue transfer inside or across a
|
|
||||||
span is rejected until paired transfer/barrier support exists.
|
|
||||||
- **Introspection hook:** commands are type-erased, and `CommandKind` carries
|
|
||||||
no data, so Task 2's attachment validations (view/image identity, duplicate
|
|
||||||
subresources, layout legality) need a `fn render_pass_desc(&self) ->
|
|
||||||
Option<&RenderPassDesc>` method on `RecordedCommand` (or a validation hook
|
|
||||||
called from `compile()`, or an attachment summary stored in
|
|
||||||
`CommandList::push`, which is generic over `C: Command`). Without one,
|
|
||||||
these checks are not implementable as written.
|
|
||||||
- **Error-path safety:** `record_commands` returning mid-pass must not leave
|
|
||||||
an unclosed rendering instance; track an `in_render_pass` flag in the
|
|
||||||
recorder/compiled graph, end the rendering instance (or discard the command
|
|
||||||
buffer) on the error path, and reset the flag (`graph.rs:2540-2553`).
|
|
||||||
|
|
||||||
### D6 — Execution order is precomputed at compile time
|
|
||||||
|
|
||||||
Compile an `execution_order: Vec<CommandIndex>` once: expanded contracted
|
|
||||||
batches — begin → interiors in **original record order** → end. Barrier
|
|
||||||
analysis and recording both iterate it; `batches` is retained (begin-index
|
|
||||||
representative) for future parallel recording. This avoids per-frame
|
|
||||||
expansion branches and keeps `recording_steps_cached` (`graph.rs:2595-2608`)
|
|
||||||
allocation-free. Interior commands are emitted in record order, not topo
|
|
||||||
order: ordering within the pass is the user's responsibility, matching the
|
|
||||||
"ordered by begin/end" contract.
|
|
||||||
|
|
||||||
**Batch-index assertion fix:** `build_barriers_with_initial_states` asserts
|
|
||||||
`batch_indices[previous] < batch_indices[current]` (`graph.rs:962-966`).
|
|
||||||
With interior commands mapped to the pass node's batch index, an allowed
|
|
||||||
intra-span hazard gives equal batch indices → debug panic. Use
|
|
||||||
**execution-order positions** for that assertion, not batch indices.
|
|
||||||
|
|
||||||
### D7 — Recording is allocation-free per frame
|
|
||||||
|
|
||||||
- `BeginRendering::apply` precomputes `vk::RenderingAttachmentInfo`s in the
|
|
||||||
command (or uses `ArrayVec`/thread-local scratch, mirroring
|
|
||||||
`pipeline_barrier`'s `BARRIER_SCRATCH` pattern, `recorder.rs:217-292`) so
|
|
||||||
no `Vec` is built per frame. Resolving attachment images per frame is
|
|
||||||
`O(a log R)` `BTreeMap` lookups — negligible.
|
|
||||||
- **View plumbing into `apply`:** `RecordedCommand::apply` receives only
|
|
||||||
`CommandRecorder` (`recorder.rs:108`), which cannot see `CompiledGraph`'s
|
|
||||||
view cache. Extend `CommandRecorder` with a view map
|
|
||||||
(`ResourceId → Arc<ImageView>`, or `ResourceId → Arc<Image>` plus
|
|
||||||
command-local view cache populated via `&mut self`) populated by
|
|
||||||
`record_commands`. `BeginRendering::apply` looks up the view; a missing or
|
|
||||||
unresolved view is a new `RecordError::MissingView`. Views are created
|
|
||||||
with `ManuallyDrop::into_inner(create_view(...))` — never store the
|
|
||||||
`ManuallyDrop` directly (leak).
|
|
||||||
- **Resolve idempotence:** views are created only after a fully successful
|
|
||||||
placement pass, atomically; a failed view creation must not leave a
|
|
||||||
partially-populated cache that later makes `Arc::get_mut`
|
|
||||||
(`graph.rs:2472-2480`) fail on a retried placement. Repeated `resolve()`
|
|
||||||
calls are idempotent; `create_view` failures map to `GraphError` variants.
|
|
||||||
|
|
||||||
### D8 — Frontier reuse for placement and alias barriers (performance amendment, corrected)
|
|
||||||
|
|
||||||
`ResourceInterference::from_resource_uses` already caches frontier sets per
|
|
||||||
resource (`graph.rs:1348-1356`); the plan does not claim to change it. The
|
|
||||||
actual recomputation hot spots contraction does not touch are
|
|
||||||
`first_fit_placement` (`graph.rs:1642-1649`), the `overlapping` loop in
|
|
||||||
`plan_placements` (`graph.rs:2384-2399`), and `build_alias_barriers`
|
|
||||||
(`terminal_uses` per old resource per new resource at `graph.rs:1109-1117`;
|
|
||||||
`initial_uses` per new at `graph.rs:1088`). Compute terminal/initial
|
|
||||||
frontiers **once per resource** and reuse them across all three sites, so
|
|
||||||
the performance rationale is honest: contraction removes the dependency-edge
|
|
||||||
and closure quadratics; frontier reuse removes the placement/alias ones.
|
|
||||||
|
|
||||||
### D9 — Attachment API for created textures
|
|
||||||
|
|
||||||
`RenderGraph::create_texture` returns only a `Texture` id (`graph.rs:352-354`);
|
|
||||||
the `Arc<Image>` is created privately in `plan_placements`/`resolve`
|
|
||||||
(`graph.rs:2129-2495`) and `CompiledGraph` exposes no accessor, so a caller
|
|
||||||
can never supply an `Arc<ImageView>` for a transient render target — the
|
|
||||||
main use case. Resolution:
|
|
||||||
|
|
||||||
- `ColorAttachment.view` is an enum:
|
|
||||||
`AttachmentView::Created(ImageViewDesc) | Imported(Arc<ImageView>)`.
|
|
||||||
- **`Created(ImageViewDesc)` must carry concrete aspect/mip/layer ranges**
|
|
||||||
(`REMAINING_*` sentinels are rejected at validation): `side_effects` runs
|
|
||||||
at `push` time (`recorder.rs:119-124`), before `resolve()` creates any
|
|
||||||
image, `image_view_subresource_range` is private (`images.rs:1183`), and
|
|
||||||
`create_view` normalizes/mutates the desc (`set_max_end`, format fill-in,
|
|
||||||
`images.rs:566-589`; `MipRange` addition panics on overflow in debug for
|
|
||||||
`base > 0` with a `REMAINING` count, `images.rs:1319-1333`). The declared
|
|
||||||
access range therefore comes from the desc, and is validated against the
|
|
||||||
image's mip/layer counts at compile time (from `GraphTextureDesc` /
|
|
||||||
imported `ImageDesc`).
|
|
||||||
- The crate creates and caches views after `resolve()` (`Image::create_view`
|
|
||||||
needs `&Arc<Image>`, `images.rs:558`), keyed by `(Texture, ImageViewDesc)`;
|
|
||||||
`BeginRendering::apply` consumes the cached view via the D7 recorder view
|
|
||||||
map.
|
|
||||||
- The D5 view/image consistency check applies to the imported case at
|
|
||||||
compile time and to the created case after view creation.
|
|
||||||
|
|
||||||
## Work items (all in `crates/renderer_rendergraph`)
|
|
||||||
|
|
||||||
### Task 1 — New commands in `src/commands.rs`
|
|
||||||
|
|
||||||
- `BeginRendering { color: Vec<ColorAttachment>, depth: Option<DepthStencilAttachment>, ... }`
|
|
||||||
with `ColorAttachment { texture: Texture, view: AttachmentView (D9),
|
|
||||||
layout: vk::ImageLayout, load_op, store_op, clear_value }` (depth/stencil
|
|
||||||
analogous). `EndRendering` (unit struct).
|
|
||||||
- `Command::side_effects`: `BeginRendering` inserts **one merged scope per
|
|
||||||
attachment** (write + conditional LOAD read, same stage, concrete range
|
|
||||||
from the desc/view — D3, D9). `EndRendering` inserts synthetic
|
|
||||||
lifetime-only accesses (D2), tagged to be skipped by barrier generation
|
|
||||||
and usage derivation.
|
|
||||||
- Add `fn kind(&self) -> CommandKind` and
|
|
||||||
`fn allowed_in_render_pass(&self) -> bool` and
|
|
||||||
`fn render_pass_desc(&self) -> Option<&RenderPassDesc>` to
|
|
||||||
`RecordedCommand` (D5): `Draw` → `Graphics`/allowed; begin/end →
|
|
||||||
`Begin`/`End`; `CopyBuffer`/`ClearTexture` → `Transfer`/not allowed;
|
|
||||||
default `Other`/not allowed.
|
|
||||||
- `RecordedCommand::apply`: `BeginRendering` builds `vk::RenderingInfo` +
|
|
||||||
precomputed `vk::RenderingAttachmentInfo`s (resolving `vk::Image` via
|
|
||||||
`recorder.image(...)`, using the cached/created view via the D7 view map)
|
|
||||||
and calls `cmd_begin_rendering`; `EndRendering` calls `cmd_end_rendering`.
|
|
||||||
Allocation-free per D7.
|
|
||||||
- Export both from `lib.rs`.
|
|
||||||
|
|
||||||
### Task 2 — Span tracking & validation in `src/graph.rs`
|
|
||||||
|
|
||||||
- Derive spans in `compile()` by scanning command record order using
|
|
||||||
`CommandKind`; build `node_of_command` and `spans`.
|
|
||||||
- Validation pass implementing the D5 rules, including the raw-handle
|
|
||||||
view/image consistency check (imported case), layout legality, duplicate
|
|
||||||
attachment subresources (via the introspection hook), and the
|
|
||||||
classification-based inside/outside-pass checks. Linear per D5.
|
|
||||||
- Update the five existing unit tests that compile `Draw` without a pass
|
|
||||||
(pushes at `graph.rs:3563, 3749, 3815, 4329, 4469`).
|
|
||||||
|
|
||||||
### Task 3 — Span contraction in the compiler
|
|
||||||
|
|
||||||
- Contract spans **before** `dependencies()` per D2: aggregate access scopes
|
|
||||||
per pass node, skip intra-span pairs during edge construction, begin-index
|
|
||||||
node representation, contracted `dependency_edges`; `HappensBefore` stores
|
|
||||||
the command→node map and translates in queries.
|
|
||||||
- Fix the pre-existing stale-`old_layout` defect in `build_barriers`
|
|
||||||
(`graph.rs:951-982`) with a per-subresource current-layout map (D4).
|
|
||||||
- Add `BarrierOrigin` to `BarrierResource` (D4).
|
|
||||||
- Barrier analysis runs over the uncontracted access list in execution order
|
|
||||||
with interior positions mapped to the pass node (D2); switch the
|
|
||||||
`batch_indices` debug assertion to execution-order positions (D6).
|
|
||||||
- `bind_lifetimes`, `HappensBefore::from_dag`, `uses_by_resource`, and
|
|
||||||
`build_alias_barriers` thread the span map through per D2; all closure
|
|
||||||
consumers route through the span-aware query; same-node uses are unordered
|
|
||||||
for aliasing/interference (D2 blocker fix).
|
|
||||||
- Precompute `execution_order` per D6.
|
|
||||||
- Frontier reuse per D8 (`first_fit_placement`, `plan_placements` overlapping
|
|
||||||
loop, `build_alias_barriers`).
|
|
||||||
|
|
||||||
### Task 4 — Barrier hoisting, in-pass barriers & recording
|
|
||||||
|
|
||||||
- Hoist cross-boundary barriers per D4, including initial-state transitions
|
|
||||||
for interior first uses (`BarrierOrigin::Initial`); coalesce per
|
|
||||||
`(resource, exact range, target_layout)` with unioned dst masks;
|
|
||||||
per-`(aspect, mip, layer)` layout handling for overlapping/disjoint ranges.
|
|
||||||
- Enforce the aliasing placement constraint per D3 (interference edge /
|
|
||||||
`aliasing_allowed` predicate) before placement commits offsets.
|
|
||||||
- Emit in-pass barriers (any `old_layout == new_layout`) between interior
|
|
||||||
commands in execution order for allowed intra-span hazards (D3).
|
|
||||||
- Hoisting sequenced before `build_alias_barriers`, hoisted barriers within
|
|
||||||
the ordinary prefix, `rebuild_barriers` re-runs hoisting (D4).
|
|
||||||
- `recording_steps_cached` iterates `execution_order`, expanding pass nodes:
|
|
||||||
barriers (hoisted) → `BeginRendering` → interior commands in record order
|
|
||||||
(with in-pass barriers interleaved) → `EndRendering`.
|
|
||||||
- `record_commands`: expanded steps, D7 view map population, and the D5
|
|
||||||
error-path safety (`in_render_pass` flag, no unclosed rendering instance
|
|
||||||
on mid-pass `RecordError`).
|
|
||||||
|
|
||||||
### Task 5 — Tests
|
|
||||||
|
|
||||||
- Unit tests in `graph.rs`:
|
|
||||||
- span validation errors (unbalanced, nested, draw-outside-pass,
|
|
||||||
transfer-inside-pass, view/image mismatch, layout legality, duplicate
|
|
||||||
attachment subresources, feedback-loop rejection);
|
|
||||||
- contraction correctness (foreign command never appears between begin and
|
|
||||||
end in `execution_order`; interior accesses still produce barriers;
|
|
||||||
lifetimes resolved for interior commands);
|
|
||||||
- barrier hoisting (barrier for an outside producer consumed by an interior
|
|
||||||
command is keyed to begin; **two interior consumers of one
|
|
||||||
outside-produced image coalesce into one barrier with unioned dst masks
|
|
||||||
and correct per-subresource `old_layout`** — regression test for the
|
|
||||||
reproduced blocker; also cover the non-hoisted two-reader case fixed by
|
|
||||||
the general `build_barriers` change);
|
|
||||||
- initial-state hoisting (transient storage image first used inside a pass
|
|
||||||
gets its `UNDEFINED → GENERAL` transition before `BeginRendering`);
|
|
||||||
- in-pass barrier emission (interior storage-buffer *and* storage-image
|
|
||||||
same-layout hazards produce barriers between the interior commands, no
|
|
||||||
pass split; layout-transition hazard is rejected);
|
|
||||||
- **aliasing safety (regression test for the rev-4 blocker):** created
|
|
||||||
attachment `A` + interior-only storage image `B` must **not** share
|
|
||||||
memory, and no barrier may target an interior command for `A`/`B`;
|
|
||||||
attachment `terminal_uses == End`;
|
|
||||||
- ordering of both incoming and outgoing dependencies around the whole
|
|
||||||
span;
|
|
||||||
- span-aware closure queries (`happens_before` with interior endpoints;
|
|
||||||
same-node uses unordered for aliasing).
|
|
||||||
- Integration tests in `tests/graph_validation.rs` (following the existing
|
|
||||||
`TestVulkan` pattern):
|
|
||||||
- skip when `core13.dynamic_rendering != TRUE` in addition to the existing
|
|
||||||
`synchronization2` check (`tests/graph_validation.rs:108-111`);
|
|
||||||
- the previously proposed `Draw`-based graph is **not constructible**
|
|
||||||
(`CopyBuffer` accepts only buffers, `commands.rs:30-79`; `Draw` has no
|
|
||||||
pipeline handle and never binds one, `commands.rs:143-153, 226-335`).
|
|
||||||
Use a custom no-op command (like `ScopedNoop`) with a texture read
|
|
||||||
access as the post-`End` consumer:
|
|
||||||
`ClearTexture → BeginRendering(color=target) → allowed no-op (texture
|
|
||||||
read) → EndRendering → no-op (texture read)` compiles, records,
|
|
||||||
submits, and validates. Adding a real `BindPipeline` command (with a
|
|
||||||
documented sequence edge to the following `Draw`, since pipeline binding
|
|
||||||
declares no resources) is an optional separate work item;
|
|
||||||
- a case proving a hazard-free foreign command is emitted *after*
|
|
||||||
`EndRendering`, not inside the pass;
|
|
||||||
- an outside-producer → interior-consumer case asserting the hoisted
|
|
||||||
barrier appears before `BeginRendering`.
|
|
||||||
|
|
||||||
### Task 6 — Existing test/API churn
|
|
||||||
|
|
||||||
- `batches()` assertions and helpers that assume command-space batches:
|
|
||||||
`graph.rs:3774, 4285, 4388-4393, 4674, 5237, 5245, 5257, 5337, 5343, 5397,
|
|
||||||
5409, 5414` (including `recording_steps(compiled.batches(), …)` at 5343).
|
|
||||||
- `GraphCompiler::new` / `build_barriers` signature changes (execution order +
|
|
||||||
span map) break direct unit-test call sites at `graph.rs:3177-3178, 4125,
|
|
||||||
4242, 4716-4718, 4792-4800, 4846-4854, 4893-4901, 4962-4976, 5051-5052,
|
|
||||||
5102-5104, 5142-5145, 5171-5173`; `HappensBefore::from_dag` is called
|
|
||||||
directly by a test (`graph.rs:4140-4155`) and its signature changes.
|
|
||||||
- Wrapping a `Draw` in a pass shifts barrier indices in tests such as
|
|
||||||
`sampled_image_import_sets_usage_and_layout_barrier` (asserts
|
|
||||||
`compiled.barriers()[0]`, `graph.rs:3837-3845`).
|
|
||||||
- Provide compatibility constructors where cheap, or update all listed call
|
|
||||||
sites; `cargo test -p renderer_rendergraph` must pass at the end.
|
|
||||||
|
|
||||||
## Sequencing
|
|
||||||
|
|
||||||
Tasks 1–2 and 3–4 share types, so execute strictly in order: **1 → 2 → 3 →
|
|
||||||
4 → 5 → 6**, with `cargo check -p renderer_rendergraph` after each and
|
|
||||||
`cargo test -p renderer_rendergraph` at the end. No changes to other crates;
|
|
||||||
`renderer`'s legacy `render_graph` module is out of scope.
|
|
||||||
|
|
||||||
## Performance summary (from rev-2 review, corrected in revs 3–5)
|
|
||||||
|
|
||||||
- **Contraction before `dependencies()`** removes the interior-draw
|
|
||||||
`O(Σ u_r²)` edge construction and shrinks the `O(N²/64)` closure
|
|
||||||
memory/time by roughly `(N_c/N)²` (a pass spanning 90% of commands →
|
|
||||||
~100× less closure work).
|
|
||||||
- **Contraction does not touch** the placement/alias frontiers; D8's
|
|
||||||
frontier reuse addresses `first_fit_placement`, `plan_placements`'
|
|
||||||
overlapping loop, and `build_alias_barriers` (interference frontier
|
|
||||||
caching already exists).
|
|
||||||
- **Barrier hoisting is cost-neutral to slightly positive** provided
|
|
||||||
coalescing follows D4 (per-target-layout, per-subresource handling,
|
|
||||||
unioned dst masks only for identical ranges/layouts).
|
|
||||||
- **In-pass barriers for same-layout hazards** avoid the significant
|
|
||||||
tiled-GPU regression of forced pass splits (attachment store/load
|
|
||||||
round-trips); only layout transitions and feedback loops force splits.
|
|
||||||
- **Validation is linear** with the barrier-plan-based intra-span check
|
|
||||||
(`O(B)`) and per-subresource-set duplicate detection; the existing
|
|
||||||
layout-conflict precheck grows by `O(a²)` pair iterations per pass
|
|
||||||
(28 for 8 attachments with the merged single access) — negligible.
|
|
||||||
- **Recording stays allocation-free** via precomputed attachment infos,
|
|
||||||
thread-local scratch, precomputed `execution_order`, and begin-index node
|
|
||||||
representation.
|
|
||||||
|
|
||||||
## Risks / open points
|
|
||||||
|
|
||||||
- **API shape:** raw `push(BeginRendering)` / `push(EndRendering)` vs. a
|
|
||||||
scoped `render_pass(desc, |graph| ...)` builder that pushes begin, runs the
|
|
||||||
closure, and pushes end while recording span command indices. The scoped
|
|
||||||
builder makes unbalanced spans unrepresentable; raw push is more flexible.
|
|
||||||
Decide before Task 1.
|
|
||||||
- **Created-texture views (D9):** view creation after `resolve()` adds a
|
|
||||||
cache keyed by `(Texture, ImageViewDesc)`; `ImageView` holds `Arc<Image>`
|
|
||||||
so lifetime is safe; resolve idempotence and atomic creation per D7.
|
|
||||||
- **Public API semantics:** `batches()` / `dependency_edges()` keep
|
|
||||||
command-space indices with the begin index as pass-node representative;
|
|
||||||
document the semantics and consider adding `execution_order()` as the
|
|
||||||
primary accessor.
|
|
||||||
- **`BindPipeline`:** out of scope for this plan; without it, submitted
|
|
||||||
tests cannot include real draws. If added later, it needs a sequence edge
|
|
||||||
to the following `Draw` (pipeline binding declares no resources, so the
|
|
||||||
hazard graph will not order it).
|
|
||||||
|
|
@ -1,202 +0,0 @@
|
||||||
# Plan: Correct Transient-Resource Aliasing in `renderer_rendergraph`
|
|
||||||
|
|
||||||
Status: proposed — rev 3 (code-verified; rev-2 review + follow-up comments applied)
|
|
||||||
Date: 2026-10-26
|
|
||||||
Scope: `crates/renderer_rendergraph`, `crates/renderer_resources`, `crates/renderer_device`
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Fix the broken transient-resource aliasing in the rendergraph: lifetimes must be
|
|
||||||
derived from the graph's real ordering (dependency DAG + batches, not command
|
|
||||||
indices), alias reuse must be synchronized with barriers, and the resources
|
|
||||||
crate must gain the infrastructure to compute correct memory requirements so
|
|
||||||
correctly sized pools can be allocated and bound to graph-created resources.
|
|
||||||
|
|
||||||
## Diagnosis (verified against code)
|
|
||||||
|
|
||||||
| # | Defect | Evidence |
|
|
||||||
|---|--------|----------|
|
|
||||||
| 1 | Lifetimes are command-index intervals (`first_command`/`last_command`), but commands within a `topo_batches` batch are conceptually concurrent — two resources used by different commands of the same batch are wrongly treated as non-overlapping | `graph.rs:66-79, 862-877` |
|
|
||||||
| 2 | Batch index is **not** a happens-before proof: the DAG only contains edges for conflicting same-resource accesses and is discarded after `topo_batches`; e.g. test `independent_commands_share_a_batch...` produces batches `[0,2],[1]` with no edge `2→1` | `graph.rs:643-657, 683-697, 699-714` |
|
|
||||||
| 3 | Queue transfers unmodeled: `QueueTransfer` exists but every barrier sets `None`; no command→queue assignment exists anywhere (`derive_queue_capabilities` is a resource-wide stage union, not a per-command queue) | `graph.rs:41-58, 792, 822, 1461-1541` |
|
|
||||||
| 4 | No alias-safety barriers: `build_barriers` only tracks prior uses of the *same* resource; nothing orders last use of alias A before first use of alias B sharing memory | `graph.rs:728-835` |
|
|
||||||
| 5 | Packing uses `texture_size_estimate`/`desc.size`, ignoring real `VkMemoryRequirements`, alignment, memory-type bits, `bufferImageGranularity`, and dedicated-allocation requirements; offsets are never bound to resources | `graph.rs:888-919, 1543-1588` |
|
|
||||||
| 6 | No offset-aware bind path: `bind_buffer_memory`/`bind_image_memory` bind whole VMA allocations; `Allocation::Shared(Arc<DeviceObject<GpuAllocation>>)` exists but is unused for pooling | `device.rs:209-214, 766-778` |
|
|
||||||
|
|
||||||
## Design decisions (post-review)
|
|
||||||
|
|
||||||
### D1 — Happens-before, not batch numbers
|
|
||||||
|
|
||||||
Retain the dependency adjacency from `GraphCompiler::dependencies()` in
|
|
||||||
`CompiledGraph` and add an explicit `happens_before(a, b)` reachability query.
|
|
||||||
Alias A→B is permitted only if *every* terminal use of A happens-before *every*
|
|
||||||
initial use of B. Batch intervals are computed (for reporting/limits) but never
|
|
||||||
used as the safety criterion. Same-batch uses always interfere.
|
|
||||||
|
|
||||||
`happens_before` is computed as a **transitive closure at compile time**: one
|
|
||||||
bitset per command (`Vec<FixedBitSet>`-style reachability rows, or bitset
|
|
||||||
columns if that packs better), built once in `compile()` from the Kahn
|
|
||||||
adjacency. Queries are then O(1) and deterministic. Rationale: the command
|
|
||||||
count is bounded by the recorded command list (thousands at most), closure is
|
|
||||||
O(V·E/64) once, and per-query BFS would be re-run per alias-candidate pair
|
|
||||||
during placement for no benefit. Note the closure's semantics: edges encode
|
|
||||||
only conflicting same-resource accesses, so reachability means exactly
|
|
||||||
"provably ordered via a resource-hazard chain" — precisely the aliasing-safety
|
|
||||||
criterion. Commands mutually unreachable (including same-batch pairs) interfere.
|
|
||||||
|
|
||||||
### D2 — Phase split
|
|
||||||
|
|
||||||
`compile()` stays device-free: dependency order, batch lifetimes, interference.
|
|
||||||
A new device-dependent `plan_placements(device)` (called from `resolve`) does
|
|
||||||
requirements queries, pool placement, alias-barrier construction, and binding.
|
|
||||||
Tests that call `bind_lifetimes()` directly are updated.
|
|
||||||
|
|
||||||
### D3 — Conservative queue policy
|
|
||||||
|
|
||||||
There is no command→queue assignment today and all commands record into one
|
|
||||||
command buffer. Aliasing is restricted to the single-queue case; any pair whose
|
|
||||||
accesses can't be proven ordered on one queue interferes. Cross-queue aliasing
|
|
||||||
(with ownership transfers/semaphores) is explicitly out of scope and documented
|
|
||||||
as a follow-up — no fake `QueueTransfer` support.
|
|
||||||
|
|
||||||
### D4 — Alias synchronization done correctly
|
|
||||||
|
|
||||||
- For aliased **buffers**: emit a `VkMemoryBarrier2` (new `MemoryBarrier` +
|
|
||||||
`PipelineBarrier.memory_barriers`) before the new occupant's *first-use
|
|
||||||
command* (a concrete command, not "batch start"), with `src` = old occupant's
|
|
||||||
aggregated terminal stage/access (writes; execution-only if read-only),
|
|
||||||
`dst` = new occupant's aggregated first-use scope.
|
|
||||||
- For aliased **images**: fold the old occupant's terminal scope into the new
|
|
||||||
image's first-use image barrier (`srcStageMask`/`srcAccessMask` = old scope,
|
|
||||||
`oldLayout = UNDEFINED`) — a bare memory barrier does not chain with a
|
|
||||||
following `srcStageMask=NONE` layout transition.
|
|
||||||
- Aggregate multi-scope accesses per command (OR all stage/access for the
|
|
||||||
resource at that command).
|
|
||||||
- Placement is range-based: a new resource's `[offset, offset+size)` is tested
|
|
||||||
against **all** occupied ranges; barriers include every old occupant
|
|
||||||
intersecting the range.
|
|
||||||
|
|
||||||
### D5 — Vulkan aliasing correctness details
|
|
||||||
|
|
||||||
- `bufferImageGranularity`: **separate pools per class** (linear-only and
|
|
||||||
optimal-only) is the primary strategy — first-fit stays simple and no
|
|
||||||
padding logic is needed. Intra-pool offsets are rounded only to each
|
|
||||||
resource's `alignment`. (Intra-pool interleaving with granularity-driven
|
|
||||||
offset rounding is a deliberate non-goal; revisit only if pool-class split
|
|
||||||
waste ever shows up in practice.)
|
|
||||||
- Set `VK_IMAGE_CREATE_ALIAS_BIT` on pooled images.
|
|
||||||
- Exclude from pools: `requiresDedicatedAllocation` resources, explicit
|
|
||||||
`AllocationStrategy::Dedicated` (incl. the 64 MiB heuristic), and
|
|
||||||
host-visible (`CpuToGpu`/`GpuToCpu`) resources — `Buffer::map()` returns
|
|
||||||
slices from the allocation base and would silently map wrong bytes unless
|
|
||||||
suballocation offsets are tracked.
|
|
||||||
- Created-but-unused resources: skip creation entirely (or allocate outside the
|
|
||||||
pool) — decided as "skip unused transients", with a test.
|
|
||||||
- Pooled images must have `GraphTextureDesc.layout == UNDEFINED`:
|
|
||||||
`plan_placements` rejects non-UNDEFINED declared initial layouts on pooled
|
|
||||||
images with a graph error (a created image's declared initial layout cannot
|
|
||||||
be honored by reused memory, whose contents are always undefined at first
|
|
||||||
use). Non-pooled images keep today's behavior.
|
|
||||||
|
|
||||||
### D6 — Device/resources infra (new APIs, not existing ones)
|
|
||||||
|
|
||||||
- `renderer_device`: `allocate_memory(requirements, …)` via
|
|
||||||
`vk_mem::Alloc::allocate_memory` with `CAN_ALIAS`; `find_memory_type_index`;
|
|
||||||
`bind_buffer_at_offset`/`bind_image_at_offset` via `bind_*_memory2` with
|
|
||||||
`allocation_local_offset`; require API ≥1.1 or enable the full
|
|
||||||
`get_memory_requirements2`/`bind_memory2`/`dedicated_allocation` extension
|
|
||||||
set with a clear error otherwise (same guard pattern as the existing
|
|
||||||
dedicated-allocation check in `device.rs`); define shared-allocation
|
|
||||||
ownership/cleanup: at bind time the group **transfers** the
|
|
||||||
`Arc<DeviceObject<GpuAllocation>>` into each bound resource's
|
|
||||||
`Allocation::Shared` and drops its own reference — the underlying memory is
|
|
||||||
freed when the last bound resource drops (via `DeviceObject` drop), matching
|
|
||||||
the existing `Owned`/`Shared` semantics. The group never retains a post-bind
|
|
||||||
handle.
|
|
||||||
- `renderer_resources`: symmetric `Buffer::new_unallocated` /
|
|
||||||
`Image::new_unallocated`, `memory_requirements()` (incl.
|
|
||||||
`VkMemoryDedicatedRequirements`), `bind_allocation(alloc, offset)` with
|
|
||||||
alignment/type-bits/bounds validation; a `memory` module with
|
|
||||||
`MemoryRequirements` + `MemoryGroup` (bind → `Allocation::Shared`). Pool
|
|
||||||
groups are keyed by a memory type that is in **every member's**
|
|
||||||
`memoryTypeBits` (intersection), not by any single member's individually
|
|
||||||
best type; a member whose bits exclude the group's type moves to another
|
|
||||||
group or its own allocation. New `renderer_resources::Error` variants for
|
|
||||||
failed requirement queries / bind-time validation (alignment, type-bits,
|
|
||||||
bounds); `renderer_rendergraph` maps these into new `GraphError` variants
|
|
||||||
(e.g. `PlacementFailed(ResourceId)`, `BindFailed(ResourceId)`) — bind/placement
|
|
||||||
failures surface through `resolve`/`plan_placements`'s existing
|
|
||||||
`renderer_resources::Result` boundary and map at the call site. No graph
|
|
||||||
types leak into this crate.
|
|
||||||
|
|
||||||
## Task breakdown (ordered, dependency-explicit)
|
|
||||||
|
|
||||||
1. **`renderer_device`**: memory-type query, aliasable shared allocation,
|
|
||||||
offset-aware bind, API/extension guards, error variants, ownership docs.
|
|
||||||
Tests with `TestVulkan` (`--features testing`).
|
|
||||||
2. **`renderer_resources`**: unbound create + requirements + bind APIs for
|
|
||||||
buffer *and* image; `memory` module (`MemoryRequirements`, `MemoryGroup`);
|
|
||||||
update crate AGENTS.md. Existing `Buffer::new`/`Image::new` unchanged.
|
|
||||||
3. **`renderer_rendergraph` (analysis)**: retain dependency DAG in
|
|
||||||
`CompiledGraph`; `happens_before` query; batch-mapped
|
|
||||||
`ResourceLifetime {first_batch, last_batch}`; replace `memory_offset: u64`
|
|
||||||
with a typed `Placement {offset, size, alignment, pool, aliased}`; imported
|
|
||||||
resources tracked but excluded from packing; queue-conservatism rule per D3.
|
|
||||||
4. **`renderer_rendergraph` (synchronization)**: `MemoryBarrier` +
|
|
||||||
`PipelineBarrier.memory_barriers`; recorder emits `vk::MemoryBarrier2` in
|
|
||||||
`DependencyInfo` (anchored via the existing `recording_steps`
|
|
||||||
`before_command` mechanism, like today's image/barrier steps); alias-barrier
|
|
||||||
construction per D4 (buffer and image variants, aggregation,
|
|
||||||
first-use-command anchoring); new `GraphError` variants for placement/bind
|
|
||||||
failures per D6, mapped from `renderer_resources::Error` at the
|
|
||||||
`resolve` boundary.
|
|
||||||
5. **`renderer_rendergraph` (placement)**: `plan_placements(device)` **replaces
|
|
||||||
`resolve()`'s current eager create+allocate loop** (graph.rs:1218-1276) for
|
|
||||||
pooled resources — create unbound, query requirements, group by a memory
|
|
||||||
type common to every member's `memoryTypeBits`, granularity-aware first-fit
|
|
||||||
over ranges (D4/D5, pool-per-class), allocate groups, bind, store
|
|
||||||
placements in `CompiledGraph`. Non-poolable resources (dedicated,
|
|
||||||
host-visible, imports) keep an allocate-and-bind-whole path equivalent to
|
|
||||||
today's. "Skip unused transients" lands here: created resources absent from
|
|
||||||
`lifetimes` are never created (no behavior change needed in
|
|
||||||
`record_commands`, which already skips ids absent from `lifetimes` at
|
|
||||||
graph.rs:1290). Transactional and idempotent; remove
|
|
||||||
`texture_size_estimate` from allocation decisions.
|
|
||||||
6. **Docs/tests**: update stale TODOs (`graph.rs:882`, `1315-1319`), document
|
|
||||||
the single-queue submission contract and the future multi-queue invariant
|
|
||||||
(alias transitions become semaphore edges).
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
- Device-free unit tests: same-batch interference; unordered-but-distinct-batch
|
|
||||||
interference; proven-ordered reuse succeeds; granularity boundary (buffer
|
|
||||||
next to optimal image); range-overlap first-fit; dedicated/host-visible
|
|
||||||
exclusion; unused-transient skip; pooled-image non-UNDEFINED initial layout
|
|
||||||
rejected; alias-barrier scope correctness incl. read-only old occupant and
|
|
||||||
image `UNDEFINED` transition chaining.
|
|
||||||
- GPU-gated (`tests/graph_validation.rs`): buffer-buffer and image-image alias
|
|
||||||
reuse through the real barrier path, submit, `device_wait_idle`,
|
|
||||||
`assert_no_validation_errors`; teardown of shared allocation after all
|
|
||||||
resources drop.
|
|
||||||
- Commands: `cargo check --workspace`, `cargo test -p renderer_rendergraph`,
|
|
||||||
`cargo test -p renderer_resources --features testing`,
|
|
||||||
`cargo test -p renderer_device --features testing` (from workspace root,
|
|
||||||
pinned toolchain).
|
|
||||||
|
|
||||||
## Review history
|
|
||||||
|
|
||||||
- Drafted independently by deepseek/planner and luna/planner; cross-reviewed by
|
|
||||||
deepseek/reviewer and luna/reviewer (both verdicts: CHANGES REQUIRED).
|
|
||||||
- Blockers folded into this revision: batch-index ≠ happens-before (D1),
|
|
||||||
compile/resolve phase split (D2), `bufferImageGranularity` handling (D5),
|
|
||||||
alias-barrier/layout-transition chaining (D4), offset-aware bind APIs are new
|
|
||||||
work not existing capability (D6), queue model restricted to single-queue
|
|
||||||
(D3).
|
|
||||||
- Rev 3: diagnosis re-verified against code and `vk-mem 0.5.0` sources
|
|
||||||
(all six rows and the D6 API names confirmed). Follow-up comments folded in:
|
|
||||||
`happens_before` = compile-time transitive closure (D1), pool-per-class
|
|
||||||
granularity strategy picked (D5), pooled-image UNDEFINED initial-layout
|
|
||||||
rule (D5), pool groups keyed by intersecting `memoryTypeBits` (D6),
|
|
||||||
`MemoryGroup` transfers the `Arc` at bind time instead of retaining it (D6),
|
|
||||||
placement/bind error surfacing through `GraphError` (D6/task 4),
|
|
||||||
`plan_placements` explicitly replaces `resolve`'s eager loop (task 5).
|
|
||||||
Loading…
Reference in a new issue