diff --git a/CHANGELOG.md b/CHANGELOG.md index 6466b649..2d316b4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,26 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.14.5] - 2026-07-13 + +### Added + +- `Fixture` import and export support for serializing graph resource and command relationships. +- Real-workload fixtures and benchmarks for submission scheduling. + +### Changed + +- Submission command reordering now uses dependency-safe topological scheduling to reduce + scheduling overhead while preserving per-resource command order. +- Distinct command-stream arguments can no longer bind to the same parent graph node. + +### Fixed + +- Partial `RecordSelection` scheduling now includes complete predecessor command prefixes when a + resource is rediscovered at a later command boundary. +- Repeated partial recordings no longer duplicate queue-family ownership transfers for overlapping + buffer and image subresource ranges. + ## [0.14.4] - 2026-07-03 ### Added @@ -743,7 +763,9 @@ _See [#25](https://github.com/attackgoat/screen-13/pull/25) for migration detail platforms and require no bare-metal graphics API knowledge - "Hello, world!" example using a bitmapped font -[Unreleased]: https://github.com/attackgoat/vk-graph/compare/v0.14.3...HEAD +[Unreleased]: https://github.com/attackgoat/vk-graph/compare/v0.14.5...HEAD +[0.14.5]: https://crates.io/crates/vk-graph/0.14.5 +[0.14.4]: https://crates.io/crates/vk-graph/0.14.4 [0.14.3]: https://crates.io/crates/vk-graph/0.14.3 [0.14.2]: https://crates.io/crates/vk-graph/0.14.2 [0.1.0]: https://crates.io/crates/screen-13/0.1.0 diff --git a/Cargo.toml b/Cargo.toml index 8c90621e..4a1a61eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ readme = "README.md" log = "0.4" profiling = "1.0" read-only = "0.1" -vk-graph = { path = "", version = "0.14.4" } +vk-graph = { path = "", version = "0.14.5" } vk-graph-egui = { path = "crates/vk-graph-egui", version = "0.1.2" } vk-graph-fx = { path = "crates/vk-graph-fx", version = "0.1.1" } vk-graph-hot = { path = "crates/vk-graph-hot", version = "0.1.1" } @@ -34,7 +34,7 @@ winit = "0.30" [package] name = "vk-graph" -version = "0.14.4" +version = "0.14.5" authors = ["John Wells "] edition.workspace = true license.workspace = true diff --git a/benches/submission_reorder.rs b/benches/submission_reorder.rs index 2b60993b..a7efc2a9 100644 --- a/benches/submission_reorder.rs +++ b/benches/submission_reorder.rs @@ -1,6 +1,10 @@ use { criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main}, - vk_graph::submission::bench::{ReorderBenchHarness, ReorderBenchSpec}, + std::path::Path, + vk_graph::{ + Graph, + submission::bench::{ReorderBenchHarness, ReorderBenchSpec}, + }, }; fn submission_reorder_bench(c: &mut Criterion) { @@ -62,6 +66,29 @@ fn submission_reorder_bench(c: &mut Criterion) { ); } + for (fixture_name, file_name) in [ + ("real_game_49", "graph-1783212230368.bin"), + ("real_game_114", "graph-1783212245365.bin"), + ] { + let graph = Graph::import_fixture( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("res/graph-fixture") + .join(file_name), + ) + .unwrap_or_else(|err| panic!("unable to import {file_name}: {err}")); + + for repeat_count in [1, 8, 32] { + let mut harness = ReorderBenchHarness::from_graph(&graph, repeat_count); + let cmd_count = harness.cmd_count(); + group.throughput(Throughput::Elements(cmd_count as u64)); + group.bench_with_input( + BenchmarkId::new(fixture_name, format!("{cmd_count}c_{repeat_count}x")), + &repeat_count, + |b, _| b.iter(|| black_box(harness.reorder_once())), + ); + } + } + group.finish(); } diff --git a/examples/shader-toy/build.rs b/examples/shader-toy/build.rs index 5fe3b051..4afbc4e3 100644 --- a/examples/shader-toy/build.rs +++ b/examples/shader-toy/build.rs @@ -184,7 +184,7 @@ fn read_shader_source(path: impl AsRef) -> String { &mut self, path: &ResolvedIncludePath, ) -> Result { - println!("cargo:rerun-if-changed={}", &path.0); + println!("cargo:rerun-if-changed={}", path.0); Ok(read_to_string(&path.0)?) } diff --git a/res/graph-fixture/graph-1783212230368.bin b/res/graph-fixture/graph-1783212230368.bin new file mode 100644 index 00000000..1494504e Binary files /dev/null and b/res/graph-fixture/graph-1783212230368.bin differ diff --git a/res/graph-fixture/graph-1783212230368.md b/res/graph-fixture/graph-1783212230368.md new file mode 100644 index 00000000..35c7c23a --- /dev/null +++ b/res/graph-fixture/graph-1783212230368.md @@ -0,0 +1,292 @@ + + +# Graph Relationship Overview + +This view keeps the binary fixture complete, but collapses the Mermaid diagram to command dependencies. Read/read-only relationships are omitted, and high-degree hub resources are summarized below instead of drawn. + +- Resources: 125 +- Commands: 49 +- Accesses: 262 +- Compact dependencies: 207 +- Displayed dependencies: 207 +- Omitted dependencies: 0 +- Omitted hub resources: 0 + +## Command Dependencies + +Edge labels are `resources / accesses / dependency kinds`, where RAW is read-after-write, WAR is write-after-read, and WAW is write-after-write. + +```mermaid +flowchart TD + C0["cmd 0: init pose"] + C1["cmd 1: update animation 1"] + C2["cmd 2: update geometry"] + C3["cmd 3: copy buffer"] + C4["cmd 4: BLAS"] + C5["cmd 5: TLAS"] + C6["cmd 6: generate path ReSTIR reservoir"] + C7["cmd 7: spatial reuse path ReSTIR reservoir"] + C8["cmd 8: spatial reuse path ReSTIR reservoir"] + C9["cmd 9: shade path ReSTIR reservoir"] + C10["cmd 10: copy compute depth buffer to image"] + C11["cmd 11: SSAO"] + C12["cmd 12: box blur 2d x"] + C13["cmd 13: box blur 2d y"] + C14["cmd 14: SSAO apply"] + C15["cmd 15: command"] + C16["cmd 16: accumulate path traced color"] + C17["cmd 17: denoise path traced color"] + C18["cmd 18: denoise path traced color"] + C19["cmd 19: denoise path traced color"] + C20["cmd 20: command"] + C21["cmd 21: accumulate path traced direct"] + C22["cmd 22: denoise path traced direct"] + C23["cmd 23: denoise path traced direct"] + C24["cmd 24: accumulate path traced indirect diffuse"] + C25["cmd 25: denoise path traced indirect diffuse"] + C26["cmd 26: denoise path traced indirect diffuse"] + C27["cmd 27: denoise path traced indirect diffuse"] + C28["cmd 28: accumulate path traced indirect specular"] + C29["cmd 29: denoise path traced indirect specular"] + C30["cmd 30: denoise path traced indirect specular"] + C31["cmd 31: composite path traced signals"] + C32["cmd 32: command"] + C33["cmd 33: update debug buffer"] + C34["cmd 34: draw box (debug)"] + C35["cmd 35: update debug buffer"] + C36["cmd 36: Draw capsule cap top (debug)"] + C37["cmd 37: Draw capsule body (debug)"] + C38["cmd 38: Draw capsule cap bottom (debug)"] + C39["cmd 39: update debug buffer"] + C40["cmd 40: draw cone (debug)"] + C41["cmd 41: update debug buffer"] + C42["cmd 42: draw cylinder (debug)"] + C43["cmd 43: update debug buffer"] + C44["cmd 44: draw line 3D (debug)"] + C45["cmd 45: update debug buffer"] + C46["cmd 46: draw sphere (debug)"] + C47["cmd 47: tone map"] + C48["cmd 48: present copy"] + C0 -- "1 res / 2 acc / WAW" --> C1 + C1 -- "1 res / 2 acc / RAW" --> C2 + C2 -- "1 res / 2 acc / RAW" --> C4 + C2 -- "1 res / 3 acc / RAW" --> C6 + C3 -- "1 res / 2 acc / RAW" --> C6 + C4 -- "2 res / 4 acc / RAW" --> C5 + C5 -- "1 res / 2 acc / RAW" --> C6 + C6 -- "3 res / 6 acc / RAW" --> C7 + C6 -- "3 res / 6 acc / RAW+WAW" --> C8 + C6 -- "4 res / 8 acc / RAW" --> C9 + C7 -- "2 res / 4 acc / RAW+WAR" --> C8 + C8 -- "1 res / 2 acc / RAW" --> C9 + C9 -- "1 res / 2 acc / RAW" --> C10 + C9 -- "1 res / 2 acc / RAW" --> C11 + C9 -- "1 res / 2 acc / RAW" --> C14 + C9 -- "1 res / 2 acc / WAW" --> C15 + C9 -- "3 res / 6 acc / RAW" --> C16 + C9 -- "1 res / 2 acc / WAW" --> C20 + C9 -- "3 res / 6 acc / RAW" --> C21 + C9 -- "3 res / 6 acc / RAW" --> C24 + C9 -- "3 res / 6 acc / RAW" --> C28 + C9 -- "1 res / 2 acc / WAW" --> C32 + C9 -- "1 res / 2 acc / RAW+WAW" --> C34 + C9 -- "1 res / 2 acc / RAW+WAW" --> C36 + C9 -- "1 res / 2 acc / RAW+WAW" --> C37 + C9 -- "1 res / 2 acc / RAW+WAW" --> C38 + C9 -- "1 res / 2 acc / RAW+WAW" --> C40 + C9 -- "1 res / 2 acc / RAW+WAW" --> C42 + C9 -- "1 res / 2 acc / RAW+WAW" --> C44 + C9 -- "1 res / 2 acc / RAW+WAW" --> C46 + C9 -- "1 res / 2 acc / RAW" --> C47 + C10 -- "1 res / 2 acc / RAW" --> C11 + C10 -- "1 res / 2 acc / RAW" --> C16 + C10 -- "1 res / 2 acc / RAW" --> C21 + C10 -- "1 res / 2 acc / RAW" --> C24 + C10 -- "1 res / 2 acc / RAW" --> C28 + C10 -- "1 res / 2 acc / RAW+WAW" --> C34 + C10 -- "1 res / 2 acc / RAW+WAW" --> C36 + C10 -- "1 res / 2 acc / RAW+WAW" --> C37 + C10 -- "1 res / 2 acc / RAW+WAW" --> C38 + C10 -- "1 res / 2 acc / RAW+WAW" --> C40 + C10 -- "1 res / 2 acc / RAW+WAW" --> C42 + C10 -- "1 res / 2 acc / RAW+WAW" --> C44 + C10 -- "1 res / 2 acc / RAW+WAW" --> C46 + C11 -- "1 res / 2 acc / RAW" --> C12 + C11 -- "1 res / 2 acc / WAR" --> C34 + C11 -- "1 res / 2 acc / WAR" --> C36 + C11 -- "1 res / 2 acc / WAR" --> C37 + C11 -- "1 res / 2 acc / WAR" --> C38 + C11 -- "1 res / 2 acc / WAR" --> C40 + C11 -- "1 res / 2 acc / WAR" --> C42 + C11 -- "1 res / 2 acc / WAR" --> C44 + C11 -- "1 res / 2 acc / WAR" --> C46 + C12 -- "1 res / 2 acc / RAW" --> C13 + C13 -- "1 res / 2 acc / RAW" --> C14 + C14 -- "2 res / 4 acc / RAW+WAR" --> C15 + C14 -- "1 res / 2 acc / WAR" --> C20 + C14 -- "1 res / 2 acc / WAR" --> C32 + C14 -- "1 res / 2 acc / WAR" --> C34 + C14 -- "1 res / 2 acc / WAR" --> C36 + C14 -- "1 res / 2 acc / WAR" --> C37 + C14 -- "1 res / 2 acc / WAR" --> C38 + C14 -- "1 res / 2 acc / WAR" --> C40 + C14 -- "1 res / 2 acc / WAR" --> C42 + C14 -- "1 res / 2 acc / WAR" --> C44 + C14 -- "1 res / 2 acc / WAR" --> C46 + C15 -- "1 res / 2 acc / RAW" --> C16 + C15 -- "1 res / 2 acc / WAW" --> C20 + C15 -- "1 res / 2 acc / WAW" --> C32 + C15 -- "1 res / 2 acc / RAW+WAW" --> C34 + C15 -- "1 res / 2 acc / RAW+WAW" --> C36 + C15 -- "1 res / 2 acc / RAW+WAW" --> C37 + C15 -- "1 res / 2 acc / RAW+WAW" --> C38 + C15 -- "1 res / 2 acc / RAW+WAW" --> C40 + C15 -- "1 res / 2 acc / RAW+WAW" --> C42 + C15 -- "1 res / 2 acc / RAW+WAW" --> C44 + C15 -- "1 res / 2 acc / RAW+WAW" --> C46 + C15 -- "1 res / 2 acc / RAW" --> C47 + C16 -- "4 res / 8 acc / RAW" --> C17 + C16 -- "3 res / 6 acc / RAW" --> C18 + C16 -- "3 res / 6 acc / RAW" --> C19 + C16 -- "1 res / 2 acc / WAR" --> C20 + C16 -- "1 res / 2 acc / WAW" --> C21 + C16 -- "1 res / 2 acc / WAW" --> C24 + C16 -- "1 res / 2 acc / WAW" --> C28 + C16 -- "1 res / 2 acc / WAR" --> C32 + C16 -- "2 res / 4 acc / WAR" --> C34 + C16 -- "2 res / 4 acc / WAR" --> C36 + C16 -- "2 res / 4 acc / WAR" --> C37 + C16 -- "2 res / 4 acc / WAR" --> C38 + C16 -- "2 res / 4 acc / WAR" --> C40 + C16 -- "2 res / 4 acc / WAR" --> C42 + C16 -- "2 res / 4 acc / WAR" --> C44 + C16 -- "2 res / 4 acc / WAR" --> C46 + C17 -- "1 res / 2 acc / RAW" --> C18 + C17 -- "1 res / 2 acc / WAW" --> C19 + C17 -- "1 res / 2 acc / RAW" --> C20 + C18 -- "2 res / 4 acc / RAW+WAR" --> C19 + C19 -- "1 res / 2 acc / RAW" --> C20 + C20 -- "1 res / 2 acc / WAW" --> C32 + C20 -- "1 res / 2 acc / RAW+WAW" --> C34 + C20 -- "1 res / 2 acc / RAW+WAW" --> C36 + C20 -- "1 res / 2 acc / RAW+WAW" --> C37 + C20 -- "1 res / 2 acc / RAW+WAW" --> C38 + C20 -- "1 res / 2 acc / RAW+WAW" --> C40 + C20 -- "1 res / 2 acc / RAW+WAW" --> C42 + C20 -- "1 res / 2 acc / RAW+WAW" --> C44 + C20 -- "1 res / 2 acc / RAW+WAW" --> C46 + C20 -- "1 res / 2 acc / RAW" --> C47 + C21 -- "4 res / 8 acc / RAW" --> C22 + C21 -- "3 res / 6 acc / RAW" --> C23 + C21 -- "1 res / 2 acc / WAW" --> C24 + C21 -- "1 res / 2 acc / WAW" --> C28 + C21 -- "1 res / 2 acc / WAR" --> C34 + C21 -- "1 res / 2 acc / WAR" --> C36 + C21 -- "1 res / 2 acc / WAR" --> C37 + C21 -- "1 res / 2 acc / WAR" --> C38 + C21 -- "1 res / 2 acc / WAR" --> C40 + C21 -- "1 res / 2 acc / WAR" --> C42 + C21 -- "1 res / 2 acc / WAR" --> C44 + C21 -- "1 res / 2 acc / WAR" --> C46 + C22 -- "1 res / 2 acc / RAW" --> C23 + C23 -- "1 res / 2 acc / RAW" --> C31 + C24 -- "4 res / 8 acc / RAW" --> C25 + C24 -- "3 res / 6 acc / RAW" --> C26 + C24 -- "3 res / 6 acc / RAW" --> C27 + C24 -- "1 res / 2 acc / WAW" --> C28 + C24 -- "1 res / 2 acc / WAR" --> C34 + C24 -- "1 res / 2 acc / WAR" --> C36 + C24 -- "1 res / 2 acc / WAR" --> C37 + C24 -- "1 res / 2 acc / WAR" --> C38 + C24 -- "1 res / 2 acc / WAR" --> C40 + C24 -- "1 res / 2 acc / WAR" --> C42 + C24 -- "1 res / 2 acc / WAR" --> C44 + C24 -- "1 res / 2 acc / WAR" --> C46 + C25 -- "1 res / 2 acc / RAW" --> C26 + C25 -- "1 res / 2 acc / WAW" --> C27 + C25 -- "1 res / 2 acc / RAW" --> C31 + C26 -- "2 res / 4 acc / RAW+WAR" --> C27 + C27 -- "1 res / 2 acc / RAW" --> C31 + C28 -- "4 res / 8 acc / RAW" --> C29 + C28 -- "3 res / 6 acc / RAW" --> C30 + C28 -- "1 res / 2 acc / WAR" --> C34 + C28 -- "1 res / 2 acc / WAR" --> C36 + C28 -- "1 res / 2 acc / WAR" --> C37 + C28 -- "1 res / 2 acc / WAR" --> C38 + C28 -- "1 res / 2 acc / WAR" --> C40 + C28 -- "1 res / 2 acc / WAR" --> C42 + C28 -- "1 res / 2 acc / WAR" --> C44 + C28 -- "1 res / 2 acc / WAR" --> C46 + C29 -- "1 res / 2 acc / RAW" --> C30 + C30 -- "1 res / 2 acc / RAW" --> C31 + C31 -- "1 res / 2 acc / RAW" --> C32 + C32 -- "1 res / 2 acc / RAW+WAW" --> C34 + C32 -- "1 res / 2 acc / RAW+WAW" --> C36 + C32 -- "1 res / 2 acc / RAW+WAW" --> C37 + C32 -- "1 res / 2 acc / RAW+WAW" --> C38 + C32 -- "1 res / 2 acc / RAW+WAW" --> C40 + C32 -- "1 res / 2 acc / RAW+WAW" --> C42 + C32 -- "1 res / 2 acc / RAW+WAW" --> C44 + C32 -- "1 res / 2 acc / RAW+WAW" --> C46 + C32 -- "1 res / 2 acc / RAW" --> C47 + C33 -- "1 res / 2 acc / RAW" --> C34 + C34 -- "2 res / 4 acc / RAW+WAR+WAW" --> C36 + C34 -- "2 res / 4 acc / RAW+WAR+WAW" --> C37 + C34 -- "2 res / 4 acc / RAW+WAR+WAW" --> C38 + C34 -- "2 res / 4 acc / RAW+WAR+WAW" --> C40 + C34 -- "2 res / 4 acc / RAW+WAR+WAW" --> C42 + C34 -- "2 res / 4 acc / RAW+WAR+WAW" --> C44 + C34 -- "2 res / 4 acc / RAW+WAR+WAW" --> C46 + C34 -- "1 res / 2 acc / RAW" --> C47 + C35 -- "1 res / 2 acc / RAW" --> C36 + C35 -- "1 res / 2 acc / RAW" --> C37 + C35 -- "1 res / 2 acc / RAW" --> C38 + C36 -- "2 res / 4 acc / RAW+WAR+WAW" --> C37 + C36 -- "2 res / 4 acc / RAW+WAR+WAW" --> C38 + C36 -- "2 res / 4 acc / RAW+WAR+WAW" --> C40 + C36 -- "2 res / 4 acc / RAW+WAR+WAW" --> C42 + C36 -- "2 res / 4 acc / RAW+WAR+WAW" --> C44 + C36 -- "2 res / 4 acc / RAW+WAR+WAW" --> C46 + C36 -- "1 res / 2 acc / RAW" --> C47 + C37 -- "2 res / 4 acc / RAW+WAR+WAW" --> C38 + C37 -- "2 res / 4 acc / RAW+WAR+WAW" --> C40 + C37 -- "2 res / 4 acc / RAW+WAR+WAW" --> C42 + C37 -- "2 res / 4 acc / RAW+WAR+WAW" --> C44 + C37 -- "2 res / 4 acc / RAW+WAR+WAW" --> C46 + C37 -- "1 res / 2 acc / RAW" --> C47 + C38 -- "2 res / 4 acc / RAW+WAR+WAW" --> C40 + C38 -- "2 res / 4 acc / RAW+WAR+WAW" --> C42 + C38 -- "2 res / 4 acc / RAW+WAR+WAW" --> C44 + C38 -- "2 res / 4 acc / RAW+WAR+WAW" --> C46 + C38 -- "1 res / 2 acc / RAW" --> C47 + C39 -- "1 res / 2 acc / RAW" --> C40 + C40 -- "2 res / 4 acc / RAW+WAR+WAW" --> C42 + C40 -- "2 res / 4 acc / RAW+WAR+WAW" --> C44 + C40 -- "2 res / 4 acc / RAW+WAR+WAW" --> C46 + C40 -- "1 res / 2 acc / RAW" --> C47 + C41 -- "1 res / 2 acc / RAW" --> C42 + C42 -- "2 res / 4 acc / RAW+WAR+WAW" --> C44 + C42 -- "2 res / 4 acc / RAW+WAR+WAW" --> C46 + C42 -- "1 res / 2 acc / RAW" --> C47 + C43 -- "1 res / 2 acc / RAW" --> C44 + C44 -- "2 res / 4 acc / RAW+WAR+WAW" --> C46 + C44 -- "1 res / 2 acc / RAW" --> C47 + C45 -- "1 res / 2 acc / RAW" --> C46 + C46 -- "1 res / 2 acc / RAW" --> C47 + C47 -- "1 res / 2 acc / RAW" --> C48 +``` diff --git a/res/graph-fixture/graph-1783212245365.bin b/res/graph-fixture/graph-1783212245365.bin new file mode 100644 index 00000000..689d9a1d Binary files /dev/null and b/res/graph-fixture/graph-1783212245365.bin differ diff --git a/res/graph-fixture/graph-1783212245365.md b/res/graph-fixture/graph-1783212245365.md new file mode 100644 index 00000000..37cf9163 --- /dev/null +++ b/res/graph-fixture/graph-1783212245365.md @@ -0,0 +1,352 @@ + + +# Graph Relationship Overview + +This view keeps the binary fixture complete, but collapses the Mermaid diagram to command dependencies. Read/read-only relationships are omitted, and high-degree hub resources are summarized below instead of drawn. + +- Resources: 114 +- Commands: 114 +- Accesses: 401 +- Compact dependencies: 548 +- Displayed dependencies: 250 +- Omitted dependencies: 298 +- Omitted hub resources: 0 + +## Command Dependencies + +Edge labels are `resources / accesses / dependency kinds`, where RAW is read-after-write, WAR is write-after-read, and WAW is write-after-write. + +```mermaid +flowchart TD + C0["cmd 0: init pose"] + C1["cmd 1: update animation 1"] + C2["cmd 2: update geometry"] + C3["cmd 3: copy buffer"] + C4["cmd 4: bounding box triangle split"] + C5["cmd 5: bounding box triangle split"] + C6["cmd 6: bounding box min/max"] + C7["cmd 7: bounding box reduce"] + C8["cmd 8: bounding box reduce"] + C9["cmd 9: copy buffer"] + C10["cmd 10: bvh morton code"] + C11["cmd 11: radix histogram 1/4"] + C12["cmd 12: radix sort 1/4"] + C13["cmd 13: radix histogram 2/4"] + C14["cmd 14: radix sort 2/4"] + C15["cmd 15: radix histogram 3/4"] + C16["cmd 16: radix sort 3/4"] + C17["cmd 17: radix histogram 4/4"] + C18["cmd 18: radix sort 4/4"] + C19["cmd 19: bvh hierarchy"] + C20["cmd 20: bvh bounding box"] + C21["cmd 21: bounding box triangle split"] + C22["cmd 22: bounding box triangle split"] + C23["cmd 23: bounding box min/max"] + C24["cmd 24: bounding box reduce"] + C25["cmd 25: bounding box reduce"] + C26["cmd 26: copy buffer"] + C27["cmd 27: bvh morton code"] + C28["cmd 28: radix histogram 1/4"] + C29["cmd 29: radix sort 1/4"] + C30["cmd 30: radix histogram 2/4"] + C31["cmd 31: radix sort 2/4"] + C32["cmd 32: radix histogram 3/4"] + C33["cmd 33: radix sort 3/4"] + C34["cmd 34: radix histogram 4/4"] + C35["cmd 35: radix sort 4/4"] + C36["cmd 36: bvh hierarchy"] + C37["cmd 37: bvh bounding box"] + C38["cmd 38: copy scene bvh mesh bounding boxes"] + C39["cmd 39: copy scene bvh mesh bounding boxes"] + C40["cmd 40: copy scene bvh mesh bounding boxes"] + C41["cmd 41: copy scene bvh mesh bounding boxes"] + C42["cmd 42: copy scene bvh mesh bounding boxes"] + C43["cmd 43: copy scene bvh mesh bounding boxes"] + C44["cmd 44: copy scene bvh mesh bounding boxes"] + C45["cmd 45: copy scene bvh mesh bounding boxes"] + C46["cmd 46: copy scene bvh mesh bounding boxes"] + C47["cmd 47: copy scene bvh mesh bounding boxes"] + C48["cmd 48: copy scene bvh nodes"] + C49["cmd 49: copy scene bvh nodes"] + C50["cmd 50: copy scene bvh nodes"] + C51["cmd 51: copy scene bvh nodes"] + C52["cmd 52: copy scene bvh nodes"] + C53["cmd 53: copy scene bvh nodes"] + C54["cmd 54: copy scene bvh nodes"] + C55["cmd 55: copy scene bvh nodes"] + C56["cmd 56: copy scene bvh nodes"] + C57["cmd 57: copy scene bvh nodes"] + C58["cmd 58: scene bvh instance bounding boxes"] + C73["cmd 73: trace scene wavefront primary rays"] + C75["cmd 75: run scene wavefront primary logic"] + C77["cmd 77: evaluate scene wavefront primary materials"] + C80["cmd 80: trace scene wavefront direct shadows"] + C82["cmd 82: run scene wavefront direct-shadow logic"] + C86["cmd 86: trace scene wavefront indirect rays"] + C88["cmd 88: run scene wavefront secondary logic"] + C0 -- "1 res / 2 acc / WAW" --> C1 + C1 -- "1 res / 2 acc / RAW" --> C2 + C2 -- "1 res / 2 acc / RAW" --> C4 + C2 -- "1 res / 2 acc / RAW" --> C5 + C2 -- "1 res / 2 acc / RAW" --> C6 + C2 -- "1 res / 2 acc / RAW" --> C21 + C2 -- "1 res / 2 acc / RAW" --> C22 + C2 -- "1 res / 2 acc / RAW" --> C23 + C2 -- "1 res / 2 acc / RAW" --> C73 + C2 -- "1 res / 2 acc / RAW" --> C80 + C2 -- "1 res / 2 acc / RAW" --> C86 + C3 -- "1 res / 2 acc / RAW" --> C58 + C3 -- "1 res / 2 acc / RAW" --> C73 + C3 -- "1 res / 2 acc / RAW" --> C75 + C3 -- "1 res / 2 acc / RAW" --> C77 + C3 -- "1 res / 2 acc / RAW" --> C80 + C3 -- "1 res / 2 acc / RAW" --> C82 + C3 -- "1 res / 2 acc / RAW" --> C86 + C3 -- "1 res / 2 acc / RAW" --> C88 + C4 -- "1 res / 2 acc / WAW" --> C5 + C4 -- "1 res / 2 acc / RAW" --> C10 + C4 -- "1 res / 2 acc / RAW" --> C19 + C5 -- "1 res / 2 acc / RAW" --> C10 + C5 -- "1 res / 2 acc / RAW" --> C19 + C6 -- "1 res / 2 acc / RAW" --> C7 + C6 -- "1 res / 2 acc / WAW" --> C8 + C6 -- "1 res / 2 acc / RAW" --> C9 + C7 -- "2 res / 4 acc / RAW+WAR" --> C8 + C8 -- "1 res / 2 acc / RAW" --> C9 + C9 -- "1 res / 2 acc / RAW" --> C10 + C9 -- "1 res / 2 acc / RAW" --> C46 + C10 -- "1 res / 2 acc / RAW" --> C11 + C10 -- "1 res / 2 acc / RAW" --> C12 + C10 -- "1 res / 2 acc / WAW" --> C14 + C10 -- "1 res / 2 acc / RAW" --> C15 + C10 -- "1 res / 2 acc / RAW" --> C16 + C10 -- "1 res / 2 acc / WAW" --> C18 + C10 -- "1 res / 2 acc / RAW" --> C19 + C11 -- "1 res / 2 acc / RAW" --> C12 + C11 -- "1 res / 2 acc / WAW" --> C13 + C11 -- "2 res / 4 acc / RAW+WAR" --> C14 + C11 -- "1 res / 2 acc / WAW" --> C15 + C11 -- "1 res / 2 acc / RAW" --> C16 + C11 -- "1 res / 2 acc / WAW" --> C17 + C11 -- "2 res / 4 acc / RAW+WAR" --> C18 + C12 -- "2 res / 4 acc / RAW+WAR" --> C13 + C12 -- "2 res / 4 acc / RAW+WAR" --> C14 + C12 -- "1 res / 2 acc / WAR" --> C15 + C12 -- "1 res / 2 acc / WAW" --> C16 + C12 -- "2 res / 4 acc / RAW+WAR" --> C17 + C12 -- "2 res / 4 acc / RAW+WAR" --> C18 + C13 -- "1 res / 2 acc / RAW" --> C14 + C13 -- "1 res / 2 acc / WAW" --> C15 + C13 -- "2 res / 4 acc / RAW+WAR" --> C16 + C13 -- "1 res / 2 acc / WAW" --> C17 + C13 -- "1 res / 2 acc / RAW" --> C18 + C14 -- "2 res / 4 acc / RAW+WAR" --> C15 + C14 -- "2 res / 4 acc / RAW+WAR" --> C16 + C14 -- "1 res / 2 acc / WAR" --> C17 + C14 -- "1 res / 2 acc / WAW" --> C18 + C14 -- "1 res / 2 acc / RAW" --> C19 + C15 -- "1 res / 2 acc / RAW" --> C16 + C15 -- "1 res / 2 acc / WAW" --> C17 + C15 -- "2 res / 4 acc / RAW+WAR" --> C18 + C16 -- "2 res / 4 acc / RAW+WAR" --> C17 + C16 -- "2 res / 4 acc / RAW+WAR" --> C18 + C17 -- "1 res / 2 acc / RAW" --> C18 + C18 -- "1 res / 2 acc / RAW" --> C19 + C19 -- "2 res / 4 acc / RAW+WAW" --> C20 + C19 -- "1 res / 2 acc / RAW" --> C56 + C20 -- "1 res / 2 acc / RAW" --> C56 + C21 -- "1 res / 2 acc / WAW" --> C22 + C21 -- "1 res / 2 acc / RAW" --> C27 + C21 -- "1 res / 2 acc / RAW" --> C36 + C22 -- "1 res / 2 acc / RAW" --> C27 + C22 -- "1 res / 2 acc / RAW" --> C36 + C23 -- "1 res / 2 acc / RAW" --> C24 + C23 -- "1 res / 2 acc / WAW" --> C25 + C23 -- "1 res / 2 acc / RAW" --> C26 + C24 -- "2 res / 4 acc / RAW+WAR" --> C25 + C25 -- "1 res / 2 acc / RAW" --> C26 + C26 -- "1 res / 2 acc / RAW" --> C27 + C26 -- "1 res / 2 acc / RAW" --> C47 + C27 -- "1 res / 2 acc / RAW" --> C28 + C27 -- "1 res / 2 acc / RAW" --> C29 + C27 -- "1 res / 2 acc / WAW" --> C31 + C27 -- "1 res / 2 acc / RAW" --> C32 + C27 -- "1 res / 2 acc / RAW" --> C33 + C27 -- "1 res / 2 acc / WAW" --> C35 + C27 -- "1 res / 2 acc / RAW" --> C36 + C28 -- "1 res / 2 acc / RAW" --> C29 + C28 -- "1 res / 2 acc / WAW" --> C30 + C28 -- "2 res / 4 acc / RAW+WAR" --> C31 + C28 -- "1 res / 2 acc / WAW" --> C32 + C28 -- "1 res / 2 acc / RAW" --> C33 + C28 -- "1 res / 2 acc / WAW" --> C34 + C28 -- "2 res / 4 acc / RAW+WAR" --> C35 + C29 -- "2 res / 4 acc / RAW+WAR" --> C30 + C29 -- "2 res / 4 acc / RAW+WAR" --> C31 + C29 -- "1 res / 2 acc / WAR" --> C32 + C29 -- "1 res / 2 acc / WAW" --> C33 + C29 -- "2 res / 4 acc / RAW+WAR" --> C34 + C29 -- "2 res / 4 acc / RAW+WAR" --> C35 + C30 -- "1 res / 2 acc / RAW" --> C31 + C30 -- "1 res / 2 acc / WAW" --> C32 + C30 -- "2 res / 4 acc / RAW+WAR" --> C33 + C30 -- "1 res / 2 acc / WAW" --> C34 + C30 -- "1 res / 2 acc / RAW" --> C35 + C31 -- "2 res / 4 acc / RAW+WAR" --> C32 + C31 -- "2 res / 4 acc / RAW+WAR" --> C33 + C31 -- "1 res / 2 acc / WAR" --> C34 + C31 -- "1 res / 2 acc / WAW" --> C35 + C31 -- "1 res / 2 acc / RAW" --> C36 + C32 -- "1 res / 2 acc / RAW" --> C33 + C32 -- "1 res / 2 acc / WAW" --> C34 + C32 -- "2 res / 4 acc / RAW+WAR" --> C35 + C33 -- "2 res / 4 acc / RAW+WAR" --> C34 + C33 -- "2 res / 4 acc / RAW+WAR" --> C35 + C34 -- "1 res / 2 acc / RAW" --> C35 + C35 -- "1 res / 2 acc / RAW" --> C36 + C36 -- "2 res / 4 acc / RAW+WAW" --> C37 + C36 -- "1 res / 2 acc / RAW" --> C57 + C37 -- "1 res / 2 acc / RAW" --> C57 + C38 -- "1 res / 2 acc / WAW" --> C39 + C38 -- "1 res / 2 acc / WAW" --> C40 + C38 -- "1 res / 2 acc / WAW" --> C41 + C38 -- "1 res / 2 acc / WAW" --> C42 + C38 -- "1 res / 2 acc / WAW" --> C43 + C38 -- "1 res / 2 acc / WAW" --> C44 + C38 -- "1 res / 2 acc / WAW" --> C45 + C38 -- "1 res / 2 acc / WAW" --> C46 + C38 -- "1 res / 2 acc / WAW" --> C47 + C39 -- "1 res / 2 acc / WAW" --> C40 + C39 -- "1 res / 2 acc / WAW" --> C41 + C39 -- "1 res / 2 acc / WAW" --> C42 + C39 -- "1 res / 2 acc / WAW" --> C43 + C39 -- "1 res / 2 acc / WAW" --> C44 + C39 -- "1 res / 2 acc / WAW" --> C45 + C39 -- "1 res / 2 acc / WAW" --> C46 + C39 -- "1 res / 2 acc / WAW" --> C47 + C40 -- "1 res / 2 acc / WAW" --> C41 + C40 -- "1 res / 2 acc / WAW" --> C42 + C40 -- "1 res / 2 acc / WAW" --> C43 + C40 -- "1 res / 2 acc / WAW" --> C44 + C40 -- "1 res / 2 acc / WAW" --> C45 + C40 -- "1 res / 2 acc / WAW" --> C46 + C40 -- "1 res / 2 acc / WAW" --> C47 + C41 -- "1 res / 2 acc / WAW" --> C42 + C41 -- "1 res / 2 acc / WAW" --> C43 + C41 -- "1 res / 2 acc / WAW" --> C44 + C41 -- "1 res / 2 acc / WAW" --> C45 + C41 -- "1 res / 2 acc / WAW" --> C46 + C41 -- "1 res / 2 acc / WAW" --> C47 + C42 -- "1 res / 2 acc / WAW" --> C43 + C42 -- "1 res / 2 acc / WAW" --> C44 + C42 -- "1 res / 2 acc / WAW" --> C45 + C42 -- "1 res / 2 acc / WAW" --> C46 + C42 -- "1 res / 2 acc / WAW" --> C47 + C43 -- "1 res / 2 acc / WAW" --> C44 + C43 -- "1 res / 2 acc / WAW" --> C45 + C43 -- "1 res / 2 acc / WAW" --> C46 + C43 -- "1 res / 2 acc / WAW" --> C47 + C44 -- "1 res / 2 acc / WAW" --> C45 + C44 -- "1 res / 2 acc / WAW" --> C46 + C44 -- "1 res / 2 acc / WAW" --> C47 + C45 -- "1 res / 2 acc / WAW" --> C46 + C45 -- "1 res / 2 acc / WAW" --> C47 + C46 -- "1 res / 2 acc / WAW" --> C47 + C48 -- "1 res / 2 acc / WAW" --> C49 + C48 -- "1 res / 2 acc / WAW" --> C50 + C48 -- "1 res / 2 acc / WAW" --> C51 + C48 -- "1 res / 2 acc / WAW" --> C52 + C48 -- "1 res / 2 acc / WAW" --> C53 + C48 -- "1 res / 2 acc / WAW" --> C54 + C48 -- "1 res / 2 acc / WAW" --> C55 + C48 -- "1 res / 2 acc / WAW" --> C56 + C48 -- "1 res / 2 acc / WAW" --> C57 + C48 -- "1 res / 2 acc / RAW" --> C58 + C48 -- "1 res / 2 acc / RAW" --> C73 + C48 -- "1 res / 2 acc / RAW" --> C80 + C48 -- "1 res / 2 acc / RAW" --> C86 + C49 -- "1 res / 2 acc / WAW" --> C50 + C49 -- "1 res / 2 acc / WAW" --> C51 + C49 -- "1 res / 2 acc / WAW" --> C52 + C49 -- "1 res / 2 acc / WAW" --> C53 + C49 -- "1 res / 2 acc / WAW" --> C54 + C49 -- "1 res / 2 acc / WAW" --> C55 + C49 -- "1 res / 2 acc / WAW" --> C56 + C49 -- "1 res / 2 acc / WAW" --> C57 + C49 -- "1 res / 2 acc / RAW" --> C58 + C49 -- "1 res / 2 acc / RAW" --> C73 + C49 -- "1 res / 2 acc / RAW" --> C80 + C49 -- "1 res / 2 acc / RAW" --> C86 + C50 -- "1 res / 2 acc / WAW" --> C51 + C50 -- "1 res / 2 acc / WAW" --> C52 + C50 -- "1 res / 2 acc / WAW" --> C53 + C50 -- "1 res / 2 acc / WAW" --> C54 + C50 -- "1 res / 2 acc / WAW" --> C55 + C50 -- "1 res / 2 acc / WAW" --> C56 + C50 -- "1 res / 2 acc / WAW" --> C57 + C50 -- "1 res / 2 acc / RAW" --> C58 + C50 -- "1 res / 2 acc / RAW" --> C73 + C50 -- "1 res / 2 acc / RAW" --> C80 + C50 -- "1 res / 2 acc / RAW" --> C86 + C51 -- "1 res / 2 acc / WAW" --> C52 + C51 -- "1 res / 2 acc / WAW" --> C53 + C51 -- "1 res / 2 acc / WAW" --> C54 + C51 -- "1 res / 2 acc / WAW" --> C55 + C51 -- "1 res / 2 acc / WAW" --> C56 + C51 -- "1 res / 2 acc / WAW" --> C57 + C51 -- "1 res / 2 acc / RAW" --> C58 + C51 -- "1 res / 2 acc / RAW" --> C73 + C51 -- "1 res / 2 acc / RAW" --> C80 + C51 -- "1 res / 2 acc / RAW" --> C86 + C52 -- "1 res / 2 acc / WAW" --> C53 + C52 -- "1 res / 2 acc / WAW" --> C54 + C52 -- "1 res / 2 acc / WAW" --> C55 + C52 -- "1 res / 2 acc / WAW" --> C56 + C52 -- "1 res / 2 acc / WAW" --> C57 + C52 -- "1 res / 2 acc / RAW" --> C58 + C52 -- "1 res / 2 acc / RAW" --> C73 + C52 -- "1 res / 2 acc / RAW" --> C80 + C52 -- "1 res / 2 acc / RAW" --> C86 + C53 -- "1 res / 2 acc / WAW" --> C54 + C53 -- "1 res / 2 acc / WAW" --> C55 + C53 -- "1 res / 2 acc / WAW" --> C56 + C53 -- "1 res / 2 acc / WAW" --> C57 + C53 -- "1 res / 2 acc / RAW" --> C58 + C53 -- "1 res / 2 acc / RAW" --> C73 + C53 -- "1 res / 2 acc / RAW" --> C80 + C53 -- "1 res / 2 acc / RAW" --> C86 + C54 -- "1 res / 2 acc / WAW" --> C55 + C54 -- "1 res / 2 acc / WAW" --> C56 + C54 -- "1 res / 2 acc / WAW" --> C57 + C54 -- "1 res / 2 acc / RAW" --> C58 + C54 -- "1 res / 2 acc / RAW" --> C73 + C54 -- "1 res / 2 acc / RAW" --> C80 + C54 -- "1 res / 2 acc / RAW" --> C86 + C55 -- "1 res / 2 acc / WAW" --> C56 + C55 -- "1 res / 2 acc / WAW" --> C57 + C55 -- "1 res / 2 acc / RAW" --> C58 + C55 -- "1 res / 2 acc / RAW" --> C73 + C55 -- "1 res / 2 acc / RAW" --> C80 + C55 -- "1 res / 2 acc / RAW" --> C86 + C56 -- "1 res / 2 acc / WAW" --> C57 + C56 -- "1 res / 2 acc / RAW" --> C58 + C56 -- "1 res / 2 acc / RAW" --> C73 + C56 -- "1 res / 2 acc / RAW" --> C80 + C56 -- "1 res / 2 acc / RAW" --> C86 + C57 -- "1 res / 2 acc / RAW" --> C58 +``` diff --git a/src/driver/physical_device.rs b/src/driver/physical_device.rs index 5447cf91..89e5be72 100644 --- a/src/driver/physical_device.rs +++ b/src/driver/physical_device.rs @@ -930,7 +930,7 @@ impl Debug for PhysicalDevice { write!( f, "{} ({:?})", - &self.properties_v1_0.device_name, self.properties_v1_0.device_type + self.properties_v1_0.device_name, self.properties_v1_0.device_type ) } } @@ -3156,7 +3156,7 @@ impl From> for Vulkan12Properties { } #[cfg(test)] -mod tests { +mod test { use super::*; fn c_chars(bytes: &[u8]) -> Vec { diff --git a/src/fixture.rs b/src/fixture.rs new file mode 100644 index 00000000..3b5d411f --- /dev/null +++ b/src/fixture.rs @@ -0,0 +1,1068 @@ +use { + super::{AnyResource, CommandData, Execution, Graph}, + crate::{ + cmd::SubresourceRange, + driver::{ + self, + accel_struct::AccelerationStructureInfo, + buffer::{BufferInfo, BufferSubresourceRange}, + image::{ImageInfo, SampleCount}, + is_read_access, + }, + }, + ash::vk, + std::{ + collections::BTreeMap, + fmt::Write as _, + fs::write, + io::{self, ErrorKind}, + path::Path, + }, + vk_sync::AccessType, +}; + +const MAGIC: &[u8; 8] = b"VKGRFXT1"; +const MAX_ITEM_COUNT: usize = 1_000_000; +const MAX_FIXTURE_BYTES: u64 = 64 * 1024 * 1024; + +fn access_writes(access: AccessType) -> bool { + !matches!( + access, + AccessType::Nothing + | AccessType::CommandBufferReadNVX + | AccessType::IndirectBuffer + | AccessType::IndexBuffer + | AccessType::VertexBuffer + | AccessType::VertexShaderReadUniformBuffer + | AccessType::VertexShaderReadSampledImageOrUniformTexelBuffer + | AccessType::VertexShaderReadOther + | AccessType::MeshShaderReadUniformBuffer + | AccessType::MeshShaderReadSampledImageOrUniformTexelBuffer + | AccessType::MeshShaderReadOther + | AccessType::TaskShaderReadUniformBuffer + | AccessType::TaskShaderReadSampledImageOrUniformTexelBuffer + | AccessType::TaskShaderReadOther + | AccessType::TessellationControlShaderReadUniformBuffer + | AccessType::TessellationControlShaderReadSampledImageOrUniformTexelBuffer + | AccessType::TessellationControlShaderReadOther + | AccessType::TessellationEvaluationShaderReadUniformBuffer + | AccessType::TessellationEvaluationShaderReadSampledImageOrUniformTexelBuffer + | AccessType::TessellationEvaluationShaderReadOther + | AccessType::GeometryShaderReadUniformBuffer + | AccessType::GeometryShaderReadSampledImageOrUniformTexelBuffer + | AccessType::GeometryShaderReadOther + | AccessType::FragmentShaderReadUniformBuffer + | AccessType::FragmentShaderReadSampledImageOrUniformTexelBuffer + | AccessType::FragmentShaderReadColorInputAttachment + | AccessType::FragmentShaderReadDepthStencilInputAttachment + | AccessType::FragmentShaderReadOther + | AccessType::ColorAttachmentRead + | AccessType::DepthStencilAttachmentRead + | AccessType::ComputeShaderReadUniformBuffer + | AccessType::ComputeShaderReadSampledImageOrUniformTexelBuffer + | AccessType::ComputeShaderReadOther + | AccessType::AnyShaderReadUniformBuffer + | AccessType::AnyShaderReadUniformBufferOrVertexBuffer + | AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer + | AccessType::AnyShaderReadOther + | AccessType::TransferRead + | AccessType::HostRead + | AccessType::Present + | AccessType::RayTracingShaderReadSampledImageOrUniformTexelBuffer + | AccessType::RayTracingShaderReadColorInputAttachment + | AccessType::RayTracingShaderReadDepthStencilInputAttachment + | AccessType::RayTracingShaderReadAccelerationStructure + | AccessType::RayTracingShaderReadOther + | AccessType::AccelerationStructureBuildRead + ) +} + +fn invalid_data(message: &'static str) -> io::Error { + io::Error::new(ErrorKind::InvalidData, message) +} + +fn invalid_input(label: &'static str, message: &'static str) -> io::Error { + io::Error::new(ErrorKind::InvalidInput, format!("{label} {message}")) +} + +fn meaningful_execs(command: &CommandData) -> impl Iterator { + command + .execs + .iter() + .filter(|exec| exec.func.is_some() || exec.accesses.iter().len() != 0) +} + +fn meaningful_commands(commands: &[CommandData]) -> Vec<&CommandData> { + commands + .iter() + .filter(|command| meaningful_execs(command).next().is_some()) + .collect() +} + +fn relationship_markdown(graph: &Graph, commands: &[&CommandData]) -> String { + const MAX_EDGES: usize = 250; + + let mut by_resource = (0..graph.resources.len()) + .map(|_| BTreeMap::::new()) + .collect::>(); + let mut read_count = 0; + let mut write_count = 0; + let mut read_write_count = 0; + + for (command_idx, command) in commands.iter().enumerate() { + for exec in meaningful_execs(command) { + for (node_idx, accesses) in exec.accesses.iter() { + for access in accesses { + let reads = is_read_access(access.access); + let writes = access_writes(access.access); + match (reads, writes) { + (true, true) => read_write_count += 1, + (true, false) => read_count += 1, + (false, true) => write_count += 1, + (false, false) => {} + } + + let entry = by_resource[node_idx].entry(command_idx).or_default(); + entry.access_count += 1; + entry.reads |= reads; + entry.writes |= writes; + } + } + } + } + + let mut edges = BTreeMap::<(usize, usize), OverviewEdge>::new(); + for accesses in &by_resource { + let accesses = accesses.iter().collect::>(); + for left_idx in 0..accesses.len() { + let first_cmd = *accesses[left_idx].0; + let first = accesses[left_idx].1; + for second in &accesses[left_idx + 1..] { + let second_cmd = *second.0; + let second = second.1; + let raw = first.writes && second.reads; + let war = first.reads && second.writes; + let waw = first.writes && second.writes; + if !raw && !war && !waw { + continue; + } + + let edge = edges.entry((first_cmd, second_cmd)).or_default(); + edge.access_count += first.access_count + second.access_count; + edge.resource_count += 1; + edge.raw |= raw; + edge.war |= war; + edge.waw |= waw; + } + } + } + + let access_count = read_count + write_count + read_write_count; + let displayed = edges.len().min(MAX_EDGES); + let mut markdown = String::new(); + let _ = writeln!(markdown, "\n"); + let _ = writeln!(markdown, "# Graph Relationship Overview\n"); + let _ = writeln!(markdown, "- Resources: {}", graph.resources.len()); + let _ = writeln!(markdown, "- Commands: {}", commands.len()); + let _ = writeln!(markdown, "- Accesses: {access_count}"); + let _ = writeln!(markdown, "- Compact dependencies: {}\n", edges.len()); + let _ = writeln!(markdown, "## Command Dependencies\n"); + let _ = writeln!( + markdown, + "Edge labels are `resources / accesses / dependency kinds`, where RAW is read-after-write, WAR is write-after-read, and WAW is write-after-write.\n" + ); + let _ = writeln!(markdown, "```mermaid\nflowchart TD"); + + let mut displayed_commands = vec![false; commands.len()]; + for (&(first, second), _) in edges.iter().take(MAX_EDGES) { + displayed_commands[first] = true; + displayed_commands[second] = true; + } + for (command_idx, command) in commands.iter().enumerate() { + if displayed_commands[command_idx] { + let label = command.name().replace(['"', '\n', '\r'], " "); + let _ = writeln!(markdown, " C{command_idx}[\"cmd {command_idx}: {label}\"]"); + } + } + for (&(first, second), edge) in edges.iter().take(MAX_EDGES) { + let mut kinds = Vec::new(); + if edge.raw { + kinds.push("RAW"); + } + if edge.war { + kinds.push("WAR"); + } + if edge.waw { + kinds.push("WAW"); + } + let _ = writeln!( + markdown, + " C{first} -- \"{} res / {} acc / {}\" --> C{second}", + edge.resource_count, + edge.access_count, + kinds.join("+") + ); + } + let _ = writeln!(markdown, "```"); + + markdown +} + +fn sample_count_into_u8(sample_count: SampleCount) -> u8 { + match sample_count { + SampleCount::Type1 => 1, + SampleCount::Type2 => 2, + SampleCount::Type4 => 4, + SampleCount::Type8 => 8, + SampleCount::Type16 => 16, + SampleCount::Type32 => 32, + SampleCount::Type64 => 64, + } +} + +fn sample_count_from_u8(value: u8) -> io::Result { + match value { + 1 => Ok(SampleCount::Type1), + 2 => Ok(SampleCount::Type2), + 4 => Ok(SampleCount::Type4), + 8 => Ok(SampleCount::Type8), + 16 => Ok(SampleCount::Type16), + 32 => Ok(SampleCount::Type32), + 64 => Ok(SampleCount::Type64), + _ => Err(invalid_data("invalid image sample count")), + } +} + +/// An unstable graph fixture used for scheduler development. +/// +/// This type intentionally carries no Vulkan objects. [`Self::into_graph`] rebuilds a graph with +/// stream-argument resources and no-op command callbacks while preserving resource metadata, +/// execution boundaries, access types, and subresource ranges. +#[doc(hidden)] +#[derive(Debug)] +pub struct Fixture { + resources: Vec, + commands: Vec, +} + +impl Fixture { + /// Returns the number of subresource accesses stored by this fixture. + #[doc(hidden)] + pub fn access_count(&self) -> usize { + self.commands + .iter() + .flat_map(|cmd| &cmd.execs) + .map(|exec| exec.accesses.len()) + .sum() + } + + /// Returns the number of commands stored by this fixture. + #[doc(hidden)] + pub fn command_count(&self) -> usize { + self.commands.len() + } + + /// Rebuilds this fixture as a graph containing no-op command callbacks. + #[doc(hidden)] + pub fn into_graph(self) -> Graph { + let mut graph = Graph::new(); + + for (expected_idx, resource) in self.resources.into_iter().enumerate() { + let node_idx = match resource { + FixtureResource::AccelerationStructure(info) => { + graph.bind_stream_arg_resource(AnyResource::AccelerationStructureArg(info)) + } + FixtureResource::Buffer(info) => { + graph.bind_stream_arg_resource(AnyResource::BufferArg(info)) + } + FixtureResource::Image(info) => { + graph.bind_stream_arg_resource(AnyResource::ImageArg(info)) + } + }; + debug_assert_eq!(node_idx, expected_idx); + } + + for command in self.commands { + let mut graph_cmd = graph.begin_cmd(); + for exec in command.execs { + for access in exec.accesses { + graph_cmd.push_subresource_access_index( + access.node_idx, + access.subresource, + access.access, + ); + } + graph_cmd.record_cmd_mut(|_| {}); + } + graph_cmd.end_cmd(); + } + + graph + } + + /// Reads a binary graph fixture. + #[doc(hidden)] + pub fn read(path: impl AsRef) -> io::Result { + let path = path.as_ref(); + let file = std::fs::File::open(path)?; + let file_len = file.metadata()?.len(); + if file_len > MAX_FIXTURE_BYTES { + return Err(invalid_data("unsupported file size")); + } + + let bytes = std::fs::read(path)?; + let mut reader = FixtureReader::new(&bytes); + if reader.bytes(MAGIC.len())? != MAGIC { + return Err(invalid_data("invalid magic")); + } + + let resource_count = reader.count("resource count")?; + let command_count = reader.count("command count")?; + let mut resources = Vec::with_capacity(resource_count); + for _ in 0..resource_count { + resources.push(reader.read_resource()?); + } + + let mut commands = Vec::with_capacity(command_count); + for _ in 0..command_count { + let exec_count = reader.count("execution count")?; + if exec_count == 0 { + return Err(invalid_data("empty command")); + } + + let mut execs = Vec::with_capacity(exec_count); + for _ in 0..exec_count { + let access_count = reader.count("access count")?; + let mut accesses = Vec::with_capacity(access_count); + for _ in 0..access_count { + let node_idx = reader.u32()? as usize; + if node_idx >= resource_count { + return Err(invalid_data("invalid resource access")); + } + + let access_value = reader.u8()?; + if access_value > 67 { + return Err(invalid_data("invalid access type")); + } + + let subresource = reader.read_subresource()?; + resources[node_idx].validate_subresource(subresource)?; + accesses.push(FixtureAccess { + node_idx, + access: driver::access_type_from_u8(access_value), + subresource, + }); + } + execs.push(FixtureExecution { accesses }); + } + commands.push(FixtureCommand { execs }); + } + + if !reader.is_empty() { + return Err(invalid_data("unexpected trailing bytes")); + } + + Ok(Self { + resources, + commands, + }) + } + + /// Returns the number of resources stored by this fixture. + #[doc(hidden)] + pub fn resource_count(&self) -> usize { + self.resources.len() + } +} + +#[derive(Debug)] +struct FixtureAccess { + node_idx: usize, + access: AccessType, + subresource: SubresourceRange, +} + +#[derive(Debug)] +struct FixtureCommand { + execs: Vec, +} + +#[derive(Debug)] +struct FixtureExecution { + accesses: Vec, +} + +#[derive(Debug)] +enum FixtureResource { + AccelerationStructure(AccelerationStructureInfo), + Buffer(BufferInfo), + Image(ImageInfo), +} + +impl FixtureResource { + fn validate_subresource(&self, subresource: SubresourceRange) -> io::Result<()> { + match (self, subresource) { + (Self::AccelerationStructure(_), SubresourceRange::AccelerationStructure) => Ok(()), + (Self::Buffer(info), SubresourceRange::Buffer(range)) => { + let end = if range.end == vk::WHOLE_SIZE { + info.size + } else { + range.end + }; + if range.start >= end || end > info.size { + return Err(invalid_data("invalid buffer subresource range")); + } + + Ok(()) + } + (Self::Image(info), SubresourceRange::Image(range)) => { + let aspect_mask = driver::format_aspect_mask(info.format); + if range.aspect_mask.is_empty() || !aspect_mask.contains(range.aspect_mask) { + return Err(invalid_data("invalid image aspect mask")); + } + + let layers_fit = range.base_array_layer < info.array_layer_count + && (range.layer_count == vk::REMAINING_ARRAY_LAYERS + || range.layer_count > 0 + && range + .base_array_layer + .checked_add(range.layer_count) + .is_some_and(|end| end <= info.array_layer_count)); + let levels_fit = range.base_mip_level < info.mip_level_count + && (range.level_count == vk::REMAINING_MIP_LEVELS + || range.level_count > 0 + && range + .base_mip_level + .checked_add(range.level_count) + .is_some_and(|end| end <= info.mip_level_count)); + if !layers_fit || !levels_fit { + return Err(invalid_data("invalid image subresource range")); + } + + Ok(()) + } + _ => Err(invalid_data("subresource kind does not match resource")), + } + } +} + +struct FixtureReader<'a> { + bytes: &'a [u8], + cursor: usize, +} + +impl<'a> FixtureReader<'a> { + fn new(bytes: &'a [u8]) -> Self { + Self { bytes, cursor: 0 } + } + + fn bool(&mut self) -> io::Result { + match self.u8()? { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(invalid_data("invalid boolean")), + } + } + + fn bytes(&mut self, len: usize) -> io::Result<&'a [u8]> { + let end = self + .cursor + .checked_add(len) + .ok_or_else(|| invalid_data("overflow"))?; + let value = self + .bytes + .get(self.cursor..end) + .ok_or_else(|| invalid_data("underrun"))?; + self.cursor = end; + Ok(value) + } + + fn count(&mut self, label: &'static str) -> io::Result { + let count = self.u32()? as usize; + if count > MAX_ITEM_COUNT || count > self.bytes.len().saturating_sub(self.cursor) { + return Err(invalid_data(label)); + } + Ok(count) + } + + fn is_empty(&self) -> bool { + self.cursor == self.bytes.len() + } + + fn i32(&mut self) -> io::Result { + Ok(i32::from_le_bytes( + self.bytes(4)?.try_into().expect("slice length checked"), + )) + } + + fn u8(&mut self) -> io::Result { + Ok(self.bytes(1)?[0]) + } + + fn u32(&mut self) -> io::Result { + Ok(u32::from_le_bytes( + self.bytes(4)?.try_into().expect("slice length checked"), + )) + } + + fn u64(&mut self) -> io::Result { + Ok(u64::from_le_bytes( + self.bytes(8)?.try_into().expect("slice length checked"), + )) + } + + fn read_resource(&mut self) -> io::Result { + match self.u8()? { + 0 => Ok(FixtureResource::AccelerationStructure( + AccelerationStructureInfo { + acceleration_structure_type: vk::AccelerationStructureTypeKHR::from_raw( + self.i32()?, + ), + size: self.u64()?, + }, + )), + 1 => Ok(FixtureResource::Buffer(BufferInfo { + alignment: self.u64()?, + alloc_dedicated: self.bool()?, + host_readable: self.bool()?, + host_writable: self.bool()?, + sharing_mode: vk::SharingMode::from_raw(self.i32()?), + size: self.u64()?, + usage: vk::BufferUsageFlags::from_raw(self.u32()?), + })), + 2 => Ok(FixtureResource::Image(ImageInfo { + alloc_dedicated: self.bool()?, + array_layer_count: self.u32()?, + depth: self.u32()?, + flags: vk::ImageCreateFlags::from_raw(self.u32()?), + format: vk::Format::from_raw(self.i32()?), + height: self.u32()?, + host_readable: self.bool()?, + host_writable: self.bool()?, + mip_level_count: self.u32()?, + sample_count: sample_count_from_u8(self.u8()?)?, + sharing_mode: vk::SharingMode::from_raw(self.i32()?), + tiling: vk::ImageTiling::from_raw(self.i32()?), + image_type: vk::ImageType::from_raw(self.i32()?), + usage: vk::ImageUsageFlags::from_raw(self.u32()?), + width: self.u32()?, + })), + _ => Err(invalid_data("invalid resource kind")), + } + } + + fn read_subresource(&mut self) -> io::Result { + match self.u8()? { + 0 => Ok(SubresourceRange::AccelerationStructure), + 1 => Ok(SubresourceRange::Buffer(BufferSubresourceRange { + start: self.u64()?, + end: self.u64()?, + })), + 2 => Ok(SubresourceRange::Image(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::from_raw(self.u32()?), + base_mip_level: self.u32()?, + level_count: self.u32()?, + base_array_layer: self.u32()?, + layer_count: self.u32()?, + })), + _ => Err(invalid_data("invalid subresource kind")), + } + } +} + +#[derive(Default)] +struct FixtureWriter(Vec); + +impl FixtureWriter { + fn bool(&mut self, value: bool) { + self.u8(u8::from(value)); + } + + fn bytes(&mut self, value: &[u8]) { + self.0.extend_from_slice(value); + } + + fn count(&mut self, value: usize, label: &'static str) -> io::Result<()> { + if value > MAX_ITEM_COUNT { + return Err(invalid_input(label, "exceeds the fixture limit")); + } + + let value = u32::try_from(value) + .map_err(|_| invalid_input(label, "does not fit in the fixture format"))?; + self.u32(value); + + Ok(()) + } + + fn finish(self) -> io::Result> { + Self::validate_size(self.0.len())?; + + Ok(self.0) + } + + fn i32(&mut self, value: i32) { + self.bytes(&value.to_le_bytes()); + } + + fn u8(&mut self, value: u8) { + self.0.push(value); + } + + fn u32(&mut self, value: u32) { + self.bytes(&value.to_le_bytes()); + } + + fn u64(&mut self, value: u64) { + self.bytes(&value.to_le_bytes()); + } + + fn validate_size(len: usize) -> io::Result<()> { + let len = u64::try_from(len) + .map_err(|_| invalid_input("fixture size", "does not fit in the fixture format"))?; + if len > MAX_FIXTURE_BYTES { + return Err(invalid_input("fixture size", "exceeds the fixture limit")); + } + + Ok(()) + } + + fn write_resource(&mut self, resource: &AnyResource) { + let kind = match resource { + AnyResource::AccelerationStructure(_) + | AnyResource::AccelerationStructureArg(_) + | AnyResource::AccelerationStructureLease(_) => 0, + AnyResource::Buffer(_) | AnyResource::BufferArg(_) | AnyResource::BufferLease(_) => 1, + AnyResource::Image(_) + | AnyResource::ImageArg(_) + | AnyResource::ImageLease(_) + | AnyResource::SwapchainImage(_) => 2, + }; + self.u8(kind); + match kind { + 0 => { + let info = resource.expect_accel_struct_info(); + self.i32(info.acceleration_structure_type.as_raw()); + self.u64(info.size); + } + 1 => { + let info = resource.expect_buffer_info(); + self.u64(info.alignment); + self.bool(info.alloc_dedicated); + self.bool(info.host_readable); + self.bool(info.host_writable); + self.i32(info.sharing_mode.as_raw()); + self.u64(info.size); + self.u32(info.usage.as_raw()); + } + 2 => { + let info = resource.expect_image_info(); + self.bool(info.alloc_dedicated); + self.u32(info.array_layer_count); + self.u32(info.depth); + self.u32(info.flags.as_raw()); + self.i32(info.format.as_raw()); + self.u32(info.height); + self.bool(info.host_readable); + self.bool(info.host_writable); + self.u32(info.mip_level_count); + self.u8(sample_count_into_u8(info.sample_count)); + self.i32(info.sharing_mode.as_raw()); + self.i32(info.tiling.as_raw()); + self.i32(info.image_type.as_raw()); + self.u32(info.usage.as_raw()); + self.u32(info.width); + } + _ => unreachable!(), + } + } + + fn write_subresource(&mut self, subresource: SubresourceRange) { + match subresource { + SubresourceRange::AccelerationStructure => self.u8(0), + SubresourceRange::Buffer(range) => { + self.u8(1); + self.u64(range.start); + self.u64(range.end); + } + SubresourceRange::Image(range) => { + self.u8(2); + self.u32(range.aspect_mask.as_raw()); + self.u32(range.base_mip_level); + self.u32(range.level_count); + self.u32(range.base_array_layer); + self.u32(range.layer_count); + } + } + } +} + +impl Graph { + /// Exports scheduler data to an unstable binary fixture and Markdown overview. + /// + /// The fixture contains resource descriptions, command execution boundaries, access types, and + /// subresource ranges, but no resource contents or command callbacks. This API exists for + /// development and benchmark fixture generation; its format may change without notice. + #[doc(hidden)] + pub fn export_fixture( + &self, + binary_path: impl AsRef, + markdown_path: impl AsRef, + ) -> io::Result<()> { + let commands = meaningful_commands(&self.cmds); + let mut writer = FixtureWriter::default(); + writer.bytes(MAGIC); + writer.count(self.resources.len(), "resource count")?; + writer.count(commands.len(), "command count")?; + + for resource in self.resources.iter() { + writer.write_resource(resource); + } + + for command in &commands { + let execs = meaningful_execs(command).collect::>(); + writer.count(execs.len(), "execution count")?; + for exec in execs { + let access_count = exec + .accesses + .iter() + .map(|(_, accesses)| accesses.len()) + .sum(); + writer.count(access_count, "access count")?; + + for (node_idx, accesses) in exec.accesses.iter() { + for access in accesses { + writer.u32(node_idx as u32); + writer.u8(driver::access_type_into_u8(access.access)); + writer.write_subresource(access.subresource); + } + } + } + } + + let bytes = writer.finish()?; + write(binary_path, bytes)?; + write(markdown_path, relationship_markdown(self, &commands)) + } + + /// Imports an unstable graph fixture as a graph with no-op callbacks. + #[doc(hidden)] + pub fn import_fixture(binary_path: impl AsRef) -> io::Result { + Self::read_fixture(binary_path).map(Fixture::into_graph) + } + + /// Reads an unstable graph fixture. + #[doc(hidden)] + pub fn read_fixture(binary_path: impl AsRef) -> io::Result { + Fixture::read(binary_path) + } +} + +#[derive(Clone, Copy, Default)] +struct ResourceCommandAccess { + access_count: usize, + reads: bool, + writes: bool, +} + +#[derive(Clone, Copy, Default)] +struct OverviewEdge { + access_count: usize, + resource_count: usize, + raw: bool, + war: bool, + waw: bool, +} + +#[cfg(test)] +mod test { + use { + super::*, + crate::driver::{ + accel_struct::AccelerationStructureInfo, buffer::BufferInfo, image::ImageInfo, + }, + }; + + fn fixture_paths(name: &str) -> (std::path::PathBuf, std::path::PathBuf) { + let stamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system time before UNIX_EPOCH") + .as_nanos(); + let base = + std::env::temp_dir().join(format!("vk_graph_{name}_{}_{}", std::process::id(), stamp)); + (base.with_extension("bin"), base.with_extension("md")) + } + + fn fixture_graph() -> Graph { + let mut graph = Graph::new(); + let buffer = + graph.bind_stream_arg_resource(AnyResource::BufferArg(BufferInfo::device_mem( + 64, + vk::BufferUsageFlags::TRANSFER_DST | vk::BufferUsageFlags::TRANSFER_SRC, + ))); + let image = + graph.bind_stream_arg_resource(AnyResource::ImageArg(ImageInfo::image_2d_array( + 4, + 4, + 2, + vk::Format::R8_UINT, + vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST, + ))); + let accel_struct = graph.bind_stream_arg_resource(AnyResource::AccelerationStructureArg( + AccelerationStructureInfo::blas(256), + )); + + let mut command = graph.begin_cmd(); + command.push_subresource_access_index( + buffer, + SubresourceRange::Buffer(BufferSubresourceRange { start: 0, end: 32 }), + AccessType::TransferWrite, + ); + command.push_subresource_access_index( + image, + SubresourceRange::Image(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, + }), + AccessType::TransferRead, + ); + command.record_cmd_mut(|_| {}); + command.end_cmd(); + + let mut command = graph.begin_cmd(); + command.push_subresource_access_index( + accel_struct, + SubresourceRange::AccelerationStructure, + AccessType::RayTracingShaderReadAccelerationStructure, + ); + command.push_subresource_access_index( + image, + SubresourceRange::Image(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 1, + layer_count: 1, + }), + AccessType::TransferWrite, + ); + command.record_cmd_mut(|_| {}); + command.end_cmd(); + + graph + } + + fn fixture_with_access(resource: AnyResource, subresource: SubresourceRange) -> Vec { + let mut writer = FixtureWriter::default(); + writer.bytes(MAGIC); + writer.count(1, "resource count").unwrap(); + writer.count(1, "command count").unwrap(); + writer.write_resource(&resource); + writer.count(1, "execution count").unwrap(); + writer.count(1, "access count").unwrap(); + writer.u32(0); + writer.u8(driver::access_type_into_u8(AccessType::TransferRead)); + writer.write_subresource(subresource); + writer.finish().unwrap() + } + + fn read_fixture_bytes(name: &str, bytes: &[u8]) -> io::Result { + let (binary_path, _) = fixture_paths(name); + std::fs::write(&binary_path, bytes).expect("unable to write test fixture"); + let result = Fixture::read(&binary_path); + let _ = std::fs::remove_file(binary_path); + result + } + + #[test] + fn fixture_round_trips() { + let graph = fixture_graph(); + let (binary_path, markdown_path) = fixture_paths("round_trip"); + graph + .export_fixture(&binary_path, &markdown_path) + .expect("unable to export fixture"); + + let fixture = Fixture::read(&binary_path).expect("unable to read fixture"); + assert_eq!(fixture.resource_count(), 3); + assert_eq!(fixture.command_count(), 2); + assert_eq!(fixture.access_count(), 4); + + let rebuilt = fixture.into_graph(); + assert_eq!(rebuilt.resources.len(), 3); + assert_eq!(rebuilt.cmds.len(), 2); + assert_eq!( + rebuilt + .cmds + .iter() + .flat_map(|command| &command.execs) + .flat_map(|exec| exec.accesses.iter()) + .map(|(_, accesses)| accesses.len()) + .sum::(), + 4 + ); + assert_eq!(rebuilt.finalize().graph().cmds.len(), 2); + + let markdown = std::fs::read_to_string(&markdown_path).expect("missing Markdown fixture"); + assert!(markdown.contains("Command Dependencies")); + assert!(markdown.contains("WAR")); + + let _ = std::fs::remove_file(binary_path); + let _ = std::fs::remove_file(markdown_path); + } + + #[test] + fn real_fixtures_import_and_finalize() { + for (name, resources, commands, accesses) in [ + ("graph-1783212230368.bin", 125, 49, 262), + ("graph-1783212245365.bin", 114, 114, 401), + ] { + let path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("res/graph-fixture") + .join(name); + let fixture = + Fixture::read(&path).unwrap_or_else(|err| panic!("unable to read {name}: {err}")); + assert_eq!(fixture.resource_count(), resources); + assert_eq!(fixture.command_count(), commands); + assert_eq!(fixture.access_count(), accesses); + assert_eq!(fixture.into_graph().finalize().graph().cmds.len(), commands); + } + } + + #[test] + fn fixture_rejects_malformed_subresources() { + let buffer_info = BufferInfo::device_mem(64, vk::BufferUsageFlags::TRANSFER_SRC); + let image_info = ImageInfo::image_2d_array( + 4, + 4, + 2, + vk::Format::R8_UINT, + vk::ImageUsageFlags::TRANSFER_SRC, + ); + let image_range = + |aspect_mask, base_mip_level, level_count, base_array_layer, layer_count| { + SubresourceRange::Image(vk::ImageSubresourceRange { + aspect_mask, + base_mip_level, + level_count, + base_array_layer, + layer_count, + }) + }; + + for (name, bytes, message) in [ + ( + "mismatched_kind", + fixture_with_access( + AnyResource::BufferArg(buffer_info), + SubresourceRange::AccelerationStructure, + ), + "subresource kind does not match resource", + ), + ( + "buffer_bounds", + fixture_with_access( + AnyResource::BufferArg(buffer_info), + SubresourceRange::Buffer(BufferSubresourceRange { start: 32, end: 65 }), + ), + "invalid buffer subresource range", + ), + ( + "image_bounds", + fixture_with_access( + AnyResource::ImageArg(image_info), + image_range(vk::ImageAspectFlags::COLOR, 0, 1, 1, 2), + ), + "invalid image subresource range", + ), + ( + "image_aspect", + fixture_with_access( + AnyResource::ImageArg(image_info), + image_range(vk::ImageAspectFlags::DEPTH, 0, 1, 0, 1), + ), + "invalid image aspect mask", + ), + ] { + let error = read_fixture_bytes(name, &bytes).expect_err("fixture should fail"); + assert_eq!(error.kind(), ErrorKind::InvalidData); + assert_eq!(error.to_string(), message); + } + } + + #[test] + fn fixture_accepts_remaining_subresource_ranges() { + let bytes = fixture_with_access( + AnyResource::ImageArg(ImageInfo::image_2d_array( + 4, + 4, + 2, + vk::Format::R8_UINT, + vk::ImageUsageFlags::TRANSFER_SRC, + )), + SubresourceRange::Image(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: vk::REMAINING_MIP_LEVELS, + base_array_layer: 1, + layer_count: vk::REMAINING_ARRAY_LAYERS, + }), + ); + + read_fixture_bytes("remaining_ranges", &bytes).expect("fixture should be valid"); + } + + #[test] + fn fixture_rejects_empty_commands() { + let mut writer = FixtureWriter::default(); + writer.bytes(MAGIC); + writer.count(0, "resource count").unwrap(); + writer.count(1, "command count").unwrap(); + writer.count(0, "execution count").unwrap(); + + let bytes = writer.finish().unwrap(); + let error = read_fixture_bytes("empty_command", &bytes).expect_err("fixture should fail"); + assert_eq!(error.kind(), ErrorKind::InvalidData); + assert_eq!(error.to_string(), "empty command"); + } + + #[test] + fn fixture_writer_enforces_reader_limits() { + let mut writer = FixtureWriter::default(); + let count_error = writer + .count(MAX_ITEM_COUNT + 1, "test count") + .expect_err("oversized count should fail"); + assert_eq!(count_error.kind(), ErrorKind::InvalidInput); + + FixtureWriter::validate_size(MAX_FIXTURE_BYTES as usize) + .expect("maximum size should be valid"); + let size_error = FixtureWriter::validate_size(MAX_FIXTURE_BYTES as usize + 1) + .expect_err("oversized fixture should fail"); + assert_eq!(size_error.kind(), ErrorKind::InvalidInput); + } + + #[test] + fn relationship_fixture_rejects_invalid_input() { + let (binary_path, _) = fixture_paths("invalid"); + std::fs::write(&binary_path, b"not a fixture").expect("unable to write invalid fixture"); + let error = Fixture::read(&binary_path).expect_err("fixture should fail"); + assert_eq!(error.kind(), ErrorKind::InvalidData); + let _ = std::fs::remove_file(binary_path); + } +} diff --git a/src/lib.rs b/src/lib.rs index 06c21ae4..ff1decaa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,10 @@ pub mod pool; pub mod stream; pub mod submission; +#[doc(hidden)] +pub use self::fixture::Fixture; + +mod fixture; mod lazy_str; pub use self::lazy_str::LazyStr; diff --git a/src/stream.rs b/src/stream.rs index f640b86e..f6076412 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -1036,6 +1036,7 @@ impl CommandStreamDraft { /// An in-progress invocation of a [`CommandStream`] into a [`Graph`]. /// /// Bind every declared stream argument before calling [`CommandStreamRun::finish`]. +/// Distinct stream arguments must bind to distinct parent-graph nodes. /// /// ```no_run /// # use ash::vk; @@ -1065,6 +1066,13 @@ pub struct CommandStreamRun<'a, A> { impl<'a, A> CommandStreamRun<'a, A> { /// Sets a stream argument to a graph node for this invocation. + /// + /// The same argument may be rebound, but distinct arguments cannot bind to the same parent + /// node. Stream scheduling and resource ownership tracking use node identity. + /// + /// # Panics + /// + /// Panics if another argument is already bound to `node`. pub fn with_arg(mut self, arg: StreamArg, node: N) -> Self where N: StreamArgBindable, @@ -1076,7 +1084,15 @@ impl<'a, A> CommandStreamRun<'a, A> { ); node.assert_parent_node(); self.graph.assert_node_owner(&node); - self.bindings[arg.arg_index] = Some(node.index()); + let node_idx = node.index(); + assert!( + self.bindings + .iter() + .enumerate() + .all(|(arg_idx, binding)| arg_idx == arg.arg_index || *binding != Some(node_idx)), + "distinct command stream arguments cannot bind to the same parent graph node" + ); + self.bindings[arg.arg_index] = Some(node_idx); self } @@ -1400,7 +1416,7 @@ stream_resource_node!( ); #[cfg(test)] -mod tests { +mod test { use super::*; use crate::{ driver::{ @@ -1431,6 +1447,46 @@ mod tests { } } + fn bind_test_buffer(graph: &mut Graph) -> BufferNode { + let index = graph.bind_stream_arg_resource(AnyResource::BufferArg(BufferInfo::device_mem( + 4, + vk::BufferUsageFlags::TRANSFER_SRC, + ))); + BufferNode::new( + index, + #[cfg(feature = "checked")] + graph.graph_id(), + ) + } + + fn two_buffer_arg_stream() -> CommandStreamDraft<(BufferArg, BufferArg)> { + CommandStream::finalize(|stream| { + let first = stream.arg(BufferInfo::device_mem( + 4, + vk::BufferUsageFlags::TRANSFER_SRC, + )); + let second = stream.arg(BufferInfo::device_mem( + 4, + vk::BufferUsageFlags::TRANSFER_DST, + )); + + stream + .begin_cmd() + .resource_access(first, vk_sync::AccessType::TransferRead) + .record_cmd(|_| {}); + stream + .begin_cmd() + .resource_access(second, vk_sync::AccessType::TransferWrite) + .record_cmd(|_| {}); + stream + .begin_cmd() + .resource_access(first, vk_sync::AccessType::TransferRead) + .record_cmd(|_| {}); + + (first, second) + }) + } + #[test] fn empty_stream_can_be_inserted() { let stream = CommandStream::finalize(|_| {}).into_stream(); @@ -1508,6 +1564,56 @@ mod tests { assert_eq!(graph.cmds.len(), 1); } + #[test] + #[should_panic( + expected = "distinct command stream arguments cannot bind to the same parent graph node" + )] + fn unprepared_stream_rejects_aliased_arguments() { + let stream = two_buffer_arg_stream().into_stream(); + let mut graph = Graph::new(); + let buffer = bind_test_buffer(&mut graph); + + graph + .insert_cmd_stream(&stream) + .with_arg(stream.args.0, buffer) + .with_arg(stream.args.1, buffer) + .finish(); + } + + #[test] + #[should_panic( + expected = "distinct command stream arguments cannot bind to the same parent graph node" + )] + fn prepared_stream_rejects_aliased_arguments() { + let mut pool = NoopPool; + let stream = two_buffer_arg_stream() + .prepare(&mut pool) + .expect("prepare stream"); + let mut graph = Graph::new(); + let buffer = bind_test_buffer(&mut graph); + + graph + .insert_cmd_stream(&stream) + .with_arg(stream.args.0, buffer) + .with_arg(stream.args.1, buffer) + .finish(); + } + + #[test] + fn same_argument_can_be_rebound() { + let stream = two_buffer_arg_stream().into_stream(); + let mut graph = Graph::new(); + let first = bind_test_buffer(&mut graph); + let second = bind_test_buffer(&mut graph); + + graph + .insert_cmd_stream(&stream) + .with_arg(stream.args.0, first) + .with_arg(stream.args.0, second) + .with_arg(stream.args.1, first) + .finish(); + } + #[test] fn image_arg_can_use_info_based_helpers() { let stream = CommandStream::finalize(|stream| { diff --git a/src/submission.rs b/src/submission.rs index baaec732..f2e0bb9c 100644 --- a/src/submission.rs +++ b/src/submission.rs @@ -38,7 +38,7 @@ use { format_aspect_mask, graphics::{DepthStencilInfo, GraphicsPipeline}, image::{ - DenseMap, Image, image_subresource_range_contains, + DenseMap, Image, ImageInfo, image_subresource_range_contains, image_subresource_range_intersection, }, initial_image_layout_access, is_read_access, @@ -60,7 +60,8 @@ use { smallvec::SmallVec, std::{ cell::RefCell, - collections::{BTreeMap, HashMap, HashSet, VecDeque}, + cmp::Reverse, + collections::{BTreeMap, BTreeSet, HashMap}, iter::repeat_n, mem::take, ops::Range, @@ -599,41 +600,18 @@ fn pipeline_barrier_from_iters<'a>( } fn schedule_dependency_cmds_before_target_access( - access_index: &CommandAccessIndex, target_node_idx: usize, first_target_cmd_idx: usize, - schedule: &mut Vec, + schedule: &mut Schedule, ) { - let mut pending_nodes = VecDeque::new(); - let mut scheduled = FixedBitSet::with_capacity(first_target_cmd_idx); - let mut queued_nodes = FixedBitSet::with_capacity(access_index.cmds_by_node.len()); - - for node_idx in access_index.read_nodes_for_cmd(first_target_cmd_idx) { - if node_idx != target_node_idx { - pending_nodes.push_back((node_idx, first_target_cmd_idx)); - queued_nodes.insert(node_idx); - } - } - - while let Some((node_idx, end_cmd_idx)) = pending_nodes.pop_front() { - for cmd_idx in access_index.prior_cmds_for_node(node_idx, end_cmd_idx) { - if scheduled.put(cmd_idx) { - continue; - } - - schedule.push(cmd_idx); - - for read_node_idx in access_index.read_nodes_for_cmd(cmd_idx) { - if queued_nodes.put(read_node_idx) { - continue; - } - - pending_nodes.push_back((read_node_idx, cmd_idx)); - } - } - } - - schedule.sort_unstable(); + let required_prefixes = schedule + .access_index + .read_nodes_for_cmd(first_target_cmd_idx) + .filter(|&node_idx| node_idx != target_node_idx) + .map(|node_idx| (node_idx, first_target_cmd_idx)) + .collect::>(); + + schedule.schedule_required_node_prefixes(required_prefixes); } fn submit_stage_mask_legacy(stage_mask: vk::PipelineStageFlags2) -> vk::PipelineStageFlags { @@ -799,26 +777,6 @@ struct BufferQueueOwnershipTransfer { range: BufferSubresourceRange, dst_queue_family_index: u32, src_queue_family_index: u32, - src_queue_index: u32, -} - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -struct BufferSubresourceRangeKey { - start: vk::DeviceSize, - end: vk::DeviceSize, -} - -impl BufferSubresourceRangeKey { - fn from_range(BufferSubresourceRange { start, end }: BufferSubresourceRange) -> Self { - Self { start, end } - } - - fn into_range(self) -> BufferSubresourceRange { - BufferSubresourceRange { - start: self.start, - end: self.end, - } - } } #[derive(Clone, Default)] @@ -833,28 +791,6 @@ impl CommandAccessIndex { self.accessed_nodes_by_cmd[cmd_idx].iter().copied() } - #[profiling::function] - fn prior_cmds_for_node( - &self, - node_idx: usize, - end_cmd_idx: usize, - ) -> impl Iterator + '_ { - let cmds = &self.cmds_by_node[node_idx]; - let end_idx = cmds.partition_point(|&cmd_idx| cmd_idx < end_cmd_idx); - - cmds[..end_idx].iter().rev().copied() - } - - #[profiling::function] - fn prior_read_dependency_cmds( - &self, - cmd_idx: usize, - end_cmd_idx: usize, - ) -> impl Iterator + '_ { - self.read_nodes_for_cmd(cmd_idx) - .flat_map(move |node_idx| self.prior_cmds_for_node(node_idx, end_cmd_idx)) - } - fn update(&mut self, graph: &Graph, end_cmd_idx: usize) { let binding_count = graph.resources.len(); let cmds = &graph.cmds[0..end_cmd_idx]; @@ -1011,12 +947,6 @@ impl PartialEq for ImageQueueOwnershipTransfer { } } -#[derive(Debug)] -struct ImageRangeSet { - image: vk::Image, - range_keys: HashSet, -} - struct ImageSubresourceRangeDebug(vk::ImageSubresourceRange); impl std::fmt::Debug for ImageSubresourceRangeDebug { @@ -1035,54 +965,6 @@ impl std::fmt::Debug for ImageSubresourceRangeDebug { } } -/// This is needed because vk::ImageSubresourceRange doesn't currently support Hash -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -struct ImageSubresourceRangeKey { - aspect_mask: vk::ImageAspectFlags, - base_array_layer: u32, - layer_count: u32, - base_mip_level: u32, - level_count: u32, -} - -impl ImageSubresourceRangeKey { - fn from_range( - vk::ImageSubresourceRange { - aspect_mask, - base_array_layer, - layer_count, - base_mip_level, - level_count, - }: vk::ImageSubresourceRange, - ) -> Self { - Self { - aspect_mask, - base_array_layer, - layer_count, - base_mip_level, - level_count, - } - } - - fn into_range( - Self { - aspect_mask, - base_array_layer, - layer_count, - base_mip_level, - level_count, - }: Self, - ) -> vk::ImageSubresourceRange { - vk::ImageSubresourceRange { - aspect_mask, - base_array_layer, - layer_count, - base_mip_level, - level_count, - } - } -} - #[derive(Debug)] struct NodeIndexedScratch { entries: Vec>, @@ -1274,6 +1156,27 @@ struct QueueOwnershipReleaseGroup { src_queue_index: u32, } +fn queue_ownership_release_group( + groups: &mut Vec, + src_queue_family_index: u32, + src_queue_index: u32, +) -> &mut QueueOwnershipReleaseGroup { + if let Some(group_idx) = groups.iter().position(|group| { + group.src_queue_family_index == src_queue_family_index + && group.src_queue_index == src_queue_index + }) { + return &mut groups[group_idx]; + } + + groups.push(QueueOwnershipReleaseGroup { + buffers: Vec::new(), + images: Vec::new(), + src_queue_family_index, + src_queue_index, + }); + groups.last_mut().expect("missing ownership release group") +} + /// Submission payload for [`RecordedSubmission::queue_submit`]. #[derive(Clone, Copy, Debug)] pub enum QueueSubmitInfo<'a> { @@ -1663,6 +1566,7 @@ pub struct Recording<'p, P, Cb> { #[readonly] pub resource_pool: &'p mut P, + ownership: RecordingOwnership, submission: Submission, } @@ -1690,6 +1594,7 @@ where P: Pool, { let Self { + ownership: _, cmd_buf, resource_pool, submission, @@ -1739,18 +1644,163 @@ where self.resource_pool, self.cmd_buf.as_ref(), selection.into(), + &mut self.ownership, ) } } +#[derive(Debug, Default)] +struct RecordingOwnership { + // These ranges are effectively owned by this recording, but global ownership is not updated + // until its command buffer is submitted successfully. + buffers: HashMap>, + images: HashMap>, +} + +impl RecordingOwnership { + fn claim_buffer( + &mut self, + node_idx: usize, + range: BufferSubresourceRange, + ) -> SmallVec<[BufferSubresourceRange; 4]> { + let claimed = self.buffers.entry(node_idx).or_default(); + let mut unclaimed = SmallVec::<[BufferSubresourceRange; 4]>::from_slice(&[range]); + + for &claimed_range in claimed.iter() { + let mut remaining = SmallVec::<[BufferSubresourceRange; 4]>::new(); + + for range in unclaimed.drain(..) { + let Some(overlap) = range.intersection(claimed_range) else { + remaining.push(range); + continue; + }; + + if range.start < overlap.start { + remaining.push(BufferSubresourceRange { + start: range.start, + end: overlap.start, + }); + } + if overlap.end < range.end { + remaining.push(BufferSubresourceRange { + start: overlap.end, + end: range.end, + }); + } + } + + unclaimed = remaining; + if unclaimed.is_empty() { + break; + } + } + + claimed.extend(unclaimed.iter().copied()); + unclaimed + } + + fn claim_image( + &mut self, + node_idx: usize, + info: ImageInfo, + range: vk::ImageSubresourceRange, + ) -> SmallVec<[vk::ImageSubresourceRange; 4]> { + self.images + .entry(node_idx) + .or_insert_with(|| DenseMap::new(info, false)) + .swap(true, range) + .filter_map(|(claimed, range)| (!claimed).then_some(range)) + .collect() + } +} + +#[derive(Default)] +struct NodeScheduleScratch { + covered_node_prefixes: Vec, + pending_cmds: Vec, + selected_cmds: FixedBitSet, +} + #[derive(Default)] struct Schedule { access_index: CommandAccessIndex, - interdependent: Vec>, cmds: Vec, + local_of_global: Vec, + successors: Vec>, + predecessor_counts: Vec, + remaining_predecessors: Vec, + ready: BTreeSet<(usize, Reverse)>, + reordered: Vec, + node_schedule: NodeScheduleScratch, } impl Schedule { + fn schedule_required_node_prefixes( + &mut self, + required_prefixes: impl IntoIterator, + ) { + fn schedule_node_prefix( + access_index: &CommandAccessIndex, + schedule: &mut Vec, + scratch: &mut NodeScheduleScratch, + node_idx: usize, + end_cmd_idx: usize, + ) { + let node_cmds = &access_index.cmds_by_node[node_idx]; + let end_prefix = node_cmds.partition_point(|&cmd_idx| cmd_idx < end_cmd_idx); + let start_prefix = scratch.covered_node_prefixes[node_idx]; + + if end_prefix <= start_prefix { + return; + } + + scratch.covered_node_prefixes[node_idx] = end_prefix; + + // Selecting any user of a resource requires the complete preceding resource prefix. + for &cmd_idx in &node_cmds[start_prefix..end_prefix] { + if !scratch.selected_cmds.put(cmd_idx) { + schedule.push(cmd_idx); + scratch.pending_cmds.push(cmd_idx); + } + } + } + + self.cmds.clear(); + self.node_schedule.covered_node_prefixes.clear(); + self.node_schedule + .covered_node_prefixes + .resize(self.access_index.cmds_by_node.len(), 0); + self.node_schedule.pending_cmds.clear(); + self.node_schedule.selected_cmds.clear(); + self.node_schedule + .selected_cmds + .grow(self.access_index.accessed_nodes_by_cmd.len()); + + for (node_idx, end_cmd_idx) in required_prefixes { + schedule_node_prefix( + &self.access_index, + &mut self.cmds, + &mut self.node_schedule, + node_idx, + end_cmd_idx, + ); + } + + while let Some(cmd_idx) = self.node_schedule.pending_cmds.pop() { + for node_idx in self.access_index.read_nodes_for_cmd(cmd_idx) { + schedule_node_prefix( + &self.access_index, + &mut self.cmds, + &mut self.node_schedule, + node_idx, + cmd_idx + 1, + ); + } + } + + self.cmds.sort_unstable(); + } + #[profiling::function] fn reorder_cmds(&mut self, end_cmd_idx: usize) { if self.cmds.len() < 3 { @@ -1759,75 +1809,86 @@ impl Schedule { let cmd_count = self.cmds.len(); - for dep_cmds in self.interdependent.iter_mut() { - dep_cmds.clear(); - } - - self.interdependent.resize_with(cmd_count, Vec::new); - - let mut local_of_global = vec![usize::MAX; end_cmd_idx]; + self.local_of_global.resize(end_cmd_idx, usize::MAX); + self.local_of_global.fill(usize::MAX); for (local_idx, &cmd_idx) in self.cmds.iter().enumerate() { - local_of_global[cmd_idx] = local_idx; + self.local_of_global[cmd_idx] = local_idx; } - let mut seen_deps = FixedBitSet::with_capacity(cmd_count); + for successors in &mut self.successors { + successors.clear(); + } + self.successors.resize_with(cmd_count, Vec::new); + self.predecessor_counts.resize(cmd_count, 0); + self.predecessor_counts.fill(0); - for (local_idx, &cmd_idx) in self.cmds.iter().enumerate() { - for dep_cmd_idx in self - .access_index - .prior_read_dependency_cmds(cmd_idx, end_cmd_idx) - { - let dep_local_idx = local_of_global[dep_cmd_idx]; - if dep_local_idx == usize::MAX || dep_local_idx == local_idx { + // Consecutive selected users of each resource form a dependency chain. This preserves the + // original relative order for every shared-resource pair while still allowing unrelated + // command chains to be grouped for locality. + for resource_cmds in &self.access_index.cmds_by_node { + let mut previous = Option::::None; + for &cmd_idx in resource_cmds { + let Some(&local_idx) = self.local_of_global.get(cmd_idx) else { continue; - } + }; - if !seen_deps.put(dep_local_idx) { - self.interdependent[local_idx].push(dep_local_idx); + if local_idx == usize::MAX { + continue; } - } - for dep_cmd_idx in self - .access_index - .prior_read_dependency_cmds(cmd_idx, end_cmd_idx) - { - let dep_local_idx = local_of_global[dep_cmd_idx]; - if dep_local_idx != usize::MAX && dep_local_idx != local_idx { - seen_deps.set(dep_local_idx, false); + if let Some(previous_idx) = previous { + self.successors[previous_idx].push(local_idx); + self.predecessor_counts[local_idx] += 1; } + + previous = Some(local_idx); } } - let mut scheduled = FixedBitSet::with_capacity(cmd_count); - let mut scheduled_count = 0; + self.remaining_predecessors + .clone_from(&self.predecessor_counts); + self.ready.clear(); + + for local_idx in self + .remaining_predecessors + .iter() + .enumerate() + .filter_map(|(idx, remaining)| (*remaining == 0).then_some(idx)) + { + self.ready.insert((0, Reverse(local_idx))); + } + + self.reordered.clear(); + self.reordered.reserve(cmd_count); - while scheduled_count < cmd_count { - let mut best_idx = scheduled_count; - let mut best_overlap = self.interdependent[best_idx].len(); + while let Some((_, Reverse(local_idx))) = self.ready.pop_last() { + self.reordered.push(self.cmds[local_idx]); - for idx in (scheduled_count + 1)..cmd_count { - let mut overlap = 0; + for &successor_idx in &self.successors[local_idx] { + let remaining = &mut self.remaining_predecessors[successor_idx]; - for &dep_local in &self.interdependent[idx] { - if scheduled.contains(dep_local) { - overlap += 1; - } else { - break; - } - } + debug_assert!(*remaining > 0); + + *remaining -= 1; - if overlap > best_overlap { - best_overlap = overlap; - best_idx = idx; + if *remaining == 0 { + self.ready.insert(( + self.predecessor_counts[successor_idx], + Reverse(successor_idx), + )); } } - - scheduled.insert(best_idx); - self.cmds.swap(scheduled_count, best_idx); - self.interdependent.swap(scheduled_count, best_idx); - scheduled_count += 1; } + + assert_eq!( + self.reordered.len(), + cmd_count, + "command dependency cycle detected" + ); + + self.cmds.clear(); + self.cmds.append(&mut self.reordered); } } @@ -2020,6 +2081,8 @@ impl Submission { &mut self, cmd_buf: &CommandBuffer, ) -> Result<(), DriverError> { + let mut ownership = RecordingOwnership::default(); + thread_local! { static SCHEDULE: RefCell = Default::default(); } @@ -2030,9 +2093,7 @@ impl Submission { .update(&self.graph, self.graph.cmds.len()); schedule.cmds.clear(); schedule.cmds.extend(0..self.graph.cmds.len()); - self.track_pending_transfers(schedule, cmd_buf.info.queue_family_index); - self.queue_ownership_release_groups - .extend(self.collect_queue_ownership_release_groups()); + self.track_pending_transfers(schedule, cmd_buf.info.queue_family_index, &mut ownership); }); self.record_cmd_indices(cmd_buf, 0..self.graph.cmds.len())?; @@ -2688,69 +2749,6 @@ impl Submission { Ok(()) } - /// Collects release groups from pending ownership transfers. - fn collect_queue_ownership_release_groups(&self) -> Box<[QueueOwnershipReleaseGroup]> { - let mut release_groups = Vec::::new(); - - thread_local! { - static TRANSFER_GROUP_INDICES: RefCell> = Default::default(); - } - - TRANSFER_GROUP_INDICES.with_borrow_mut(|tls| { - tls.clear(); - - if let Some(pending_buffer_transfer_nodes) = &self.pending_buffer_transfer_nodes { - for (_, buffer, transfers) in pending_buffer_transfer_nodes.iter() { - for transfer in transfers.iter().copied() { - let key = (transfer.src_queue_family_index, transfer.src_queue_index); - let buffer_transfer = (buffer, transfer.range); - - if let Some(&group_idx) = tls.get(&key) { - release_groups[group_idx].buffers.push(buffer_transfer); - } else { - let group_idx = release_groups.len(); - - release_groups.push(QueueOwnershipReleaseGroup { - src_queue_family_index: key.0, - src_queue_index: key.1, - buffers: vec![buffer_transfer], - images: Vec::new(), - }); - - tls.insert(key, group_idx); - } - } - } - } - - if let Some(pending_image_transfer_nodes) = &self.pending_image_transfer_nodes { - for (_, image, transfers) in pending_image_transfer_nodes.iter() { - for transfer in transfers.iter().copied() { - let key = (transfer.src_queue_family_index, transfer.src_queue_index); - let image_transfer = (image, transfer.layout, transfer.range); - - if let Some(&group_idx) = tls.get(&key) { - release_groups[group_idx].images.push(image_transfer); - } else { - let group_idx = release_groups.len(); - - release_groups.push(QueueOwnershipReleaseGroup { - src_queue_family_index: key.0, - src_queue_index: key.1, - buffers: Vec::new(), - images: vec![image_transfer], - }); - - tls.insert(key, group_idx); - } - } - } - } - }); - - release_groups.into_boxed_slice() - } - /// Returns `true` when this submission contains no more commands to record. pub fn is_empty(&self) -> bool { self.graph.cmds.is_empty() @@ -3900,16 +3898,21 @@ impl Submission { resource_pool: &mut P, cmd_buf: &CommandBuffer, node: AnyNode, + ownership: &mut RecordingOwnership, ) -> Result<(), DriverError> where P: SubmissionPool, { match node { AnyNode::AccelerationStructure(node) => { - self.record_resource_impl(resource_pool, cmd_buf, node) + self.record_resource_impl(resource_pool, cmd_buf, node, ownership) + } + AnyNode::Buffer(node) => { + self.record_resource_impl(resource_pool, cmd_buf, node, ownership) + } + AnyNode::Image(node) => { + self.record_resource_impl(resource_pool, cmd_buf, node, ownership) } - AnyNode::Buffer(node) => self.record_resource_impl(resource_pool, cmd_buf, node), - AnyNode::Image(node) => self.record_resource_impl(resource_pool, cmd_buf, node), } } @@ -3919,6 +3922,7 @@ impl Submission { resource_pool: &mut P, cmd_buf: &CommandBuffer, selection: RecordSelection<'a>, + ownership: &mut RecordingOwnership, ) -> Result<(), DriverError> where P: SubmissionPool, @@ -3926,22 +3930,24 @@ impl Submission { let _ = CommandBufferDebugLabel::begin(cmd_buf, "graph submission"); match selection { - RecordSelection::All => self.record_impl(resource_pool, cmd_buf), + RecordSelection::All => self.record_impl(resource_pool, cmd_buf, ownership), RecordSelection::Dependencies(node) => match node { AnyNode::AccelerationStructure(node) => { - self.record_resource_dependencies_impl(resource_pool, cmd_buf, node) + self.record_resource_dependencies_impl(resource_pool, cmd_buf, node, ownership) } AnyNode::Buffer(node) => { - self.record_resource_dependencies_impl(resource_pool, cmd_buf, node) + self.record_resource_dependencies_impl(resource_pool, cmd_buf, node, ownership) } AnyNode::Image(node) => { - self.record_resource_dependencies_impl(resource_pool, cmd_buf, node) + self.record_resource_dependencies_impl(resource_pool, cmd_buf, node, ownership) } }, - RecordSelection::Node(node) => self.record_node(resource_pool, cmd_buf, node), + RecordSelection::Node(node) => { + self.record_node(resource_pool, cmd_buf, node, ownership) + } RecordSelection::Nodes(nodes) => { for &node in nodes { - self.record_node(resource_pool, cmd_buf, node)?; + self.record_node(resource_pool, cmd_buf, node, ownership)?; } Ok(()) @@ -4577,6 +4583,7 @@ impl Submission { cmd_buf: &CommandBuffer, node_idx: usize, end_cmd_idx: usize, + ownership: &mut RecordingOwnership, ) -> Result<(), DriverError> where P: SubmissionPool, @@ -4590,67 +4597,40 @@ impl Submission { schedule.cmds.clear(); self.schedule_node_cmds(node_idx, end_cmd_idx, schedule); - self.record_scheduled_cmds(pool, cmd_buf, schedule, end_cmd_idx) + self.record_scheduled_cmds(pool, cmd_buf, schedule, end_cmd_idx, ownership) }) } - fn track_pending_transfers(&mut self, schedule: &Schedule, queue_family_index: u32) { - #[derive(Debug)] - struct BufferRangeSet { - buffer: vk::Buffer, - range_keys: HashSet, - } - - #[derive(Default)] - struct PendingTransferScratch { - buffers: HashMap, - images: HashMap, - } - - thread_local! { - static PENDING_TRANSFER: RefCell = Default::default(); - } + fn track_pending_transfers( + &mut self, + schedule: &Schedule, + queue_family_index: u32, + ownership: &mut RecordingOwnership, + ) { + let resource_count = self.graph.resources.len(); - PENDING_TRANSFER.with_borrow_mut(|tls| { - tls.buffers.clear(); - tls.images.clear(); + for cmd_idx in schedule.cmds.iter().copied() { + let cmd = &self.graph.cmds[cmd_idx]; - for cmd_idx in schedule.cmds.iter().copied() { - let cmd = &self.graph.cmds[cmd_idx]; + for (node_idx, accesses) in cmd.execs.iter().flat_map(|exec| exec.accesses.iter()) { + if let Some(buffer) = self.graph.resources[node_idx].as_buffer() { + if buffer.info.sharing_mode == vk::SharingMode::CONCURRENT { + continue; + } - for (node_idx, accesses) in cmd.execs.iter().flat_map(|exec| exec.accesses.iter()) { - if let Some(buffer) = self.graph.resources[node_idx].as_buffer() { - if buffer.info.sharing_mode == vk::SharingMode::CONCURRENT { + for access in accesses.iter() { + let SubresourceRange::Buffer(access_range) = access.subresource else { continue; - } - - let transfer = - tls.buffers - .entry(node_idx) - .or_insert_with(|| BufferRangeSet { - buffer: buffer.handle, - range_keys: Default::default(), - }); - - for access in accesses.iter() { - let SubresourceRange::Buffer(access_range) = access.subresource else { - continue; - }; - - let access_range = BufferSubresourceRange { - start: access_range.start, - end: if access_range.end == vk::WHOLE_SIZE { - buffer.info.size - } else { - access_range.end - }, - }; - let access_key = BufferSubresourceRangeKey::from_range(access_range); + }; + let unclaimed = ownership + .claim_buffer(node_idx, access_range.resolve_whole(buffer.info.size)); - if !transfer.range_keys.insert(access_key) { - continue; - } + self.exclusive_buffer_ranges + .entry(node_idx) + .or_default() + .extend(unclaimed.iter().copied()); + for access_range in unclaimed { for (subresource, sharing) in buffer.sync_info_with_sharing_range(access_range) { @@ -4658,58 +4638,59 @@ impl Submission { else { continue; }; - let Some((src_queue_family_index, src_queue_index)) = exclusive_transfer_source(sharing, queue_family_index) else { continue; }; + let transfer = BufferQueueOwnershipTransfer { + src_queue_family_index, + dst_queue_family_index: queue_family_index, + range, + }; + queue_ownership_release_group( + &mut self.queue_ownership_release_groups, + src_queue_family_index, + src_queue_index, + ) + .buffers + .push((buffer.handle, range)); self.pending_buffer_transfer_nodes .get_or_insert_with(|| { - PendingTransferNodes::new(self.graph.resources.len()) + PendingTransferNodes::new(resource_count) }) - .push_transfer( - node_idx, - transfer.buffer, - BufferQueueOwnershipTransfer { - src_queue_family_index, - src_queue_index, - dst_queue_family_index: queue_family_index, - range, - }, - ); + .push_transfer(node_idx, buffer.handle, transfer); } } - - continue; - } - - let Some(image) = self.graph.resources[node_idx].as_image() else { - continue; - }; - - if image.info.sharing_mode == vk::SharingMode::CONCURRENT { - continue; } - let transfer = tls.images.entry(node_idx).or_insert_with(|| ImageRangeSet { - image: image.handle, - range_keys: Default::default(), - }); + continue; + } - for access in accesses.iter() { - let SubresourceRange::Image(access_range) = access.subresource else { - continue; - }; + let Some(image) = self.graph.resources[node_idx].as_image() else { + continue; + }; + if image.info.sharing_mode == vk::SharingMode::CONCURRENT { + continue; + } - let access_range = image.info.resolve_subresource_counts(access_range); - let access_key = ImageSubresourceRangeKey::from_range(access_range); + for access in accesses.iter() { + let SubresourceRange::Image(access_range) = access.subresource else { + continue; + }; + let unclaimed = ownership.claim_image( + node_idx, + image.info, + image.info.resolve_subresource_counts(access_range), + ); - if !transfer.range_keys.insert(access_key) { - continue; - } + self.exclusive_image_ranges + .entry(node_idx) + .or_default() + .extend(unclaimed.iter().copied()); + for access_range in unclaimed { for (subresource, sharing) in image.sync_info_with_sharing_range(access_range) { @@ -4719,61 +4700,35 @@ impl Submission { ) else { continue; }; - - let layout = subresource.layout.unwrap_or(vk::ImageLayout::UNDEFINED); - let Some((src_queue_family_index, src_queue_index)) = exclusive_transfer_source(sharing, queue_family_index) else { continue; }; + let layout = subresource.layout.unwrap_or(vk::ImageLayout::UNDEFINED); + let transfer = ImageQueueOwnershipTransfer { + src_queue_family_index, + src_queue_index, + dst_queue_family_index: queue_family_index, + layout, + range, + }; + queue_ownership_release_group( + &mut self.queue_ownership_release_groups, + src_queue_family_index, + src_queue_index, + ) + .images + .push((image.handle, layout, range)); self.pending_image_transfer_nodes - .get_or_insert_with(|| { - PendingTransferNodes::new(self.graph.resources.len()) - }) - .push_transfer( - node_idx, - transfer.image, - ImageQueueOwnershipTransfer { - src_queue_family_index, - src_queue_index, - dst_queue_family_index: queue_family_index, - layout, - range, - }, - ); + .get_or_insert_with(|| PendingTransferNodes::new(resource_count)) + .push_transfer(node_idx, image.handle, transfer); } } } } - - for (node_idx, transfer) in tls.buffers.iter() { - self.exclusive_buffer_ranges - .entry(*node_idx) - .or_default() - .extend( - transfer - .range_keys - .iter() - .copied() - .map(BufferSubresourceRangeKey::into_range), - ); - } - - for (node_idx, transfer) in tls.images.iter() { - self.exclusive_image_ranges - .entry(*node_idx) - .or_default() - .extend( - transfer - .range_keys - .iter() - .copied() - .map(ImageSubresourceRangeKey::into_range), - ); - } - }); + } } fn record_cmd_indices( @@ -4984,6 +4939,7 @@ impl Submission { cmd_buf: &CommandBuffer, schedule: &mut Schedule, end_cmd_idx: usize, + ownership: &mut RecordingOwnership, ) -> Result<(), DriverError> where P: SubmissionPool, @@ -5006,9 +4962,7 @@ impl Submission { schedule.reorder_cmds(end_cmd_idx); self.merge_scheduled_cmds(&mut schedule.cmds); self.lease_scheduled_resources(pool, &schedule.cmds)?; - self.track_pending_transfers(schedule, cmd_buf.info.queue_family_index); - self.queue_ownership_release_groups - .extend(self.collect_queue_ownership_release_groups()); + self.track_pending_transfers(schedule, cmd_buf.info.queue_family_index, ownership); let has_pending_timestamp_queries = self .graph @@ -5193,92 +5147,32 @@ impl Submission { /// given node. #[profiling::function] fn schedule_node_cmds(&self, node_idx: usize, end_cmd_idx: usize, schedule: &mut Schedule) { - #[derive(Default)] - struct ScheduleSearchScratch { - pending_nodes: VecDeque<(usize, usize)>, - resolved_nodes: FixedBitSet, - scheduled_cmds: FixedBitSet, - } - - thread_local! { - static SCHEDULE_SEARCH: RefCell = Default::default(); - } - - SCHEDULE_SEARCH.with_borrow_mut(|tls| { - tls.scheduled_cmds.clear(); - tls.scheduled_cmds.grow(end_cmd_idx); - - tls.resolved_nodes.clear(); - tls.resolved_nodes.grow(self.graph.resources.len()); - - debug_assert!(tls.pending_nodes.is_empty()); - - trace!("scheduling node {node_idx}"); - - tls.resolved_nodes.insert(node_idx); - - // Schedule the first set of cmds for the node we're trying to resolve - for cmd_idx in schedule - .access_index - .prior_cmds_for_node(node_idx, end_cmd_idx) - { - trace!( - " cmd [{cmd_idx}: {}] is dependent", - self.graph.cmds[cmd_idx].name() + trace!("scheduling node {node_idx}"); + schedule.schedule_required_node_prefixes([(node_idx, end_cmd_idx)]); + + if log_enabled!(Debug) { + if !schedule.cmds.is_empty() { + debug!( + "schedule: {}", + schedule + .cmds + .iter() + .copied() + .map(|idx| format!("[{}: {}]", idx, self.graph.cmds[idx].name())) + .collect::>() + .join(", ") ); - - debug_assert!(!tls.scheduled_cmds.contains(cmd_idx)); - - tls.scheduled_cmds.insert(cmd_idx); - schedule.cmds.push(cmd_idx); - - for node_idx in schedule.access_index.read_nodes_for_cmd(cmd_idx) { - trace!(" node {node_idx} is dependent"); - - if !tls.resolved_nodes.put(node_idx) { - tls.pending_nodes.push_back((node_idx, cmd_idx)); - } - } } - trace!("secondary cmds below"); - - // Now schedule all nodes that are required, going through the tree to find them - while let Some((node_idx, cmd_idx)) = tls.pending_nodes.pop_front() { - trace!(" node {node_idx} is dependent"); - - for dep_cmd_idx in schedule - .access_index - .prior_cmds_for_node(node_idx, cmd_idx + 1) - { - if !tls.scheduled_cmds.put(dep_cmd_idx) { - schedule.cmds.push(dep_cmd_idx); + if log_enabled!(Trace) { + let unscheduled = (0..end_cmd_idx) + .filter(|&cmd_idx| !schedule.node_schedule.selected_cmds.contains(cmd_idx)) + .collect::>(); - trace!( - " cmd [{dep_cmd_idx}: {}] is dependent", - self.graph.cmds[dep_cmd_idx].name() - ); - - for node_idx in schedule.access_index.read_nodes_for_cmd(dep_cmd_idx) { - trace!(" node {node_idx} is dependent"); - - if !tls.resolved_nodes.put(node_idx) { - tls.pending_nodes.push_back((node_idx, dep_cmd_idx)); - } - } - } - } - } - - schedule.cmds.sort_unstable(); - - if log_enabled!(Debug) { - if !schedule.cmds.is_empty() { - // These are the indexes of the cmds this thread is about to resolve - debug!( - "schedule: {}", - schedule - .cmds + if !unscheduled.is_empty() { + trace!( + "delaying: {}", + unscheduled .iter() .copied() .map(|idx| format!("[{}: {}]", idx, self.graph.cmds[idx].name())) @@ -5287,45 +5181,21 @@ impl Submission { ); } - if log_enabled!(Trace) { - let unscheduled = (0..end_cmd_idx) - .filter(|&cmd_idx| !tls.scheduled_cmds.contains(cmd_idx)) - .collect::>(); - - if !unscheduled.is_empty() { - // These cmds are within the range of cmds we thought we had to do - // right now, but it turns out that nothing in "schedule" relies on them - trace!( - "delaying: {}", - unscheduled - .iter() - .copied() - .map(|idx| format!("[{}: {}]", idx, self.graph.cmds[idx].name())) - .collect::>() - .join(", ") - ); - } - - if end_cmd_idx < self.graph.cmds.len() { - // These cmds existing on the graph but are not being considered right - // now because we've been told to stop work at the "end_cmd_idx" point - trace!( - "ignoring: {}", - self.graph.cmds[end_cmd_idx..] - .iter() - .enumerate() - .map(|(idx, cmd)| format!( - "[{}: {}]", - idx + end_cmd_idx, - cmd.name() - )) - .collect::>() - .join(", ") - ); - } + if end_cmd_idx < self.graph.cmds.len() { + trace!( + "ignoring: {}", + self.graph.cmds[end_cmd_idx..] + .iter() + .enumerate() + .map(|(idx, cmd)| { + format!("[{}: {}]", idx + end_cmd_idx, cmd.name()) + }) + .collect::>() + .join(", ") + ); } } - }); + } } fn set_scissor(cmd_buf: &CommandBuffer, x: i32, y: i32, width: u32, height: u32) { @@ -5450,9 +5320,16 @@ impl Submission { P: SubmissionPool, Cb: AsRef, { - self.record_selection_impl(resource_pool, cmd_buf.as_ref(), selection.into())?; + let mut ownership = RecordingOwnership::default(); + self.record_selection_impl( + resource_pool, + cmd_buf.as_ref(), + selection.into(), + &mut ownership, + )?; Ok(Recording { + ownership, cmd_buf, resource_pool, submission: self, @@ -5460,7 +5337,12 @@ impl Submission { } #[profiling::function] - fn record_impl

(&mut self, pool: &mut P, cmd_buf: &CommandBuffer) -> Result<(), DriverError> + fn record_impl

( + &mut self, + pool: &mut P, + cmd_buf: &CommandBuffer, + ownership: &mut RecordingOwnership, + ) -> Result<(), DriverError> where P: SubmissionPool, { @@ -5479,7 +5361,7 @@ impl Submission { schedule.cmds.clear(); schedule.cmds.extend(0..self.graph.cmds.len()); - self.record_scheduled_cmds(pool, cmd_buf, schedule, self.graph.cmds.len()) + self.record_scheduled_cmds(pool, cmd_buf, schedule, self.graph.cmds.len(), ownership) }) } @@ -5489,6 +5371,7 @@ impl Submission { pool: &mut P, cmd_buf: &CommandBuffer, resource_node: impl Node, + ownership: &mut RecordingOwnership, ) -> Result<(), DriverError> where P: SubmissionPool, @@ -5507,14 +5390,8 @@ impl Submission { SCHEDULE.with_borrow_mut(|tls| { tls.access_index.update(&self.graph, end_pass_idx + 1); - tls.cmds.clear(); - schedule_dependency_cmds_before_target_access( - &tls.access_index, - node_idx, - end_pass_idx, - &mut tls.cmds, - ); - self.record_scheduled_cmds(pool, cmd_buf, tls, end_pass_idx) + schedule_dependency_cmds_before_target_access(node_idx, end_pass_idx, tls); + self.record_scheduled_cmds(pool, cmd_buf, tls, end_pass_idx, ownership) })?; } @@ -5527,6 +5404,7 @@ impl Submission { pool: &mut P, cmd_buf: &CommandBuffer, resource_node: impl Node, + ownership: &mut RecordingOwnership, ) -> Result<(), DriverError> where P: SubmissionPool, @@ -5542,7 +5420,7 @@ impl Submission { } let end_pass_idx = self.graph.cmds.len(); - self.record_node_cmds(pool, cmd_buf, node_idx, end_pass_idx) + self.record_node_cmds(pool, cmd_buf, node_idx, end_pass_idx, ownership) } #[profiling::function] @@ -6182,7 +6060,10 @@ struct TimestampQueryPoolInner { #[doc(hidden)] pub mod bench { - use super::{CommandAccessIndex, Schedule}; + use { + super::{CommandAccessIndex, Schedule}, + crate::Graph, + }; /// Synthetic workload description for scheduler benchmarks. #[derive(Clone, Copy, Debug)] @@ -6291,6 +6172,61 @@ pub mod bench { } } + /// Builds a scheduler benchmark from a graph, optionally repeating its disconnected + /// topology with independently remapped command and resource indices. + pub fn from_graph(graph: &Graph, repeat_count: usize) -> Self { + assert!(repeat_count > 0, "repeat_count must be greater than zero"); + + let base_cmd_count = graph.cmds.len(); + let base_resource_count = graph.resources.len(); + assert!(base_cmd_count > 0, "graph must contain commands"); + assert!(base_resource_count > 0, "graph must contain resources"); + + let mut base = CommandAccessIndex::default(); + base.update_from_cmds(&graph.cmds, base_resource_count); + + let cmd_count = base_cmd_count * repeat_count; + let resource_count = base_resource_count * repeat_count; + let mut cmds_by_node = Vec::with_capacity(resource_count); + let mut accessed_nodes_by_cmd = Vec::with_capacity(cmd_count); + + for copy_idx in 0..repeat_count { + let cmd_offset = copy_idx * base_cmd_count; + let resource_offset = copy_idx * base_resource_count; + + cmds_by_node.extend(base.cmds_by_node.iter().map(|cmds| { + cmds.iter() + .map(|cmd_idx| cmd_offset + cmd_idx) + .collect::>() + })); + accessed_nodes_by_cmd.extend(base.accessed_nodes_by_cmd.iter().map(|nodes| { + nodes + .iter() + .map(|node_idx| resource_offset + node_idx) + .collect::>() + })); + } + + let cmds = (0..cmd_count).collect::>(); + Self { + schedule: Schedule { + access_index: CommandAccessIndex { + cmds_by_node, + accessed_nodes_by_cmd, + }, + cmds: cmds.clone(), + ..Default::default() + }, + original_cmds: cmds, + end_cmd_idx: cmd_count, + } + } + + /// Returns the number of commands reordered by each benchmark iteration. + pub fn cmd_count(&self) -> usize { + self.end_cmd_idx + } + /// Restores the original schedule, reorders it once, and returns a checksum. pub fn reorder_once(&mut self) -> u64 { self.schedule.cmds.clear(); @@ -6328,10 +6264,7 @@ pub mod bench { #[doc(hidden)] pub mod fuzz { - use { - super::{CommandAccessIndex, Schedule}, - fixedbitset::FixedBitSet, - }; + use super::{CommandAccessIndex, Schedule}; #[derive(Clone, Copy, Debug)] pub struct ResourceAccess { @@ -6375,13 +6308,13 @@ pub mod fuzz { repeat.reorder_cmds(cmd_count); assert_eq!(reordered, repeat.cmds, "reordering is not deterministic"); - assert_hazard_order_preserved(&reordered, &normalized_accesses); - let expected = reference_reorder(access_index, cmd_count); assert_eq!( reordered, expected, "reordering diverged from reference implementation" ); + + assert_hazard_order_preserved(&reordered, &normalized_accesses); } fn build_access_index( @@ -6456,87 +6389,58 @@ pub mod fuzz { } fn reference_reorder(access_index: CommandAccessIndex, cmd_count: usize) -> Vec { - let mut cmds = (0..cmd_count).collect::>(); - if cmds.len() < 3 { - return cmds; - } - - let mut interdependent = vec![Vec::new(); cmd_count]; - let mut local_of_global = vec![usize::MAX; cmd_count]; - let mut seen_deps = FixedBitSet::with_capacity(cmd_count); - let mut scheduled = FixedBitSet::with_capacity(cmd_count); - - for (local_idx, &cmd_idx) in cmds.iter().enumerate() { - local_of_global[cmd_idx] = local_idx; + if cmd_count < 3 { + return (0..cmd_count).collect(); } - for (local_idx, &cmd_idx) in cmds.iter().enumerate() { - for dep_cmd_idx in access_index.prior_read_dependency_cmds(cmd_idx, cmd_count) { - let dep_local_idx = local_of_global[dep_cmd_idx]; - if dep_local_idx == usize::MAX || dep_local_idx == local_idx { - continue; - } - - if !seen_deps.put(dep_local_idx) { - interdependent[local_idx].push(dep_local_idx); - } - } - - for dep_cmd_idx in access_index.prior_read_dependency_cmds(cmd_idx, cmd_count) { - let dep_local_idx = local_of_global[dep_cmd_idx]; - if dep_local_idx != usize::MAX && dep_local_idx != local_idx { - seen_deps.set(dep_local_idx, false); - } + let mut predecessors = vec![Vec::new(); cmd_count]; + for resource_cmds in &access_index.cmds_by_node { + for pair in resource_cmds.windows(2) { + predecessors[pair[1]].push(pair[0]); } } - let mut scheduled_count = 0; - while scheduled_count < cmd_count { - let mut best_idx = scheduled_count; - let mut best_overlap = interdependent[best_idx].len(); - - for (idx, dep_cmds) in interdependent[..cmd_count] - .iter() - .enumerate() - .skip(scheduled_count + 1) - { - let mut overlap = 0; - - for &dep_local in dep_cmds { - if scheduled.contains(dep_local) { - overlap += 1; - } else { - break; - } + let mut scheduled = vec![false; cmd_count]; + let mut reordered = Vec::with_capacity(cmd_count); + while reordered.len() < cmd_count { + let mut best = None; + for cmd_idx in 0..cmd_count { + if scheduled[cmd_idx] + || !predecessors[cmd_idx] + .iter() + .all(|&predecessor| scheduled[predecessor]) + { + continue; } - if overlap > best_overlap { - best_overlap = overlap; - best_idx = idx; + let score = predecessors[cmd_idx].len(); + if best.is_none_or(|(best_score, best_idx)| { + score > best_score || (score == best_score && cmd_idx < best_idx) + }) { + best = Some((score, cmd_idx)); } } - scheduled.insert(best_idx); - cmds.swap(scheduled_count, best_idx); - interdependent.swap(scheduled_count, best_idx); - scheduled_count += 1; + let (_, best_idx) = best.expect("command dependency cycle detected"); + scheduled[best_idx] = true; + reordered.push(best_idx); } - cmds + reordered } } #[cfg(test)] -mod tests { +mod test { use super::{ BufferQueueOwnershipTransfer, CommandAccessIndex, CommandData, ExternalRenderPassAccessHistory, ImageQueueOwnershipTransfer, NodeIndex, PipelineStageAccessFlags, QueueSubmitInfo, RecordSelection, RecordedSubmission, - RecordedSubmissionState, Schedule, SemaphoreSubmitInfo, Submission, SubresourceAccess, - SubresourceRange, check_queue_submit_args, fuzz, + RecordedSubmissionState, RecordingOwnership, Schedule, SemaphoreSubmitInfo, Submission, + SubresourceAccess, SubresourceRange, check_queue_submit_args, fuzz, }; use crate::{ - Attachment, DepthStencilAttachment, Execution, Graph, LoadOp, Node, StoreOp, + AnyResource, Attachment, DepthStencilAttachment, Execution, Graph, LoadOp, Node, StoreOp, TimestampQuery, driver::{ DriverError, SharingMode, @@ -6614,7 +6518,6 @@ mod tests { transfers.sort_unstable_by_key(|transfer| { ( transfer.src_queue_family_index, - transfer.src_queue_index, transfer.dst_queue_family_index, transfer.range.start, transfer.range.end, @@ -6638,6 +6541,17 @@ mod tests { .find_map(|(idx, handle, transfers)| (idx == node_idx).then_some((handle, transfers))) } + fn simulate_partial_transfer_discovery( + submission: &mut Submission, + schedule: &Schedule, + queue_family_index: u32, + ownership: &mut RecordingOwnership, + ) { + submission.track_pending_transfers(schedule, queue_family_index, ownership); + submission.pending_buffer_transfer_nodes = None; + submission.pending_image_transfer_nodes = None; + } + fn pending_timestamp_query_pool(query: TimestampQuery) -> super::TimestampQueryPool { #[cfg(feature = "checked")] { @@ -6850,13 +6764,11 @@ mod tests { dst_queue_family_index: 0, range: consumed, src_queue_family_index: 1, - src_queue_index: 0, }, BufferQueueOwnershipTransfer { dst_queue_family_index: 0, range: kept, src_queue_family_index: 1, - src_queue_index: 0, }, ]; @@ -6899,6 +6811,39 @@ mod tests { assert!(super::image_subresource_range_eq(pending[0].range, kept)); } + #[test] + fn recording_ownership_only_returns_unclaimed_buffer_ranges() { + let mut ownership = RecordingOwnership::default(); + let first = BufferSubresourceRange { start: 0, end: 8 }; + let overlap = BufferSubresourceRange { start: 4, end: 12 }; + + assert_eq!(ownership.claim_buffer(0, first).as_slice(), &[first]); + assert_eq!( + ownership.claim_buffer(0, overlap).as_slice(), + &[BufferSubresourceRange { start: 8, end: 12 }] + ); + assert!(ownership.claim_buffer(0, overlap).is_empty()); + } + + #[test] + fn recording_ownership_only_returns_unclaimed_image_subresources() { + let mut ownership = RecordingOwnership::default(); + let info = + ImageInfo::image_2d_array(1, 1, 3, vk::Format::R8_UINT, vk::ImageUsageFlags::SAMPLED); + let first = color_subresource_range(0..2, 0..1); + let overlap = color_subresource_range(1..3, 0..1); + let remaining = color_subresource_range(2..3, 0..1); + + let claimed = ownership.claim_image(0, info, first); + assert_eq!(claimed.len(), 1); + assert!(super::image_subresource_range_eq(claimed[0], first)); + + let claimed = ownership.claim_image(0, info, overlap); + assert_eq!(claimed.len(), 1); + assert!(super::image_subresource_range_eq(claimed[0], remaining)); + assert!(ownership.claim_image(0, info, overlap).is_empty()); + } + #[test] fn dependency_selection_schedules_inputs_to_first_target_access() { let access_index = CommandAccessIndex { @@ -6909,11 +6854,68 @@ mod tests { cmds_by_node: vec![vec![0, 1], vec![1]], accessed_nodes_by_cmd: vec![vec![0], vec![0, 1]], }; - let mut schedule = Vec::new(); + let mut schedule = Schedule { + access_index, + ..Default::default() + }; + + super::schedule_dependency_cmds_before_target_access(1, 1, &mut schedule); + + assert_eq!(schedule.cmds, vec![0]); + } + + #[test] + fn dependency_selection_revisits_node_at_later_boundary() { + let access_index = CommandAccessIndex { + /* + A is first discovered through cmd 0, then rediscovered through cmd 2. The later + boundary must extend A's selected prefix to include cmd 1. + + cmd 0: A, B + cmd 1: A + cmd 2: A, C + cmd 3: B, C, T + */ + cmds_by_node: vec![vec![0, 1, 2], vec![0, 3], vec![2, 3], vec![3]], + accessed_nodes_by_cmd: vec![vec![0, 1], vec![0], vec![0, 2], vec![1, 2, 3]], + }; + let mut schedule = Schedule { + access_index, + ..Default::default() + }; + + super::schedule_dependency_cmds_before_target_access(3, 3, &mut schedule); + + assert_eq!(schedule.cmds, vec![0, 1, 2]); + } + + #[test] + fn node_selection_revisits_node_at_later_boundary() { + let mut graph = Graph::new(); + for _ in 0..4 { + graph.bind_stream_arg_resource(AnyResource::BufferArg(BufferInfo::device_mem( + 1, + vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST, + ))); + } + graph.cmds = vec![ + command_with_accesses(&[(0, AccessType::TransferRead), (1, AccessType::TransferRead)]), + command_with_accesses(&[(0, AccessType::TransferRead)]), + command_with_accesses(&[(0, AccessType::TransferRead), (2, AccessType::TransferRead)]), + command_with_accesses(&[ + (1, AccessType::TransferRead), + (2, AccessType::TransferRead), + (3, AccessType::TransferWrite), + ]), + ]; + + let submission = Submission::new(graph); + let mut schedule = Schedule::default(); + schedule.access_index.update(&submission.graph, 4); - super::schedule_dependency_cmds_before_target_access(&access_index, 1, 1, &mut schedule); + submission.schedule_node_cmds(1, 4, &mut schedule); - assert_eq!(schedule, vec![0]); + assert_eq!(schedule.cmds, vec![0, 1, 2, 3]); } #[cfg(test)] @@ -7280,11 +7282,14 @@ mod tests { cmds_by_node: vec![vec![0, 1], vec![1]], accessed_nodes_by_cmd: vec![vec![0], vec![0, 0, 1]], }; - let mut schedule = Vec::new(); + let mut schedule = Schedule { + access_index, + ..Default::default() + }; - super::schedule_dependency_cmds_before_target_access(&access_index, 1, 1, &mut schedule); + super::schedule_dependency_cmds_before_target_access(1, 1, &mut schedule); - assert_eq!(schedule, vec![0]); + assert_eq!(schedule.cmds, vec![0]); } #[test] @@ -7318,7 +7323,7 @@ mod tests { } #[test] - fn reorder_scheduled_cmds_matches_original_seed_choice() { + fn reorder_scheduled_cmds_groups_ready_dependency_chain() { let mut schedule = schedule_with_access_index( &[0, 1, 2, 3], &[&[0, 1], &[1, 2], &[1, 3]], @@ -7332,7 +7337,7 @@ mod tests { #[test] fn queue_ownership_release_groups_group_by_source_queue() { - use super::{ImageQueueOwnershipTransfer, image_subresource_range_eq}; + use super::{image_subresource_range_eq, queue_ownership_release_group}; let mut submission = Submission::new(Graph::new()); let image = vk::Image::null(); @@ -7352,45 +7357,17 @@ mod tests { level_count: 1, }; - let pending_image_transfer_nodes = submission - .pending_image_transfer_nodes - .get_or_insert_with(|| super::PendingTransferNodes::new(1)); - pending_image_transfer_nodes.push_transfer( - 0, - image, - ImageQueueOwnershipTransfer { - src_queue_family_index: 1, - src_queue_index: 2, - dst_queue_family_index: 3, - layout: vk::ImageLayout::GENERAL, - range: range_a, - }, - ); - pending_image_transfer_nodes.push_transfer( - 0, - image, - ImageQueueOwnershipTransfer { - src_queue_family_index: 1, - src_queue_index: 2, - dst_queue_family_index: 3, - layout: vk::ImageLayout::GENERAL, - range: range_b, - }, - ); - pending_image_transfer_nodes.push_transfer( - 0, - image, - ImageQueueOwnershipTransfer { - src_queue_family_index: 4, - src_queue_index: 5, - dst_queue_family_index: 3, - layout: vk::ImageLayout::GENERAL, - range: range_a, - }, - ); - - let groups = submission.collect_queue_ownership_release_groups(); - let mut groups = groups.into_vec(); + queue_ownership_release_group(&mut submission.queue_ownership_release_groups, 1, 2) + .images + .push((image, vk::ImageLayout::GENERAL, range_a)); + queue_ownership_release_group(&mut submission.queue_ownership_release_groups, 1, 2) + .images + .push((image, vk::ImageLayout::GENERAL, range_b)); + queue_ownership_release_group(&mut submission.queue_ownership_release_groups, 4, 5) + .images + .push((image, vk::ImageLayout::GENERAL, range_a)); + + let mut groups = submission.queue_ownership_release_groups; sort_queue_ownership_release_groups(&mut groups); assert_eq!(groups.len(), 2); @@ -7465,12 +7442,14 @@ mod tests { .end_cmd(); let mut submission = graph.finalize(); + let mut ownership = RecordingOwnership::default(); submission.track_pending_transfers( &Schedule { cmds: vec![0], ..Default::default() }, 3, + &mut ownership, ); let (handle, transfers) = pending_transfer_for_node( @@ -7541,12 +7520,14 @@ mod tests { .end_cmd(); let mut submission = graph.finalize(); + let mut ownership = RecordingOwnership::default(); submission.track_pending_transfers( &Schedule { cmds: vec![0], ..Default::default() }, 3, + &mut ownership, ); let (handle, transfers) = pending_transfer_for_node( @@ -7581,6 +7562,189 @@ mod tests { Ok(()) } + #[test] + #[ignore = "requires Vulkan device"] + fn repeated_partial_recording_does_not_duplicate_buffer_ownership_transfer() + -> Result<(), DriverError> { + let device = test_device()?; + let mut graph = Graph::new(); + let buffer = graph.bind_resource(Buffer::create( + &device, + BufferInfo::device_mem(16, vk::BufferUsageFlags::TRANSFER_DST), + )?); + let range = BufferSubresourceRange { start: 0, end: 16 }; + + graph + .resource(buffer) + .set_sharing_ranges(SharingMode::Exclusive(Some((1, 0))), &[range]); + graph + .begin_cmd() + .debug_name("touch shared range") + .subresource_access(buffer, range, AccessType::TransferWrite) + .record_cmd(|_| {}) + .end_cmd(); + + let mut submission = graph.finalize(); + let schedule = Schedule { + cmds: vec![0], + ..Default::default() + }; + let mut ownership = RecordingOwnership::default(); + + simulate_partial_transfer_discovery(&mut submission, &schedule, 3, &mut ownership); + simulate_partial_transfer_discovery(&mut submission, &schedule, 3, &mut ownership); + + let released_ranges = submission + .queue_ownership_release_groups + .iter() + .flat_map(|group| group.buffers.iter()) + .map(|(_, range)| *range) + .collect::>(); + assert_eq!(released_ranges, vec![range]); + assert_eq!( + submission.exclusive_buffer_ranges[&buffer.index()], + vec![range] + ); + + Ok(()) + } + + #[test] + #[ignore = "requires Vulkan device"] + fn partial_recording_transfers_only_unclaimed_buffer_overlap() -> Result<(), DriverError> { + let device = test_device()?; + let mut graph = Graph::new(); + let buffer = graph.bind_resource(Buffer::create( + &device, + BufferInfo::device_mem(12, vk::BufferUsageFlags::TRANSFER_DST), + )?); + let first = BufferSubresourceRange { start: 0, end: 8 }; + let second = BufferSubresourceRange { start: 4, end: 12 }; + + graph.resource(buffer).set_sharing_ranges( + SharingMode::Exclusive(Some((1, 0))), + &[BufferSubresourceRange { start: 0, end: 12 }], + ); + graph + .begin_cmd() + .debug_name("touch first overlapping range") + .subresource_access(buffer, first, AccessType::TransferWrite) + .record_cmd(|_| {}) + .end_cmd(); + graph + .begin_cmd() + .debug_name("touch second overlapping range") + .subresource_access(buffer, second, AccessType::TransferWrite) + .record_cmd(|_| {}) + .end_cmd(); + + let mut submission = graph.finalize(); + let mut ownership = RecordingOwnership::default(); + simulate_partial_transfer_discovery( + &mut submission, + &Schedule { + cmds: vec![0], + ..Default::default() + }, + 3, + &mut ownership, + ); + simulate_partial_transfer_discovery( + &mut submission, + &Schedule { + cmds: vec![1], + ..Default::default() + }, + 3, + &mut ownership, + ); + + let mut released_ranges = submission + .queue_ownership_release_groups + .iter() + .flat_map(|group| group.buffers.iter()) + .map(|(_, range)| *range) + .collect::>(); + released_ranges.sort_unstable_by_key(|range| (range.start, range.end)); + + assert!( + released_ranges + .windows(2) + .all(|ranges| ranges[0].end <= ranges[1].start), + "released ranges overlap: {released_ranges:?}" + ); + assert_eq!( + released_ranges + .iter() + .map(|range| range.end - range.start) + .sum::(), + 12 + ); + + Ok(()) + } + + #[test] + #[ignore = "requires Vulkan device"] + fn repeated_partial_recording_does_not_duplicate_image_ownership_transfer() + -> Result<(), DriverError> { + let device = test_device()?; + let mut graph = Graph::new(); + let image = graph.bind_resource(Image::create( + &device, + ImageInfo::image_2d_array(1, 1, 2, vk::Format::R8_UINT, vk::ImageUsageFlags::SAMPLED), + )?); + let range = color_subresource_range(0..2, 0..1); + + graph + .resource(image) + .set_sharing_ranges(SharingMode::Exclusive(Some((1, 0))), &[range]); + graph + .begin_cmd() + .debug_name("touch shared image range") + .subresource_access(image, range, AccessType::TransferWrite) + .record_cmd(|_| {}) + .end_cmd(); + + let mut submission = graph.finalize(); + let schedule = Schedule { + cmds: vec![0], + ..Default::default() + }; + let mut ownership = RecordingOwnership::default(); + + simulate_partial_transfer_discovery(&mut submission, &schedule, 3, &mut ownership); + let first_released_ranges = submission + .queue_ownership_release_groups + .iter() + .flat_map(|group| group.images.iter()) + .map(|(_, _, range)| *range) + .collect::>(); + simulate_partial_transfer_discovery(&mut submission, &schedule, 3, &mut ownership); + + let released_ranges = submission + .queue_ownership_release_groups + .iter() + .flat_map(|group| group.images.iter()) + .map(|(_, _, range)| *range) + .collect::>(); + assert_eq!(released_ranges.len(), first_released_ranges.len()); + assert!( + released_ranges + .iter() + .zip(&first_released_ranges) + .all(|(&lhs, &rhs)| super::image_subresource_range_eq(lhs, rhs)), + "released ranges changed: {first_released_ranges:?} -> {released_ranges:?}" + ); + assert_eq!(submission.exclusive_image_ranges[&image.index()].len(), 1); + assert!(super::image_subresource_range_eq( + submission.exclusive_image_ranges[&image.index()][0], + range + )); + + Ok(()) + } + #[test] #[ignore = "requires Vulkan device"] fn track_pending_transfers_keeps_exclusive_owner_without_known_layout() @@ -7609,12 +7773,14 @@ mod tests { .end_cmd(); let mut submission = graph.finalize(); + let mut ownership = RecordingOwnership::default(); submission.track_pending_transfers( &Schedule { cmds: vec![0], ..Default::default() }, 3, + &mut ownership, ); let (handle, transfers) = pending_transfer_for_node( @@ -7788,6 +7954,55 @@ mod tests { assert_eq!(schedule.cmds, vec![0, 1, 2, 3, 4]); } + #[test] + fn reorder_scheduled_cmds_preserves_both_branches_before_join() { + let mut schedule = + schedule_with_access_index(&[0, 1, 2], &[&[0, 2], &[1, 2]], &[&[0], &[1], &[0, 1]]); + + schedule.reorder_cmds(3); + + assert_eq!(schedule.cmds, vec![0, 1, 2]); + } + + #[test] + fn reorder_scheduled_cmds_prioritizes_ready_resource_successors() { + let mut schedule = schedule_with_access_index( + &[0, 1, 2, 3, 4], + &[&[0, 1, 3], &[0, 4], &[3]], + &[&[0, 1], &[0], &[], &[0, 2], &[1]], + ); + + schedule.reorder_cmds(5); + + assert_eq!(schedule.cmds, vec![0, 1, 3, 4, 2]); + } + + #[test] + fn reorder_scheduled_cmds_ready_ties_use_original_order() { + let mut schedule = schedule_with_access_index( + &[0, 1, 2, 3, 4, 5], + &[&[1, 2], &[1, 4], &[0, 1, 5]], + &[&[2], &[0, 1, 2], &[0], &[], &[1], &[2]], + ); + + schedule.reorder_cmds(6); + + assert_eq!(schedule.cmds, vec![0, 1, 2, 4, 5, 3]); + } + + #[test] + fn reorder_scheduled_cmds_handles_noncontiguous_global_indices() { + let mut schedule = schedule_with_access_index( + &[1, 3, 5, 7], + &[&[1, 5], &[3, 5, 7]], + &[&[], &[0], &[], &[1], &[], &[0, 1], &[], &[1]], + ); + + schedule.reorder_cmds(8); + + assert_eq!(schedule.cmds, vec![1, 3, 5, 7]); + } + #[test] fn reorder_scheduled_cmds_preserves_write_only_order() { let mut schedule = schedule_with_access_index( @@ -7847,6 +8062,47 @@ mod tests { ); } + #[test] + fn reorder_scheduled_cmds_preserves_displaced_write_before_read() { + fuzz::check_schedule_reordering( + 6, + &[ + vec![ + fuzz::ResourceAccess { + cmd_idx: 0, + write: false, + }, + fuzz::ResourceAccess { + cmd_idx: 1, + write: true, + }, + fuzz::ResourceAccess { + cmd_idx: 2, + write: false, + }, + fuzz::ResourceAccess { + cmd_idx: 5, + write: true, + }, + ], + vec![ + fuzz::ResourceAccess { + cmd_idx: 0, + write: false, + }, + fuzz::ResourceAccess { + cmd_idx: 3, + write: true, + }, + fuzz::ResourceAccess { + cmd_idx: 4, + write: false, + }, + ], + ], + ); + } + #[test] fn reorder_scheduled_cmds_preserves_write_then_read_hazard() { fuzz::check_schedule_reordering(