diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..e78efcf5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + +jobs: + check: + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + components: rustfmt, clippy + cache: false + + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Install mdBook + run: cargo install --locked mdbook --version 0.5.3 + + - name: Run CI checks + run: bin/check --allow-dirty --no-gpu diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..0b86f5b4 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,64 @@ +name: Deploy Book + +on: + push: + branches: + - main + - vk-graph + paths-ignore: + - "bin/**" + - "examples/**" + +concurrency: + group: docs-${{ github.ref }} + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + pages: write + id-token: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: false + + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Install mdBook + run: cargo install --locked mdbook --version 0.5.3 + + - name: Build Book + run: | + cd guide + mdbook build + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: guide/book + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/gpu.yml b/.github/workflows/gpu.yml new file mode 100644 index 00000000..c45e9036 --- /dev/null +++ b/.github/workflows/gpu.yml @@ -0,0 +1,46 @@ +name: GPU + +on: + workflow_dispatch: + +concurrency: + group: gpu-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + +jobs: + gpu: + name: GPU smoke and full checks + runs-on: [self-hosted, linux, vulkan] + timeout-minutes: 45 + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + components: rustfmt, clippy + cache: false + + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Install mdBook + run: cargo install --locked mdbook --version 0.5.3 + + - name: Run full checks with GPU + run: bin/check --allow-dirty diff --git a/.github/workflows/rust-clippy.yml b/.github/workflows/rust-clippy.yml index 92c1f751..d46ccc7a 100644 --- a/.github/workflows/rust-clippy.yml +++ b/.github/workflows/rust-clippy.yml @@ -1,54 +1,66 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. -# rust-clippy is a tool that runs a bunch of lints to catch common -# mistakes in your Rust code and help improve your Rust code. -# More details at https://github.com/rust-lang/rust-clippy -# and https://rust-lang.github.io/rust-clippy/ - -name: rust-clippy analyze +name: Clippy SARIF on: push: - branches: [ master ] + branches: [ main ] pull_request: # The branches below must be a subset of the branches above - branches: [ master ] + branches: [ main ] schedule: - cron: '24 22 * * 3' +concurrency: + group: clippy-${{ github.ref }} + cancel-in-progress: true + jobs: rust-clippy-analyze: name: Run rust-clippy analyzing runs-on: ubuntu-latest + timeout-minutes: 30 permissions: contents: read security-events: write steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install Rust toolchain - uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af #@v1 + uses: actions-rust-lang/setup-rust-toolchain@v1 with: - profile: minimal toolchain: stable components: clippy - override: true + cache: false + + - name: Restore Rust cache + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- - name: Install required cargo - run: cargo install clippy-sarif sarif-fmt + run: | + cargo install --locked clippy-sarif --version 0.8.0 + cargo install --locked sarif-fmt --version 0.8.0 - name: Run rust-clippy - run: - cargo clippy - --all-features - --message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt - continue-on-error: true + shell: bash + run: | + set -o pipefail + cargo clippy --all-targets --workspace --no-default-features --features loaded --message-format=json \ + | clippy-sarif \ + | tee rust-clippy-results.sarif \ + | sarif-fmt - name: Upload analysis results to GitHub - uses: github/codeql-action/upload-sarif@v1 + if: always() + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: rust-clippy-results.sarif wait-for-processing: true diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index 20cb8323..00000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Rust - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -env: - CARGO_TERM_COLOR: always - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - name: Build - run: cargo build - - name: Run tests - run: cargo test diff --git a/.gitignore b/.gitignore index ec03cfa5..9beb954c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ Cargo.lock **/target/ **/.vscode/ -!contrib/.vscode/ imgui.ini +libdxcompiler.so **/*.pak -**/*.profraw \ No newline at end of file +**/*.profraw +AGENTS.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ce066eb9..d46ce7fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to Screen 13 +# Contributing to vk-graph Thank you for taking the time to look over this document. You are encouraged to open issues, submit PRs, suggest changes, or anything else you feel might move this project forward. @@ -12,7 +12,7 @@ This section lists the absolute minimum requirements you must understand and pra involved with this project. If you fail to uphold the spirit of these rules you will not have any access to make changes within this project and you may be banned entirely. -## Licesnsing Requirements +## Licensing Requirements All contributions, ideas, issues, or other efforts you expend on this project must be provided using the existing MIT or Apache 2.0 license agreements applied to this project. This means that @@ -24,12 +24,12 @@ retain no ownership or control after contribution. All code must: -- Be modern Rust code (currently 2021 edition, latest stable compiler) _or_ GLSL. +- Be modern Rust code (currently 2024 edition, latest stable compiler) _or_ GLSL. - Pass `cargo fmt` and `cargo clippy` (debug and release) with no warnings - Support required platforms: Linux, Mac, Windows - Use only `crates.io`-published crates -### Recommentations +### Recommendations All code should: @@ -54,8 +54,8 @@ needed. ## Release checklist - Run `cargo update` to ensure you have the latest dependencies -- Double-check `cargo clippy --release` and `cargo fmt` _(use `contrib/rel-mgmt/check`)_ -- Double-check all examples compile and run as intended _(use `contrib/rel-mgmt/run-all-examples`)_ +- Double-check `cargo clippy --release` and `cargo fmt` _(use `bin/check`)_ +- Double-check all examples compile and run as intended _(use `bin/run-all-examples`)_ - Double-check the above on all supported platforms - Change log: Add a section for the new version - Change log: Transfer unreleased details to the new version diff --git a/Cargo.toml b/Cargo.toml index 858700f1..a7ceeba4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,53 @@ -[package] -name = "screen-13" -version = "0.13.0" -authors = ["John Wells "] +[workspace] +resolver = "3" +members = [ + "crates/vk-graph-egui", + "crates/vk-graph-fx", + "crates/vk-graph-hot", + "crates/vk-graph-imgui", + "crates/vk-graph-window", + "examples/shader-toy", + "examples/skeletal-anim", + "examples/vr", + "guide", + "guide/guide-helper" +] + +[workspace.package] +rust-version = "1.94" edition = "2024" license = "MIT OR Apache-2.0" +repository = "https://github.com/attackgoat/vk-graph" +readme = "README.md" + +[workspace.dependencies] +log = "0.4" +profiling = "1.0" +read-only = "0.1" +vk-graph.path = "" +vk-graph-egui.path = "crates/vk-graph-egui" +vk-graph-fx.path = "crates/vk-graph-fx" +vk-graph-hot.path = "crates/vk-graph-hot" +vk-graph-imgui.path = "crates/vk-graph-imgui" +vk-graph-window.path = "crates/vk-graph-window" +winit = "0.30" + +[package] +name = "vk-graph" +version = "0.14.0+alpha" +authors = ["John Wells "] +edition.workspace = true +license.workspace = true +repository.workspace = true readme = "README.md" -repository = "https://github.com/attackgoat/screen-13" -homepage = "https://github.com/attackgoat/screen-13" -documentation = "https://docs.rs/screen-13" +homepage = "https://github.com/attackgoat/vk-graph" +documentation = "https://docs.rs/vk-graph" keywords = ["gamedev", "vulkan"] categories = ["game-development", "multimedia::images", "rendering::engine"] -description = "An easy-to-use Vulkan rendering engine in the spirit of QBasic." +description = "A high-performance Vulkan driver with automatic resource management and execution." [features] -default = ["loaded"] +default = ["loaded", "parking_lot"] ash-molten=["loaded", "dep:ash-molten"] linked=["ash/linked"] loaded=["ash/loaded"] @@ -24,18 +58,19 @@ profile-with-superluminal = ["profiling/profile-with-superluminal"] profile-with-tracy = ["profiling/profile-with-tracy"] [dependencies] -ash = { version = "0.38", default-features = false, features = ["debug", "std"] } +ash = { version = "0.38.0", default-features = false, features = ["debug", "std"] } ash-window = "0.13" derive_builder = "0.20" gpu-allocator = "0.28" -log = "0.4" +log.workspace = true ordered-float = "5.1" parking_lot = { version = "0.12", optional = true } paste = "1.0" -profiling = "1.0" +profiling.workspace = true raw-window-handle = "0.6" +read-only.workspace = true spirq = "1.2" -vk-sync = { version = "0.5", package = "vk-sync-fork" } # // SEE: https://github.com/gwihlidal/vk-sync-rs/pull/4 -> https://github.com/expenses/vk-sync-rs +vk-sync = { version = "0.5", package = "vk-sync-fork" } # // SEE: https://github.com/gwihlidal/vk-sync-rs/pull/4 -> https://github.com/expenses/vk-sync-rs [target.'cfg(target_os = "macos")'.dependencies] ash-molten = { version = "0.20", optional = true } @@ -43,13 +78,12 @@ ash-molten = { version = "0.20", optional = true } [dev-dependencies] anyhow = "1.0" bmfont = { version = "0.3", default-features = false } -bytemuck = "1.22" +bytemuck = "1.25" clap = { version = "4.5", features = ["derive"] } -glam = { version = "0.30", features = ["bytemuck"] } +glam = { version = "0.32", features = ["bytemuck"] } half = { version = "2.4", features = ["bytemuck"] } hassle-rs = "0.11" image = "0.25" -inline-spirv = "0.2" log = "0.4" meshopt = "0.2" polyhedron-ops = ">=0.2, <=0.2.4" @@ -58,10 +92,11 @@ puffin = "0.19" puffin_http = "0.16" rand = "0.9" reqwest = { version = "0.12", features = ["blocking"] } -screen-13-fx = { path = "contrib/screen-13-fx" } -screen-13-imgui = { path = "contrib/screen-13-imgui" } -screen-13-egui = { path = "contrib/screen-13-egui" } -screen-13-window = { path = "contrib/screen-13-window" } +vk-graph-fx.workspace = true +vk-graph-imgui.workspace = true +vk-graph-egui.workspace = true +vk-graph-window.workspace = true +vk-shader-macros = "0.2" tobj = "4.0" -winit = "0.30" -winit_input_helper = { git = "https://github.com/stefnotch/winit_input_helper.git", rev = "6e76a79d01ce836c01b9cdeaa98846a6f0955dc4" } #"0.16" +winit.workspace = true +winit_input_helper = "0.17" diff --git a/README.md b/README.md index 64a1f22a..1d23e0d5 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,30 @@ -# Screen 13 +Vulkan Graph Driver +=================== -[![Crates.io](https://img.shields.io/crates/v/screen-13.svg)](https://crates.io/crates/screen-13) -[![Docs.rs](https://docs.rs/screen-13/badge.svg)](https://docs.rs/screen-13) -[![LoC](https://tokei.rs/b1/github/attackgoat/screen-13?category=code)](https://github.com/attackgoat/screen-13) +[![Crates.io](https://img.shields.io/crates/v/vk-graph.svg)](https://crates.io/crates/vk-graph) +[![Docs.rs](https://docs.rs/vk-graph/badge.svg)](https://docs.rs/vk-graph) +[![Guide Book](https://img.shields.io/badge/vk--graph-Guide_Book-blue?link=https%3A%2F%2Fattackgoat.github.io%2Fvk-graph%2F)](https://attackgoat.github.io/vk-graph) -_Screen 13_ is an easy-to-use Vulkan rendering engine in the spirit of -_[QBasic](https://en.wikipedia.org/wiki/QBasic)_. +_vk-graph_ is a high-performance [Vulkan](https://www.vulkan.org/) driver for the Rust programming +language featuring automated resource management and execution. It is _blazingly_-fast, built for +real-world use, and supports modern Vulkan commands[^modern]. ```toml [dependencies] -screen-13 = "0.12" +vk-graph = "0.14" ``` +[*Changelog*](https://github.com/attackgoat/vk-graph/blob/main/CHANGELOG.md) + +
+ ## Overview -_Screen 13_ provides a high performance [Vulkan](https://www.vulkan.org/) driver using smart -pointers. The driver may be created manually for headless rendering or automatically using the -built-in window abstraction: +_vk-graph_ supports desktop, mobile, and AR/VR platforms in headless, windowed, or full-screen +modes. An [accessory crate](crates/vk-graph-window/README.md) is provided for `winit` support: ```rust -use screen_13_window::{Window, WindowError}; +use vk_graph_window::{Window, WindowError}; fn main() -> Result<(), WindowError> { Window::new()?.run(|frame| { @@ -28,36 +33,61 @@ fn main() -> Result<(), WindowError> { } ``` +
+ ## Usage -_Screen 13_ provides a fully-generic render graph structure for simple and statically -typed access to all the resources used while rendering. The `RenderGraph` structure allows Vulkan -smart pointer resources to be bound as "nodes" which may be used anywhere in a graph. The graph -itself is not tied to swapchain access and may be used to execute general command streams. +_vk-graph_ provides a fully-generic graph structure for statically typed access to resources used +while rendering. The `Graph` structure allows Vulkan smart pointer resources to be bound as +"nodes" which may be used by shader pipelines. The graph supports swapchain integration and may be +used to execute custom command streams. -Features of the render graph: +Features of the graph: - Compute, graphic, and ray-trace pipelines - Automatic Vulkan management (render passes, subpasses, descriptors, pools, _etc._) - Automatic render pass scheduling, re-ordering, merging, with resource aliasing - Interoperable with existing Vulkan code - - Optional [shader hot-reload](contrib/screen-13-hot/README.md) from disk + - Optional [shader hot-reload](crates/vk-graph-hot/README.md) from disk + +Example code: ```rust -render_graph - .begin_pass("Fancy new algorithm for shading a moving character who is actively on fire") +graph + .begin_cmd() + .debug_name("Fancy new algorithm for shading a moving character who is actively on fire") .bind_pipeline(&gfx_pipeline) - .read_descriptor(0, some_image) - .read_descriptor(1, another_image) - .read_descriptor(3, some_buf) - .clear_color(0, swapchain_image) - .store_color(0, swapchain_image) - .record_subpass(move |subpass| { - subpass.push_constants(some_u8_slice); - subpass.draw(6, 1, 0, 0); + .shader_resource_access(0, prev_image, AccessType::FragmentShaderReadColorInputAttachment) + .shader_resource_access(1, some_image, AccessType::FragmentShaderReadOther) + .shader_resource_access(3, fire_buffer, AccessType::FragmentShaderReadUniformBuffer) + .color_attachment_image(0, swapchain_image, LoadOp::CLEAR_BLACK_ALPHA_ZERO, StoreOp::Store) + .depth_stencil_attachment_image(depth_image, LoadOp::Load, StoreOp::DontCare) + .record_cmd(move |cmd| { + cmd + .push_constants(some_u8_slice) + .draw(6, 1, 0, 0); }); ``` -### Debug Logging + +
+ +## Optional features + +_vk-graph_ puts a lot of functionality behind optional features in order to optimize +compile time for the most common use cases. The following features are +available. + +- **`loaded`** *(enabled by default)* — Support searching for the Vulkan loader manually at runtime. +- **`linked`** — Link the Vulkan loader at compile time. +- **`profile-with-*`** — Use the specified profiling backend: + `profile-with-puffin`, `profile-with-optick`, `profile-with-superluminal`, or + `profile-with-tracy` + +
+ + + +## Debug Logging This crate uses [`log`](https://crates.io/crates/log) for low-overhead logging. @@ -68,21 +98,23 @@ To enable logging, set the `RUST_LOG` environment variable to `trace`, `debug`, _You may also filter messages, for example:_ ```bash -RUST_LOG=screen_13::driver=trace,screen_13=warn cargo run --example ray_trace +RUST_LOG=vk_graph::driver=trace,vk_graph=warn cargo run --example ray_trace ``` ``` -TRACE screen_13::driver::instance > created a Vulkan instance -DEBUG screen_13::driver::physical_device > physical device: NVIDIA GeForce RTX 3090 -DEBUG screen_13::driver::physical_device > extension "VK_KHR_16bit_storage" v1 -DEBUG screen_13::driver::physical_device > extension "VK_KHR_8bit_storage" v1 -DEBUG screen_13::driver::physical_device > extension "VK_KHR_acceleration_structure" v13 +TRACE vk_graph::driver::instance > created a Vulkan instance +DEBUG vk_graph::driver::physical_device > physical device: NVIDIA GeForce RTX 3090 +DEBUG vk_graph::driver::physical_device > extension "VK_KHR_16bit_storage" v1 +DEBUG vk_graph::driver::physical_device > extension "VK_KHR_8bit_storage" v1 +DEBUG vk_graph::driver::physical_device > extension "VK_KHR_acceleration_structure" v13 ... ``` -### Performance Profiling +
+ +## Performance Profiling -This crates uses [`profiling`](https://crates.io/crates/profiling) and supports multiple profiling +This crate uses [`profiling`](https://crates.io/crates/profiling) and supports multiple profiling providers. When not in use profiling has zero cost. To enable profiling, compile with one of the `profile-with-*` features enabled and initialize the @@ -94,48 +126,43 @@ _Example code uses [puffin](https://crates.io/crates/puffin):_ cargo run --features profile-with-puffin --release --example vsm_omni ``` -Flamegraph of performance data +Flamegraph of performance data + +
## Quick Start Included are some examples you might find helpful: -- [`hello_world.rs`](contrib/screen-13-window/examples/hello_world.rs) — Displays a window on the screen. Please start here. +- [`hello_world.rs`](crates/vk-graph-window/examples/hello_world.rs) — Displays a window on the + screen. Please start here. - [`triangle.rs`](examples/triangle.rs) — Shaders and full setup of index/vertex buffers; < 100 LOC. - [`shader-toy/`](examples/shader-toy) — Recreation of a two-pass shader toy using the original shader code. See the [example code](examples/README.md), -[documentation](https://docs.rs/screen-13/latest/screen_13/), or helpful -[getting started guide](examples/getting-started.md) for more information. - -**_NOTE:_** Required development packages and libraries are listed in the _getting started guide_. -All new users should read and understand the guide. +[documentation](https://docs.rs/vk-graph/latest/vk_graph/), or helpful +[guide book](https://attackgoat.github.io/vk-graph) for more information. -## History +**_NOTE:_** Required development packages and libraries are listed in the _guide_. All new users +should read and understand the guide. -As a child I was given access to a computer that had _GW-Basic_; and later one with _QBasic_. All of -my favorite programs started with: - -```basic -CLS -SCREEN 13 -``` +
-These commands cleared the screen of text and setup a 320x200 256-color paletized video mode. There -were other video modes available, but none of them had the 'magic' of 256 colors. +#### License -Additional commands _QBasic_ offered, such as `DRAW`, allowed you to build simple games quickly -because you didn't have to grok the entirety of compiling and linking. I think we should have -options like this today, and so I started this project to allow future developers to have the -ability to get things done quickly while using modern tools. + +Licensed under either of Apache License, Version +2.0 or MIT license at your option. + -### Inspirations +
-_Screen 13_ was built from the learnings and lessons shared by others throughout our community. In -particular, here are some of the repositories I found useful: + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this crate by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. + - - [Bevy](https://bevyengine.org/): A refreshingly simple data-driven game engine built in Rust - - [Granite](https://github.com/Themaister/Granite) - Open-source Vulkan renderer - - [Kajiya](https://github.com/EmbarkStudios/kajiya) - Experimental real-time global illumination - renderer made with Rust and Vulkan +[^modern]: Modern Vulkan usage means no pixel queries. Anything else unsupported is due to there +being better options, no current need, or no interest. Please open an issue. diff --git a/bin/check b/bin/check new file mode 100755 index 00000000..d4ad5d14 --- /dev/null +++ b/bin/check @@ -0,0 +1,81 @@ +#!/bin/sh + +set -eu + +ALLOW_DIRTY=0 +MDBOOK_VERSION=0.5.3 +RUN_GPU=1 +RUN_SEMVER=0 + +for arg in "$@"; do + case "$arg" in + --allow-dirty) + ALLOW_DIRTY=1 + ;; + --no-gpu) + RUN_GPU=0 + ;; + --semver) + RUN_SEMVER=1 + ;; + *) + printf 'unknown argument: %s\n' "$arg" >&2 + exit 2 + ;; + esac +done + +fail() { + printf '%s\n' "$1" >&2 + exit "${2-1}" +} + +run() { + printf '==> %s\n' "$1" + shift + "$@" +} + +install_mdbook() { + version=$(mdbook --version 2>/dev/null | awk '{print $2}' || true) + + if [ "$version" != "$MDBOOK_VERSION" ]; then + run "install mdbook ${MDBOOK_VERSION}" cargo install --locked mdbook --version "$MDBOOK_VERSION" + fi +} + +if [ "$ALLOW_DIRTY" -ne 1 ]; then + git diff --no-ext-diff --quiet || fail "uncommitted changes (pass --allow-dirty to skip this check)" +fi + +# Rust code checks +run "format check" cargo fmt --all --check +run "workspace target check" env RUSTFLAGS="-D warnings" cargo check --all-targets --workspace --no-default-features --features loaded +run "workspace target check (parking_lot)" env RUSTFLAGS="-D warnings" cargo check --all-targets --workspace --features parking_lot +run "workspace clippy" cargo clippy --all-targets --workspace --no-default-features --features loaded -- -D warnings +run "workspace clippy (parking_lot)" cargo clippy --all-targets --workspace --features parking_lot -- -D warnings +run "workspace tests" env RUSTFLAGS="-D warnings" cargo test --all-targets --workspace --no-default-features --features loaded + +if [ "$RUN_GPU" -eq 1 ]; then + run "gpu smoke tests" env RUSTFLAGS="-D warnings" cargo test --test gpu_smoke cpu_readback -- --ignored --exact --test-threads=1 +fi + +run "workspace doctests" env RUSTFLAGS="-D warnings" cargo test --workspace --doc +run "workspace docs" env RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps +run "guide hot doctests" env RUSTFLAGS="-D warnings" cargo test -p guide --features hot + +# Build guide book +install_mdbook +run "guide build" mdbook build guide/ + +if [ "$RUN_SEMVER" -eq 1 ]; then + # Check for semver breaking changes: if this fails you must update the crate version or fix the code + run "semver check (vk-graph)" cargo semver-checks check-release -p vk-graph --default-features + run "semver check (vk-graph-egui)" cargo semver-checks check-release -p vk-graph-egui + run "semver check (vk-graph-fx)" cargo semver-checks check-release -p vk-graph-fx + run "semver check (vk-graph-hot)" cargo semver-checks check-release -p vk-graph-hot + run "semver check (vk-graph-imgui)" cargo semver-checks check-release -p vk-graph-imgui + run "semver check (vk-graph-window)" cargo semver-checks check-release -p vk-graph-window +fi + +printf 'OK\n' diff --git a/contrib/rel-mgmt/run-all-examples b/bin/run-all-examples similarity index 57% rename from contrib/rel-mgmt/run-all-examples rename to bin/run-all-examples index 66811a4e..c54741d7 100755 --- a/contrib/rel-mgmt/run-all-examples +++ b/bin/run-all-examples @@ -2,25 +2,11 @@ set -e -# Update everything -cargo update -cargo update --manifest-path contrib/screen-13-egui/Cargo.toml -cargo update --manifest-path contrib/screen-13-fx/Cargo.toml -cargo update --manifest-path contrib/screen-13-hot/Cargo.toml -cargo update --manifest-path contrib/screen-13-imgui/Cargo.toml -cargo update --manifest-path contrib/screen-13-window/Cargo.toml -cargo update --manifest-path examples/skeletal-anim/Cargo.toml -cargo update --manifest-path examples/shader-toy/Cargo.toml -cargo update --manifest-path examples/vr/Cargo.toml - -# Build everything -cargo build --examples - # Run the "test" example first cargo run --example fuzzer # Run all regular examples, in debug mode, next -cargo run --manifest-path contrib/screen-13-window/Cargo.toml --example hello_world -- --debug +cargo run -p vk-graph-window --example hello_world -- --debug cargo run --example app -- --debug cargo run --example aliasing -- --debug cargo run --example cpu_readback -- --debug @@ -47,11 +33,11 @@ cargo run --example transitions -- --debug cargo run --example vsm_omni -- --debug cargo run --example vsm_omni -- --debug --geometry-shader cargo run --example ray_omni -- --debug -cargo run --manifest-path examples/skeletal-anim/Cargo.toml -- --debug +cargo run -p skeletal-anim -- --debug # Hot-reload examples -cargo run --manifest-path contrib/screen-13-hot/Cargo.toml --example glsl -- --debug -cargo run --manifest-path contrib/screen-13-hot/Cargo.toml --example hlsl -- --debug +cargo run -p vk-graph-hot --example glsl -- --debug +cargo run -p vk-graph-hot --example hlsl -- --debug # Run this one in release mode -cargo run --manifest-path examples/shader-toy/Cargo.toml --release -- --debug +cargo run -p shader-toy --release -- --debug diff --git a/contrib/.vscode/launch.json b/contrib/.vscode/launch.json deleted file mode 100644 index ff7fedd4..00000000 --- a/contrib/.vscode/launch.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in library 'screen-13'", - "cargo": { - "args": [ - "test", - "--no-run", - "--lib", - "--package=screen-13" - ], - "filter": { - "name": "screen-13", - "kind": "lib" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug example 'hello_world'", - "cargo": { - "args": [ - "build", - "--example=hello_world", - "--package=screen-13" - ], - "filter": { - "name": "hello_world", - "kind": "example" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - } - ] -} diff --git a/contrib/.vscode/tasks.json b/contrib/.vscode/tasks.json deleted file mode 100644 index 0f79ddf9..00000000 --- a/contrib/.vscode/tasks.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "type": "cargo", - "command": "build", - "problemMatcher": [ - "$rustc" - ], - "group": { - "kind": "build", - "isDefault": true - }, - "presentation": { - "echo": false, - "revealProblems": "onProblem", - "focus": false, - "panel": "shared", - "showReuseMessage": false, - "clear": true - }, - "label": "Build Screen 13" - } - ] -} diff --git a/contrib/README.md b/contrib/README.md deleted file mode 100644 index 53d4696d..00000000 --- a/contrib/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# User Contributions to Screen 13 - -These subdirectories contain additions, changes, and other things you might find useful while -using _Screen 13_. These user-provided contributions are not guaranteed to work and are untested. - -## [`.vscode/`](.vscode/) - -Configuration files for users of _[Visual Studio Code](https://code.visualstudio.com/)_. Copy the -`.vscode/` directory into the root _Screen 13_ project directory in order to enable build and debug -configurations. - -**_NOTE:_** Requires installation of the -_[CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb)_ extension for -debugging. - -### [`rel-mgmt/`](rel-mgmt/) - -A script which exercises all test cases and build conditions which must succeed prior to merging new -code into the main branch. - -### [`screen-13-egui/`](screen-13-egui/README.md) - -Renderer for [egui](https://github.com/emilk/egui); a simple, fast, and highly portable immediate -mode GUI library. - -### [`screen-13-fx/`](screen-13-fx/README.md) - -Pre-defined effects and tools built using _Screen 13_ features. Generally anything that requires -shaders or other physical data which shouldn't be part of the main library. - -### [`screen-13-hot/`](screen-13-hot/README.md) - -Adds a hot-reload feature to compute, graphic and ray-trace shader pipelines. - -### [`screen-13-imgui/`](screen-13-imgui/README.md) - -Renderer for [Dear ImGui](https://github.com/imgui-rs/imgui-rs). Provides a graphical user interface -useful for debug purposes. \ No newline at end of file diff --git a/contrib/rel-mgmt/check b/contrib/rel-mgmt/check deleted file mode 100755 index 2bf3112d..00000000 --- a/contrib/rel-mgmt/check +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/sh - -set -e - -fail() { - printf '%s\n' "$1" >&2 ## Send message to stderr. Exclude >&2 if you don't want it that way. - exit "${2-1}" ## Return a code specified by $2 or 1 by default. -} - -diff() { - git diff --no-ext-diff --quiet -} - -# Uncommitted changes -diff || fail "Uncommitted changes" - -# Unformatted rust code -cargo fmt && diff || fail "Unformatted rust code" -cargo fmt --manifest-path contrib/screen-13-egui/Cargo.toml && diff || fail "Unformatted rust code (screen-13-egui)" -cargo fmt --manifest-path contrib/screen-13-fx/Cargo.toml && diff || fail "Unformatted rust code (screen-13-fx)" -cargo fmt --manifest-path contrib/screen-13-hot/Cargo.toml && diff || fail "Unformatted rust code (screen-13-hot)" -cargo fmt --manifest-path contrib/screen-13-imgui/Cargo.toml && diff || fail "Unformatted rust code (screen-13-imgui)" -cargo fmt --manifest-path examples/shader-toy/Cargo.toml && diff || fail "Unformatted rust code (shader-toy)" -cargo fmt --manifest-path examples/skeletal-anim/Cargo.toml && diff || fail "Unformatted rust code (skeletal-anim)" -cargo fmt --manifest-path examples/vr/Cargo.toml && diff || fail "Unformatted rust code (vr)" - -# Rust code errors -echo "Checking screen-13" -cargo check --all-targets -echo "Checking screen-13 (w/ parking_lot)" -cargo check --all-targets --features parking_lot -echo "Checking contrib/screen-13-egui" -cargo check --manifest-path contrib/screen-13-egui/Cargo.toml --all-targets --all-features -echo "Checking contrib/screen-13-fx" -cargo check --manifest-path contrib/screen-13-fx/Cargo.toml --all-targets --all-features -echo "Checking contrib/screen-13-hot" -cargo check --manifest-path contrib/screen-13-hot/Cargo.toml --all-targets --all-features -#echo "Checking contrib/screen-13-imgui" -#cargo check --manifest-path contrib/screen-13-imgui/Cargo.toml --all-targets --all-features -echo "Checking contrib/screen-13-window" -cargo check --manifest-path contrib/screen-13-window/Cargo.toml --all-targets --all-features -echo "Checking examples/shader-toy" -cargo check --manifest-path examples/shader-toy/Cargo.toml --all-targets --all-features -echo "Checking examples/skeletal-anim" -cargo check --manifest-path examples/skeletal-anim/Cargo.toml --all-targets --all-features -echo "Checking examples/vr" -cargo check --manifest-path examples/vr/Cargo.toml --all-targets --all-features - -# Rust code lints -cargo clippy --all-targets -cargo clippy --all-targets --features parking_lot -cargo clippy --manifest-path contrib/screen-13-egui/Cargo.toml --all-targets --all-features -cargo clippy --manifest-path contrib/screen-13-fx/Cargo.toml --all-targets --all-features -cargo clippy --manifest-path contrib/screen-13-hot/Cargo.toml --all-targets --all-features -#cargo clippy --manifest-path contrib/screen-13-imgui/Cargo.toml --all-targets --all-features -cargo clippy --manifest-path examples/shader-toy/Cargo.toml --all-targets --all-features -cargo clippy --manifest-path examples/skeletal-anim/Cargo.toml --all-targets --all-features -cargo clippy --manifest-path examples/vr/Cargo.toml --all-targets --all-features - -# Rust code tests -cargo test - -# Check for semver breaking changes: if this fails you must update the crate version or fix the code -cargo semver-checks check-release --default-features - -echo "OK" diff --git a/contrib/screen-13-egui/Cargo.toml b/contrib/screen-13-egui/Cargo.toml deleted file mode 100644 index b66a5e06..00000000 --- a/contrib/screen-13-egui/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "screen-13-egui" -version = "0.1.0" -authors = ["Christian Döring "] -edition = "2021" -license = "MIT OR Apache-2.0" -readme = "README.md" - -[dependencies] -bytemuck = "1.14" -# TODO: Waiting for egui to update winit version -egui = { git = "https://github.com/emilk/egui.git", rev = "3777b8d2741f298eaa1409dc08062902f7541990" } #{ version = "0.28", features = ["bytemuck"] } -egui-winit = { git = "https://github.com/emilk/egui.git", rev = "3777b8d2741f298eaa1409dc08062902f7541990" } #"0.28" -inline-spirv = "0.2" -screen-13 = { path = "../.." } -screen-13-fx = { path = "../screen-13-fx" } diff --git a/contrib/screen-13-egui/README.md b/contrib/screen-13-egui/README.md deleted file mode 100644 index 87943ef4..00000000 --- a/contrib/screen-13-egui/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# _Screen 13_ _egui_ Integration - -Preview - -[Example code](../../examples/README.md) \ No newline at end of file diff --git a/contrib/screen-13-fx/README.md b/contrib/screen-13-fx/README.md deleted file mode 100644 index 83e86744..00000000 --- a/contrib/screen-13-fx/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# _Screen 13_ Fx - -A bunch of pre-built things you might need when using _Screen 13_. \ No newline at end of file diff --git a/contrib/screen-13-fx/src/presenter.rs b/contrib/screen-13-fx/src/presenter.rs deleted file mode 100644 index 426d21dd..00000000 --- a/contrib/screen-13-fx/src/presenter.rs +++ /dev/null @@ -1,134 +0,0 @@ -use { - bytemuck::cast_slice, - glam::{vec3, Mat4}, - inline_spirv::include_spirv, - screen_13::prelude::*, - std::sync::Arc, -}; - -pub struct ComputePresenter([Arc; 2]); - -impl ComputePresenter { - pub fn new(device: &Arc) -> Result { - let pipeline1 = Arc::new(ComputePipeline::create( - device, - ComputePipelineInfo::default(), - Shader::new_compute( - include_spirv!("res/shader/compute/present1.comp", comp).as_slice(), - ), - )?); - let pipeline2 = Arc::new(ComputePipeline::create( - device, - ComputePipelineInfo::default(), - Shader::new_compute( - include_spirv!("res/shader/compute/present2.comp", comp).as_slice(), - ), - )?); - - Ok(Self([pipeline1, pipeline2])) - } - - pub fn present_image( - &self, - graph: &mut RenderGraph, - image: impl Into, - swapchain: SwapchainImageNode, - ) { - let image = image.into(); - // let image_info = graph.node_info(image); - let swapchain_info = graph.node_info(swapchain); - - // TODO: Notice non-sRGB images and run a different pipeline - - graph - .begin_pass("present (from compute)") - .bind_pipeline(&self.0[0]) - .read_descriptor(0, image) - .write_descriptor(1, swapchain) - .record_compute(move |compute, _| { - compute.dispatch(swapchain_info.width, swapchain_info.height, 1); - }); - } - - pub fn present_images( - &self, - graph: &mut RenderGraph, - top_image: impl Into, - bottom_image: impl Into, - swapchain: SwapchainImageNode, - ) { - let top_image = top_image.into(); - let bottom_image = bottom_image.into(); - // let top_image_info = graph.node_info(top_image); - // let bottom_image_info = graph.node_info(bottom_image); - let swapchain_info = graph.node_info(swapchain); - - // TODO: Notice non-sRGB images and run a different pipeline - - graph - .begin_pass("present (from compute)") - .bind_pipeline(&self.0[1]) - .read_descriptor((0, [0]), top_image) - .read_descriptor((0, [1]), bottom_image) - .write_descriptor(1, swapchain) - .record_compute(move |compute, _| { - compute.dispatch(swapchain_info.width, swapchain_info.height, 1); - }); - } -} - -pub struct GraphicPresenter { - pipeline: Arc, -} - -impl GraphicPresenter { - pub fn new(device: &Arc) -> Result { - Ok(Self { - pipeline: Arc::new(GraphicPipeline::create( - device, - GraphicPipelineInfo::default(), - [ - Shader::new_vertex( - include_spirv!("res/shader/graphic/present.vert", vert).as_slice(), - ), - Shader::new_fragment( - include_spirv!("res/shader/graphic/present.frag", frag).as_slice(), - ), - ], - )?), - }) - } - - pub fn present_image( - &self, - graph: &mut RenderGraph, - image: impl Into, - swapchain: SwapchainImageNode, - ) { - let image = image.into(); - let image_info = graph.node_info(image); - let swapchain_info = graph.node_info(swapchain); - - let (image_width, image_height) = (image_info.width as f32, image_info.height as f32); - let (swapchain_width, swapchain_height) = - (swapchain_info.width as f32, swapchain_info.height as f32); - - let scale = (swapchain_width / image_width).max(swapchain_height / image_height); - let transform = Mat4::from_scale(vec3( - scale * image_width / swapchain_width, - scale * image_height / swapchain_height, - 1.0, - )); - - graph - .begin_pass("present (from graphic)") - .bind_pipeline(&self.pipeline) - .read_descriptor(0, image) - .store_color(0, swapchain) - .record_subpass(move |subpass, _| { - // Draw a quad with implicit vertices (no buffer) - subpass.push_constants(cast_slice(&transform.to_cols_array())); - subpass.draw(6, 1, 0, 0); - }); - } -} diff --git a/contrib/screen-13-hot/.github/img/noise.png b/contrib/screen-13-hot/.github/img/noise.png deleted file mode 100644 index 4f096886..00000000 Binary files a/contrib/screen-13-hot/.github/img/noise.png and /dev/null differ diff --git a/contrib/screen-13-hot/Cargo.toml b/contrib/screen-13-hot/Cargo.toml deleted file mode 100644 index 327afa8f..00000000 --- a/contrib/screen-13-hot/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "screen-13-hot" -version = "0.1.0" -authors = ["John Wells "] -edition = "2021" -license = "MIT OR Apache-2.0" -readme = "README.md" -repository = "https://github.com/attackgoat/screen-13" -homepage = "https://github.com/attackgoat/screen-13/contrib/screen-13-hot" -keywords = ["gamedev", "vulkan"] -categories = ["game-development", "multimedia::images", "rendering::engine"] -description = "Hot-reloading shader pipelines for Screen-13" - -[dependencies] -anyhow = "1.0" -derive_builder = "0.13" -log = "0.4" -notify = "6.1" -screen-13 = { path = "../.."} -screen-13-window = { path = "../screen-13-window" } -shader-prepper = "0.3.0-pre.3" -shaderc = "0.8" - -[dev-dependencies] -clap = { version = "4.5", features = ["derive"] } -pretty_env_logger = "0.5" diff --git a/contrib/screen-13-hot/examples/res/noise.glsl b/contrib/screen-13-hot/examples/res/noise.glsl deleted file mode 100644 index 45cfc1d3..00000000 --- a/contrib/screen-13-hot/examples/res/noise.glsl +++ /dev/null @@ -1,9 +0,0 @@ -const uint k = 1103515245u; - -vec3 hash(uvec3 x) { - x = ((x >> 8u) ^ x.yzx) * k; - x = ((x >> 8u) ^ x.yzx) * k; - x = ((x >> 8u) ^ x.yzx) * k; - - return vec3(x) * (1.0 / float(0xffffffffu)); -} diff --git a/contrib/screen-13-hot/examples/res/noise.hlsl b/contrib/screen-13-hot/examples/res/noise.hlsl deleted file mode 100644 index 9f855922..00000000 --- a/contrib/screen-13-hot/examples/res/noise.hlsl +++ /dev/null @@ -1,9 +0,0 @@ -const uint k = 1103515245u; - -float3 hash(uint3 x) { - x = ((x >> 8u) ^ x.yzx) * k; - x = ((x >> 8u) ^ x.yzx) * k; - x = ((x >> 8u) ^ x.yzx) * k; - - return float3(x) * (1.0 / float(0xffffffffu)); -} diff --git a/contrib/screen-13-hot/src/compute.rs b/contrib/screen-13-hot/src/compute.rs deleted file mode 100644 index 59258f7d..00000000 --- a/contrib/screen-13-hot/src/compute.rs +++ /dev/null @@ -1,79 +0,0 @@ -use { - super::{compile_shader_and_watch, create_watcher, shader::HotShader}, - log::info, - notify::RecommendedWatcher, - screen_13::prelude::*, - std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, -}; - -#[derive(Debug)] -pub struct HotComputePipeline { - device: Arc, - has_changes: Arc, - instance: Arc, - shader: HotShader, - watcher: RecommendedWatcher, -} - -impl HotComputePipeline { - pub fn create( - device: &Arc, - info: impl Into, - shader: impl Into, - ) -> Result { - let shader = shader.into(); - - let (mut watcher, has_changes) = create_watcher(); - let compiled_shader = compile_shader_and_watch(&shader, &mut watcher)?; - - let instance = Arc::new(ComputePipeline::create(device, info, compiled_shader)?); - - let device = Arc::clone(device); - - Ok(Self { - device, - has_changes, - instance, - shader, - watcher, - }) - } - - /// Returns the most recent compilation without checking for changes or re-compiling the shader - /// source code. - pub fn cold(&self) -> &Arc { - &self.instance - } - - /// Returns the most recent compilation after checking for changes, and if needed re-compiling - /// the shader source code. - pub fn hot(&mut self) -> &Arc { - let has_changes = self.has_changes.swap(false, Ordering::Relaxed); - - if has_changes { - info!("Shader change detected"); - - let (mut watcher, has_changes) = create_watcher(); - if let Ok(compiled_shader) = compile_shader_and_watch(&self.shader, &mut watcher) { - if let Ok(instance) = - ComputePipeline::create(&self.device, self.instance.info, compiled_shader) - { - self.has_changes = has_changes; - self.watcher = watcher; - self.instance = Arc::new(instance); - } - } - } - - self.cold() - } -} - -impl AsRef for HotComputePipeline { - fn as_ref(&self) -> &ComputePipeline { - self.instance.as_ref() - } -} diff --git a/contrib/screen-13-hot/src/graphic.rs b/contrib/screen-13-hot/src/graphic.rs deleted file mode 100644 index 92ca7b1b..00000000 --- a/contrib/screen-13-hot/src/graphic.rs +++ /dev/null @@ -1,93 +0,0 @@ -use { - super::{compile_shader_and_watch, create_watcher, shader::HotShader}, - log::info, - notify::RecommendedWatcher, - screen_13::prelude::*, - std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, -}; - -#[derive(Debug)] -pub struct HotGraphicPipeline { - device: Arc, - has_changes: Arc, - instance: Arc, - shaders: Box<[HotShader]>, - watcher: RecommendedWatcher, -} - -impl HotGraphicPipeline { - pub fn create( - device: &Arc, - info: impl Into, - shaders: impl IntoIterator, - ) -> Result - where - S: Into, - { - let shaders = shaders - .into_iter() - .map(|shader| shader.into()) - .collect::>(); - - let (mut watcher, has_changes) = create_watcher(); - let compiled_shaders = shaders - .iter() - .map(|shader| compile_shader_and_watch(shader, &mut watcher)) - .collect::, _>>()?; - - let instance = Arc::new(GraphicPipeline::create(device, info, compiled_shaders)?); - - let device = Arc::clone(device); - - Ok(Self { - device, - has_changes, - instance, - shaders, - watcher, - }) - } - - /// Returns the most recent compilation without checking for changes or re-compiling the shader - /// source code. - pub fn cold(&self) -> &Arc { - &self.instance - } - - /// Returns the most recent compilation after checking for changes, and if needed re-compiling - /// the shader source code. - pub fn hot(&mut self) -> &Arc { - let has_changes = self.has_changes.swap(false, Ordering::Relaxed); - - if has_changes { - info!("Shader change detected"); - - let (mut watcher, has_changes) = create_watcher(); - if let Ok(compiled_shaders) = self - .shaders - .iter() - .map(|shader| compile_shader_and_watch(shader, &mut watcher)) - .collect::, DriverError>>() - { - if let Ok(instance) = - GraphicPipeline::create(&self.device, self.instance.info, compiled_shaders) - { - self.has_changes = has_changes; - self.watcher = watcher; - self.instance = Arc::new(instance); - } - } - } - - self.cold() - } -} - -impl AsRef for HotGraphicPipeline { - fn as_ref(&self) -> &GraphicPipeline { - self.instance.as_ref() - } -} diff --git a/contrib/screen-13-hot/src/lib.rs b/contrib/screen-13-hot/src/lib.rs deleted file mode 100644 index 4ed2cedb..00000000 --- a/contrib/screen-13-hot/src/lib.rs +++ /dev/null @@ -1,199 +0,0 @@ -pub mod compute; -pub mod graphic; -pub mod ray_trace; -pub mod shader; - -pub mod prelude { - pub use super::{ - compute::HotComputePipeline, - graphic::HotGraphicPipeline, - ray_trace::HotRayTracePipeline, - shader::{HotShader, HotShaderBuilder, OptimizationLevel, SourceLanguage, SpirvVersion}, - }; -} - -use { - self::shader::HotShader, - log::{error, info}, - notify::{recommended_watcher, Event, EventKind, RecommendedWatcher}, - screen_13::prelude::*, - shader_prepper::{ - process_file, BoxedIncludeProviderError, IncludeProvider, ResolvedInclude, - ResolvedIncludePath, - }, - shaderc::{CompileOptions, Compiler, ShaderKind, SourceLanguage}, - std::{ - collections::HashSet, - fs::read_to_string, - io::{Error, ErrorKind}, - path::{Path, PathBuf}, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, OnceLock, - }, - }, -}; - -struct CompiledShader { - files_included: HashSet, - spirv_code: Vec, -} - -fn compile_shader( - path: impl AsRef, - entry_name: &str, - shader_kind: Option, - additional_opts: Option<&CompileOptions<'_>>, -) -> anyhow::Result { - info!("Compiling: {}", path.as_ref().display()); - - let path = path.as_ref().to_path_buf(); - let shader_kind = shader_kind.unwrap_or_else(|| guess_shader_kind(&path)); - - #[derive(Default)] - struct FileIncludeProvider(HashSet); - - impl IncludeProvider for FileIncludeProvider { - type IncludeContext = PathBuf; - - fn get_include( - &mut self, - path: &ResolvedIncludePath, - ) -> Result { - self.0.insert(PathBuf::from(&path.0)); - - Ok(read_to_string(&path.0)?) - } - - fn resolve_path( - &self, - path: &str, - context: &Self::IncludeContext, - ) -> Result, BoxedIncludeProviderError> { - let path = context.join(path); - - Ok(ResolvedInclude { - resolved_path: ResolvedIncludePath(path.to_str().unwrap_or_default().to_string()), - context: path - .parent() - .map(|path| path.to_path_buf()) - .unwrap_or_default(), - }) - } - } - - let mut file_include_provider = FileIncludeProvider::default(); - let source_code = process_file( - path.to_string_lossy().as_ref(), - &mut file_include_provider, - PathBuf::new(), - ) - .map_err(|err| { - error!("Unable to process shader file: {err}"); - - Error::new(ErrorKind::InvalidData, err) - })? - .iter() - .map(|chunk| chunk.source.as_str()) - .collect::(); - let files_included = file_include_provider.0; - - static COMPILER: OnceLock = OnceLock::new(); - let spirv_code = COMPILER - .get_or_init(|| Compiler::new().expect("Unable to initialize shaderc")) - .compile_into_spirv( - &source_code, - shader_kind, - &path.to_string_lossy(), - entry_name, - additional_opts, - ) - .inspect_err(|_| { - eprintln!("Shader: {}", path.display()); - - for (line_index, line) in source_code.split('\n').enumerate() { - let line_number = line_index + 1; - eprintln!("{line_number}: {line}"); - } - })? - .as_binary_u8() - .to_vec(); - - Ok(CompiledShader { - files_included, - spirv_code, - }) -} - -fn compile_shader_and_watch( - shader: &HotShader, - watcher: &mut RecommendedWatcher, -) -> Result { - let mut base_shader = Shader::new(shader.stage, shader.compile_and_watch(watcher)?.as_slice()); - - base_shader = base_shader.entry_name(shader.entry_name.clone()); - - if let Some(specialization_info) = &shader.specialization_info { - base_shader = base_shader.specialization_info(specialization_info.clone()); - } - - Ok(base_shader) -} - -fn create_watcher() -> (RecommendedWatcher, Arc) { - let has_changes = Arc::new(AtomicBool::new(false)); - let has_changes_clone = Arc::clone(&has_changes); - let watcher = recommended_watcher(move |event: notify::Result| { - let event = event.unwrap_or_else(|_| Event::new(EventKind::Any)); - if matches!( - event.kind, - EventKind::Any | EventKind::Modify(_) | EventKind::Other - ) { - has_changes_clone.store(true, Ordering::Relaxed); - } - }) - .unwrap(); - - (watcher, has_changes) -} - -fn guess_shader_kind(path: impl AsRef) -> ShaderKind { - match path - .as_ref() - .extension() - .map(|ext| ext.to_string_lossy().to_string()) - .unwrap_or_default() - .as_str() - { - "comp" => ShaderKind::Compute, - "task" => ShaderKind::Task, - "mesh" => ShaderKind::Mesh, - "vert" => ShaderKind::Vertex, - "geom" => ShaderKind::Geometry, - "tesc" => ShaderKind::TessControl, - "tese" => ShaderKind::TessEvaluation, - "frag" => ShaderKind::Fragment, - "rgen" => ShaderKind::RayGeneration, - "rahit" => ShaderKind::AnyHit, - "rchit" => ShaderKind::ClosestHit, - "rint" => ShaderKind::Intersection, - "rcall" => ShaderKind::Callable, - "rmiss" => ShaderKind::Miss, - _ => ShaderKind::InferFromSource, - } -} - -fn guess_shader_source_language(path: impl AsRef) -> Option { - match path - .as_ref() - .extension() - .map(|ext| ext.to_string_lossy().to_string()) - .unwrap_or_default() - .as_str() - { - "comp" | "task" | "mesh" | "vert" | "geom" | "tesc" | "tese" | "frag" | "rgen" - | "rahit" | "rchit" | "rint" | "rcall" | "rmiss" | "glsl" => Some(SourceLanguage::GLSL), - "hlsl" => Some(SourceLanguage::HLSL), - _ => None, - } -} diff --git a/contrib/screen-13-hot/src/ray_trace.rs b/contrib/screen-13-hot/src/ray_trace.rs deleted file mode 100644 index c7a8ab7d..00000000 --- a/contrib/screen-13-hot/src/ray_trace.rs +++ /dev/null @@ -1,105 +0,0 @@ -use { - super::{compile_shader_and_watch, create_watcher, shader::HotShader}, - log::info, - notify::RecommendedWatcher, - screen_13::prelude::*, - std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, -}; - -#[derive(Debug)] -pub struct HotRayTracePipeline { - device: Arc, - has_changes: Arc, - instance: Arc, - shader_groups: Box<[RayTraceShaderGroup]>, - shaders: Box<[HotShader]>, - watcher: RecommendedWatcher, -} - -impl HotRayTracePipeline { - pub fn create( - device: &Arc, - info: impl Into, - shaders: impl IntoIterator, - shader_groups: impl IntoIterator, - ) -> Result - where - S: Into, - { - let shader_groups = shader_groups.into_iter().collect::>(); - let shaders = shaders - .into_iter() - .map(|shader| shader.into()) - .collect::>(); - - let (mut watcher, has_changes) = create_watcher(); - let compiled_shaders = shaders - .iter() - .map(|shader| compile_shader_and_watch(shader, &mut watcher)) - .collect::, _>>()?; - - let instance = Arc::new(RayTracePipeline::create( - device, - info, - compiled_shaders, - shader_groups.iter().copied(), - )?); - - let device = Arc::clone(device); - - Ok(Self { - device, - has_changes, - instance, - shader_groups, - shaders, - watcher, - }) - } - - /// Returns the most recent compilation without checking for changes or re-compiling the shader - /// source code. - pub fn cold(&self) -> &Arc { - &self.instance - } - - /// Returns the most recent compilation after checking for changes, and if needed re-compiling - /// the shader source code. - pub fn hot(&mut self) -> &Arc { - let has_changes = self.has_changes.swap(false, Ordering::Relaxed); - - if has_changes { - info!("Shader change detected"); - - let (mut watcher, has_changes) = create_watcher(); - if let Ok(compiled_shaders) = self - .shaders - .iter() - .map(|shader| compile_shader_and_watch(shader, &mut watcher)) - .collect::, DriverError>>() - { - if let Ok(instance) = RayTracePipeline::create( - &self.device, - self.instance.info, - compiled_shaders, - self.shader_groups.iter().copied(), - ) { - self.has_changes = has_changes; - self.watcher = watcher; - self.instance = Arc::new(instance); - } - } - } - - self.cold() - } -} - -impl AsRef for HotRayTracePipeline { - fn as_ref(&self) -> &RayTracePipeline { - self.instance.as_ref() - } -} diff --git a/contrib/screen-13-imgui/Cargo.toml b/contrib/screen-13-imgui/Cargo.toml deleted file mode 100644 index dee5fc06..00000000 --- a/contrib/screen-13-imgui/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "screen-13-imgui" -version = "0.1.0" -authors = ["John Wells "] -edition = "2021" -license = "MIT OR Apache-2.0" -readme = "README.md" - -[dependencies] -bytemuck = "1.24" -imgui = "0.12" -imgui-winit-support = "0.13" -inline-spirv = "0.2" -screen-13 = { path = "../.." } diff --git a/contrib/screen-13-imgui/README.md b/contrib/screen-13-imgui/README.md deleted file mode 100644 index 884ddb9e..00000000 --- a/contrib/screen-13-imgui/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# _Screen 13_ _Dear imGui_ Integration - -Preview - -[Example code](../../examples/README.md) \ No newline at end of file diff --git a/contrib/screen-13-window/Cargo.toml b/contrib/screen-13-window/Cargo.toml deleted file mode 100644 index 6782eb88..00000000 --- a/contrib/screen-13-window/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "screen-13-window" -version = "0.1.0" -authors = ["John Wells "] -edition = "2021" -license = "MIT OR Apache-2.0" -readme = "README.md" - -[dependencies] -log = "0.4" -profiling = "1.0" -screen-13 = { path = "../.." } -winit = "0.30" - -[dev-dependencies] -pretty_env_logger = "0.5" diff --git a/contrib/screen-13-window/README.md b/contrib/screen-13-window/README.md deleted file mode 100644 index c8af5d02..00000000 --- a/contrib/screen-13-window/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# _Screen 13_ Window Helper - -This crate may be used as a pre-built window handler for examples and simple programs. - -```sh -cargo run --example hello_world -``` - -hello_world.rs diff --git a/crates/README.md b/crates/README.md new file mode 100644 index 00000000..700b5f61 --- /dev/null +++ b/crates/README.md @@ -0,0 +1,17 @@ +# Workspace Crates + +This directory contains the publishable helper crates that extend `vk-graph`. +Each helper crate is versioned independently and has its own changelog. + +## Included Crates + +- [`vk-graph-window`](vk-graph-window/README.md): `winit` integration for window creation and frame + handling. +- [`vk-graph-egui`](vk-graph-egui/README.md): renderer integration for + [egui](https://github.com/emilk/egui). +- [`vk-graph-fx`](vk-graph-fx/README.md): reusable effects and utility helpers built on top of + `vk-graph`. +- [`vk-graph-hot`](vk-graph-hot/README.md): shader hot-reload support for compute, graphic, and + ray-trace pipelines. +- [`vk-graph-imgui`](vk-graph-imgui/README.md): renderer integration for + [Dear ImGui](https://github.com/imgui-rs/imgui-rs). diff --git a/crates/vk-graph-egui/CHANGELOG.md b/crates/vk-graph-egui/CHANGELOG.md new file mode 100644 index 00000000..97d366ed --- /dev/null +++ b/crates/vk-graph-egui/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this crate will be documented in this file. + +This crate is versioned independently from `vk-graph`. + +## [Unreleased] + +## [0.1.0] - 2026-05-30 + +- Initial release. +- Supports `vk-graph` `0.14`. diff --git a/crates/vk-graph-egui/Cargo.toml b/crates/vk-graph-egui/Cargo.toml new file mode 100644 index 00000000..27f12328 --- /dev/null +++ b/crates/vk-graph-egui/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "vk-graph-egui" +version = "0.1.0" +authors = ["Christian Döring "] +description = "egui renderer integration for vk-graph" +documentation = "https://docs.rs/vk-graph-egui" +homepage = "https://github.com/attackgoat/vk-graph/tree/main/crates/vk-graph-egui" +keywords = ["egui", "gamedev", "vulkan"] +categories = ["game-development", "multimedia::images", "rendering::engine"] +edition.workspace = true +license.workspace = true +readme = "README.md" +repository.workspace = true + +[dependencies] +bytemuck = "1.25" +egui = "0.34.3" +egui-winit = "0.34.3" +vk-graph.workspace = true +vk-graph-fx.workspace = true +vk-shader-macros = "0.2" diff --git a/crates/vk-graph-egui/README.md b/crates/vk-graph-egui/README.md new file mode 100644 index 00000000..d3de7428 --- /dev/null +++ b/crates/vk-graph-egui/README.md @@ -0,0 +1,9 @@ +# _vk-graph_ _egui_ Integration + +Renderer integration for [egui](https://github.com/emilk/egui) on top of `vk-graph`. +This crate is versioned independently from `vk-graph`. + +Preview + +See the [example code](../../examples/README.md) and the workspace +[guide book](https://attackgoat.github.io/vk-graph). diff --git a/contrib/screen-13-egui/shaders/frag.glsl b/crates/vk-graph-egui/shaders/egui.frag similarity index 100% rename from contrib/screen-13-egui/shaders/frag.glsl rename to crates/vk-graph-egui/shaders/egui.frag diff --git a/contrib/screen-13-egui/shaders/vert.glsl b/crates/vk-graph-egui/shaders/egui.vert similarity index 96% rename from contrib/screen-13-egui/shaders/vert.glsl rename to crates/vk-graph-egui/shaders/egui.vert index 8f3611e2..b21983c6 100644 --- a/contrib/screen-13-egui/shaders/vert.glsl +++ b/crates/vk-graph-egui/shaders/egui.vert @@ -1,4 +1,5 @@ #version 450 +#pragma shader_stage(vertex) layout(location = 0) in vec2 i_pos; layout(location = 1) in vec2 i_uv; diff --git a/contrib/screen-13-egui/src/lib.rs b/crates/vk-graph-egui/src/lib.rs similarity index 55% rename from contrib/screen-13-egui/src/lib.rs rename to crates/vk-graph-egui/src/lib.rs index 90cde08f..63edfb45 100644 --- a/contrib/screen-13-egui/src/lib.rs +++ b/crates/vk-graph-egui/src/lib.rs @@ -1,60 +1,71 @@ -pub mod prelude { - pub use super::{egui, Egui}; -} +//! `egui` renderer integration for `vk-graph`. + +#![deny(missing_docs)] +#![deny(rustdoc::broken_intra_doc_links)] pub use egui; -use egui_winit::winit::raw_window_handle::HasDisplayHandle; +use vk_graph::{ + cmd::{LoadOp, StoreOp}, + driver::{ + ash::vk, + buffer::BufferInfo, + device::Device, + graphic::{BlendInfo, GraphicPipeline, GraphicPipelineInfo}, + image::{Image, ImageInfo}, + shader::Shader, + sync::AccessType, + }, + node::AnyImageNode, + pool::{Lease, Pool as _, hash::HashPool}, +}; use { bytemuck::cast_slice, - egui_winit::winit::{event::Event, window::Window}, - screen_13::prelude::*, + egui_winit::winit::{event::Event, raw_window_handle::HasDisplayHandle, window::Window}, std::{borrow::Cow, collections::HashMap, sync::Arc}, + vk_graph::Graph, + vk_shader_macros::include_glsl, }; +/// Immediate-mode GUI integration for rendering `egui` output with `vk-graph`. pub struct Egui { + /// The shared `egui` context used to build and retain UI state. pub ctx: egui::Context, + egui_winit: egui_winit::State, textures: HashMap>>, cache: HashPool, - ppl: Arc, + ppl: GraphicPipeline, next_tex_id: u64, user_textures: HashMap, } impl Egui { - pub fn new(device: &Arc, display_target: &dyn HasDisplayHandle) -> Self { - let ppl = Arc::new( - GraphicPipeline::create( - device, - GraphicPipelineInfoBuilder::default() - .blend(BlendMode { - blend_enable: true, - src_color_blend_factor: vk::BlendFactor::ONE, - dst_color_blend_factor: vk::BlendFactor::ONE_MINUS_SRC_ALPHA, - color_blend_op: vk::BlendOp::ADD, - src_alpha_blend_factor: vk::BlendFactor::ONE, - dst_alpha_blend_factor: vk::BlendFactor::ONE, - alpha_blend_op: vk::BlendOp::ADD, - color_write_mask: vk::ColorComponentFlags::R - | vk::ColorComponentFlags::G - | vk::ColorComponentFlags::B - | vk::ColorComponentFlags::A, - }) - .cull_mode(vk::CullModeFlags::NONE), - [ - Shader::new_vertex( - inline_spirv::include_spirv!("shaders/vert.glsl", vert, vulkan1_2) - .as_slice(), - ), - Shader::new_fragment( - inline_spirv::include_spirv!("shaders/frag.glsl", frag, vulkan1_2) - .as_slice(), - ), - ], - ) - .unwrap(), - ); + /// Creates a new `egui` renderer for the given device and display target. + pub fn new(device: &Device, display_target: &dyn HasDisplayHandle) -> Self { + let ppl = GraphicPipeline::create( + device, + GraphicPipelineInfo::builder() + .blend(BlendInfo { + blend_enable: true, + src_color_blend_factor: vk::BlendFactor::ONE, + dst_color_blend_factor: vk::BlendFactor::ONE_MINUS_SRC_ALPHA, + color_blend_op: vk::BlendOp::ADD, + src_alpha_blend_factor: vk::BlendFactor::ONE, + dst_alpha_blend_factor: vk::BlendFactor::ONE, + alpha_blend_op: vk::BlendOp::ADD, + color_write_mask: vk::ColorComponentFlags::R + | vk::ColorComponentFlags::G + | vk::ColorComponentFlags::B + | vk::ColorComponentFlags::A, + }) + .cull_mode(vk::CullModeFlags::NONE), + [ + Shader::new_vertex(include_glsl!("shaders/egui.vert").as_slice()), + Shader::new_fragment(include_glsl!("shaders/egui.frag").as_slice()), + ], + ) + .expect("invalid egui pipeline"); let ctx = egui::Context::default(); let max_texture_side = Some( @@ -87,7 +98,7 @@ impl Egui { fn bind_and_update_textures( &mut self, deltas: &egui::TexturesDelta, - render_graph: &mut RenderGraph, + graph: &mut Graph, ) -> HashMap { let mut bound_tex = deltas .set @@ -98,35 +109,28 @@ impl Egui { assert_eq!(image.width() * image.height(), image.pixels.len()); Cow::Borrowed(&image.pixels) } - egui::ImageData::Font(image) => { - Cow::Owned(image.srgba_pixels(Some(1.)).collect::>()) - } }; let tmp_buf = { let mut buf = self .cache - .lease(BufferInfo::host_mem( + .resource(BufferInfo::host_mem( (pixels.len() * delta.image.bytes_per_pixel()) as u64, vk::BufferUsageFlags::TRANSFER_SRC, )) - .unwrap(); - Buffer::copy_from_slice(&mut buf, 0, cast_slice(&pixels)); - render_graph.bind_node(buf) + .expect("missing egui texture upload buffer"); + buf.copy_from_slice(0, cast_slice(&pixels)); + graph.bind_resource(buf) }; if let Some(pos) = delta.pos { - let image = AnyImageNode::ImageLease( - self.textures - .remove(id) - .expect("Tried updating undefined texture.") - .bind(render_graph), - ); + let image = + graph.bind_resource(self.textures.remove(id).expect("missing texture")); - render_graph.copy_buffer_to_image_region( + graph.copy_buffer_to_image_region( tmp_buf, image, - vk::BufferImageCopy { + [vk::BufferImageCopy { buffer_offset: 0, buffer_row_length: delta.image.width() as u32, buffer_image_height: delta.image.height() as u32, @@ -146,32 +150,30 @@ impl Egui { base_array_layer: 0, layer_count: 1, }, - }, + }], ); - (*id, image) + (*id, AnyImageNode::from(image)) } else { - let image = AnyImageNode::ImageLease( + let image = graph.bind_resource( self.cache - .lease(ImageInfo::image_2d( + .resource(ImageInfo::image_2d( delta.image.width() as u32, delta.image.height() as u32, vk::Format::R8G8B8A8_UNORM, vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST, )) - .unwrap() - .bind(render_graph), + .expect("missing egui texture image"), ); - render_graph.copy_buffer_to_image(tmp_buf, image); - render_graph.unbind_node(tmp_buf); - (*id, image) + graph.copy_buffer_to_image(tmp_buf, image); + (*id, AnyImageNode::from(image)) } }) .collect::>(); // Bind the rest of the textures. for (id, image) in self.textures.drain() { - bound_tex.insert(id, AnyImageNode::ImageLease(render_graph.bind_node(image))); + bound_tex.insert(id, AnyImageNode::from(graph.bind_resource(image))); } // Add user textures. @@ -185,15 +187,15 @@ impl Egui { fn unbind_and_free( &mut self, bound_tex: HashMap, - render_graph: &mut RenderGraph, + graph: &mut Graph, deltas: &egui::TexturesDelta, ) { // Unbind textures for (id, tex) in bound_tex.iter() { - if let AnyImageNode::ImageLease(tex) = tex { - if let egui::TextureId::Managed(_) = *id { - self.textures.insert(*id, render_graph.unbind_node(*tex)); - } + if let AnyImageNode::ImageLease(tex) = tex + && let egui::TextureId::Managed(_) = *id + { + self.textures.insert(*id, graph.resource(*tex).clone()); } } @@ -209,11 +211,11 @@ impl Egui { &mut self, shapes: Vec, bound_tex: &HashMap, - render_graph: &mut RenderGraph, + graph: &mut Graph, target: impl Into, ) { let target = target.into(); - let target_info = render_graph.node_info(target); + let target_info = graph.resource(target).info; for egui::ClippedPrimitive { clip_rect, primitive, @@ -226,34 +228,36 @@ impl Egui { if mesh.vertices.is_empty() || mesh.indices.is_empty() { continue; } - let texture = bound_tex.get(&mesh.texture_id).unwrap(); + let texture = bound_tex + .get(&mesh.texture_id) + .expect("missing egui mesh texture"); let idx_buf = { let mut buf = self .cache - .lease(BufferInfo::host_mem( + .resource(BufferInfo::host_mem( (mesh.indices.len() * 4) as u64, vk::BufferUsageFlags::INDEX_BUFFER, )) - .unwrap(); - Buffer::copy_from_slice(&mut buf, 0, cast_slice(&mesh.indices)); + .expect("missing egui index buffer"); + buf.copy_from_slice(0, cast_slice(&mesh.indices)); buf }; - let idx_buf = render_graph.bind_node(idx_buf); + let idx_buf = graph.bind_resource(idx_buf); let vert_buf = { let mut buf = self .cache - .lease(BufferInfo::host_mem( + .resource(BufferInfo::host_mem( (mesh.vertices.len() * std::mem::size_of::()) as u64, vk::BufferUsageFlags::VERTEX_BUFFER, )) - .unwrap(); - Buffer::copy_from_slice(&mut buf, 0, cast_slice(&mesh.vertices)); + .expect("missing egui vertex buffer"); + buf.copy_from_slice(0, cast_slice(&mesh.vertices)); buf }; - let vert_buf = render_graph.bind_node(vert_buf); + let vert_buf = graph.bind_resource(vert_buf); #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] @@ -272,28 +276,32 @@ impl Egui { let num_indices = mesh.indices.len() as u32; - let clip_x = (clip_rect.min.x * pixels_per_point) as i32; - let clip_y = (clip_rect.min.y * pixels_per_point) as i32; + let x = (clip_rect.min.x * pixels_per_point) as i32; + let y = (clip_rect.min.y * pixels_per_point) as i32; - let clip_width = - ((clip_rect.max.x - clip_rect.min.x) * pixels_per_point) as u32; - let clip_height = - ((clip_rect.max.y - clip_rect.min.y) * pixels_per_point) as u32; + let width = ((clip_rect.max.x - clip_rect.min.x) * pixels_per_point) as u32; + let height = ((clip_rect.max.y - clip_rect.min.y) * pixels_per_point) as u32; - render_graph - .begin_pass("Egui pass") + graph + .begin_cmd() + .debug_name("Egui pass") .bind_pipeline(&self.ppl) - .access_node(idx_buf, AccessType::IndexBuffer) - .access_node(vert_buf, AccessType::VertexBuffer) - .access_descriptor((0, 0), *texture, AccessType::FragmentShaderReadOther) - .load_color(0, target) - .store_color(0, target) - .record_subpass(move |subpass, _| { - subpass.bind_index_buffer(idx_buf, vk::IndexType::UINT32); - subpass.bind_vertex_buffer(vert_buf); - subpass.push_constants(cast_slice(&[push_constants])); - subpass.set_scissor(clip_x, clip_y, clip_width, clip_height); - subpass.draw_indexed(num_indices, 1, 0, 0, 0); + .resource_access(idx_buf, AccessType::IndexBuffer) + .resource_access(vert_buf, AccessType::VertexBuffer) + .shader_resource_access(0, *texture, AccessType::FragmentShaderReadOther) + .color_attachment_image(0, target, LoadOp::Load, StoreOp::Store) + .record_cmd(move |cmd| { + cmd.bind_index_buffer(idx_buf, 0, vk::IndexType::UINT32) + .bind_vertex_buffer(0, vert_buf, 0) + .push_constants(0, cast_slice(&[push_constants])) + .set_scissor( + 0, + &[vk::Rect2D { + offset: vk::Offset2D { x, y }, + extent: vk::Extent2D { width, height }, + }], + ) + .draw_indexed(num_indices, 1, 0, 0, 0); }); } _ => panic!("Primitiv callback not yet supported."), @@ -301,13 +309,14 @@ impl Egui { } } + /// Runs one `egui` frame, updates textures, and records draw commands into `graph`. pub fn run( &mut self, window: &Window, events: &[Event<()>], target: impl Into, - render_graph: &mut RenderGraph, - ui_fn: impl FnMut(&egui::Context), + graph: &mut Graph, + ui_fn: impl FnMut(&mut egui::Ui), ) { // Update events and generate shapes and texture deltas. for event in events { @@ -319,20 +328,21 @@ impl Egui { } } let raw_input = self.egui_winit.take_egui_input(window); - let full_output = self.ctx.run(raw_input, ui_fn); + let full_output = self.ctx.run_ui(raw_input, ui_fn); self.egui_winit .handle_platform_output(window, full_output.platform_output); let deltas = full_output.textures_delta; - let bound_tex = self.bind_and_update_textures(&deltas, render_graph); + let bound_tex = self.bind_and_update_textures(&deltas, graph); - self.draw_primitive(full_output.shapes, &bound_tex, render_graph, target); + self.draw_primitive(full_output.shapes, &bound_tex, graph, target); - self.unbind_and_free(bound_tex, render_graph, &deltas); + self.unbind_and_free(bound_tex, graph, &deltas); } + /// Registers a user-provided image so it can be referenced from `egui`. pub fn register_texture(&mut self, tex: impl Into) -> egui::TextureId { let id = egui::TextureId::User(self.next_tex_id); self.next_tex_id += 1; diff --git a/crates/vk-graph-fx/CHANGELOG.md b/crates/vk-graph-fx/CHANGELOG.md new file mode 100644 index 00000000..97d366ed --- /dev/null +++ b/crates/vk-graph-fx/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this crate will be documented in this file. + +This crate is versioned independently from `vk-graph`. + +## [Unreleased] + +## [0.1.0] - 2026-05-30 + +- Initial release. +- Supports `vk-graph` `0.14`. diff --git a/contrib/screen-13-fx/Cargo.toml b/crates/vk-graph-fx/Cargo.toml similarity index 60% rename from contrib/screen-13-fx/Cargo.toml rename to crates/vk-graph-fx/Cargo.toml index e067d2ae..455a38bf 100644 --- a/contrib/screen-13-fx/Cargo.toml +++ b/crates/vk-graph-fx/Cargo.toml @@ -1,16 +1,16 @@ [package] -name = "screen-13-fx" +name = "vk-graph-fx" version = "0.1.0" authors = ["John Wells "] -edition = "2021" -license = "MIT OR Apache-2.0" -readme = "README.md" -repository = "https://github.com/attackgoat/screen-13" -homepage = "https://github.com/attackgoat/screen-13/contrib/screen-13-fx" -documentation = "https://docs.rs/screen-13" +documentation = "https://docs.rs/vk-graph-fx" +homepage = "https://github.com/attackgoat/vk-graph/tree/main/crates/vk-graph-fx" keywords = ["gamedev", "vulkan"] categories = ["game-development", "multimedia::images", "rendering::engine"] -description = "A bunch of pre-built utilities that are helpful when using Screen 13" +description = "A bunch of pre-built utilities that are helpful when using vk-graph" +edition.workspace = true +license.workspace = true +readme = "README.md" +repository.workspace = true [features] default = [ @@ -24,10 +24,10 @@ matte-modes = [] [dependencies] bmfont = { version = "0.3", default-features = false } -bytemuck = "1.14" +bytemuck = "1.25" parking_lot = "0.12" -inline-spirv = "0.2" -log = "0.4" -screen-13 = { path = "../.."} +log.workspace = true anyhow = "1.0" glam = "0.27" +vk-graph.workspace = true +vk-shader-macros = "0.2" diff --git a/crates/vk-graph-fx/README.md b/crates/vk-graph-fx/README.md new file mode 100644 index 00000000..21a90d21 --- /dev/null +++ b/crates/vk-graph-fx/README.md @@ -0,0 +1,8 @@ +# _vk-graph_ Fx + +Reusable effects and utilities built on top of `vk-graph`, including image loading, bitmap font +rendering, presentation helpers, and transition pipelines. +This crate is versioned independently from `vk-graph`. + +See the workspace [examples](../../examples/README.md) and +[guide book](https://attackgoat.github.io/vk-graph) for usage patterns. diff --git a/contrib/screen-13-fx/res/shader/compute/decode_bitmap_r_rg.comp b/crates/vk-graph-fx/res/shader/compute/decode_bitmap_r_rg.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/compute/decode_bitmap_r_rg.comp rename to crates/vk-graph-fx/res/shader/compute/decode_bitmap_r_rg.comp diff --git a/contrib/screen-13-fx/res/shader/compute/decode_bitmap_rgb_rgba.comp b/crates/vk-graph-fx/res/shader/compute/decode_bitmap_rgb_rgba.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/compute/decode_bitmap_rgb_rgba.comp rename to crates/vk-graph-fx/res/shader/compute/decode_bitmap_rgb_rgba.comp diff --git a/contrib/screen-13-fx/res/shader/compute/present1.comp b/crates/vk-graph-fx/res/shader/compute/present1.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/compute/present1.comp rename to crates/vk-graph-fx/res/shader/compute/present1.comp diff --git a/contrib/screen-13-fx/res/shader/compute/present2.comp b/crates/vk-graph-fx/res/shader/compute/present2.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/compute/present2.comp rename to crates/vk-graph-fx/res/shader/compute/present2.comp diff --git a/contrib/screen-13-fx/res/shader/graphic/font.frag b/crates/vk-graph-fx/res/shader/graphic/font.frag similarity index 100% rename from contrib/screen-13-fx/res/shader/graphic/font.frag rename to crates/vk-graph-fx/res/shader/graphic/font.frag diff --git a/contrib/screen-13-fx/res/shader/graphic/font.vert b/crates/vk-graph-fx/res/shader/graphic/font.vert similarity index 100% rename from contrib/screen-13-fx/res/shader/graphic/font.vert rename to crates/vk-graph-fx/res/shader/graphic/font.vert diff --git a/contrib/screen-13-fx/res/shader/graphic/present.frag b/crates/vk-graph-fx/res/shader/graphic/present.frag similarity index 100% rename from contrib/screen-13-fx/res/shader/graphic/present.frag rename to crates/vk-graph-fx/res/shader/graphic/present.frag diff --git a/contrib/screen-13-fx/res/shader/graphic/present.vert b/crates/vk-graph-fx/res/shader/graphic/present.vert similarity index 100% rename from contrib/screen-13-fx/res/shader/graphic/present.vert rename to crates/vk-graph-fx/res/shader/graphic/present.vert diff --git a/contrib/screen-13-fx/res/shader/inc/catmull_rom.glsl b/crates/vk-graph-fx/res/shader/inc/catmull_rom.glsl similarity index 100% rename from contrib/screen-13-fx/res/shader/inc/catmull_rom.glsl rename to crates/vk-graph-fx/res/shader/inc/catmull_rom.glsl diff --git a/contrib/screen-13-fx/res/shader/inc/color_space.glsl b/crates/vk-graph-fx/res/shader/inc/color_space.glsl similarity index 100% rename from contrib/screen-13-fx/res/shader/inc/color_space.glsl rename to crates/vk-graph-fx/res/shader/inc/color_space.glsl diff --git a/contrib/screen-13-fx/res/shader/inc/content.glsl b/crates/vk-graph-fx/res/shader/inc/content.glsl similarity index 100% rename from contrib/screen-13-fx/res/shader/inc/content.glsl rename to crates/vk-graph-fx/res/shader/inc/content.glsl diff --git a/contrib/screen-13-fx/res/shader/inc/quad.glsl b/crates/vk-graph-fx/res/shader/inc/quad.glsl similarity index 100% rename from contrib/screen-13-fx/res/shader/inc/quad.glsl rename to crates/vk-graph-fx/res/shader/inc/quad.glsl diff --git a/contrib/screen-13-fx/res/shader/transition/_defs.glsl b/crates/vk-graph-fx/res/shader/transition/_defs.glsl similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/_defs.glsl rename to crates/vk-graph-fx/res/shader/transition/_defs.glsl diff --git a/contrib/screen-13-fx/res/shader/transition/_main.glsl b/crates/vk-graph-fx/res/shader/transition/_main.glsl similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/_main.glsl rename to crates/vk-graph-fx/res/shader/transition/_main.glsl diff --git a/contrib/screen-13-fx/res/shader/transition/angular.comp b/crates/vk-graph-fx/res/shader/transition/angular.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/angular.comp rename to crates/vk-graph-fx/res/shader/transition/angular.comp diff --git a/contrib/screen-13-fx/res/shader/transition/bounce.comp b/crates/vk-graph-fx/res/shader/transition/bounce.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/bounce.comp rename to crates/vk-graph-fx/res/shader/transition/bounce.comp diff --git a/contrib/screen-13-fx/res/shader/transition/bow_tie_horizontal.comp b/crates/vk-graph-fx/res/shader/transition/bow_tie_horizontal.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/bow_tie_horizontal.comp rename to crates/vk-graph-fx/res/shader/transition/bow_tie_horizontal.comp diff --git a/contrib/screen-13-fx/res/shader/transition/bow_tie_vertical.comp b/crates/vk-graph-fx/res/shader/transition/bow_tie_vertical.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/bow_tie_vertical.comp rename to crates/vk-graph-fx/res/shader/transition/bow_tie_vertical.comp diff --git a/contrib/screen-13-fx/res/shader/transition/bow_tie_with_parameter.comp b/crates/vk-graph-fx/res/shader/transition/bow_tie_with_parameter.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/bow_tie_with_parameter.comp rename to crates/vk-graph-fx/res/shader/transition/bow_tie_with_parameter.comp diff --git a/contrib/screen-13-fx/res/shader/transition/burn.comp b/crates/vk-graph-fx/res/shader/transition/burn.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/burn.comp rename to crates/vk-graph-fx/res/shader/transition/burn.comp diff --git a/contrib/screen-13-fx/res/shader/transition/butterfly_wave_scrawler.comp b/crates/vk-graph-fx/res/shader/transition/butterfly_wave_scrawler.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/butterfly_wave_scrawler.comp rename to crates/vk-graph-fx/res/shader/transition/butterfly_wave_scrawler.comp diff --git a/contrib/screen-13-fx/res/shader/transition/cannabis_leaf.comp b/crates/vk-graph-fx/res/shader/transition/cannabis_leaf.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/cannabis_leaf.comp rename to crates/vk-graph-fx/res/shader/transition/cannabis_leaf.comp diff --git a/contrib/screen-13-fx/res/shader/transition/circle.comp b/crates/vk-graph-fx/res/shader/transition/circle.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/circle.comp rename to crates/vk-graph-fx/res/shader/transition/circle.comp diff --git a/contrib/screen-13-fx/res/shader/transition/circle_crop.comp b/crates/vk-graph-fx/res/shader/transition/circle_crop.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/circle_crop.comp rename to crates/vk-graph-fx/res/shader/transition/circle_crop.comp diff --git a/contrib/screen-13-fx/res/shader/transition/circle_open.comp b/crates/vk-graph-fx/res/shader/transition/circle_open.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/circle_open.comp rename to crates/vk-graph-fx/res/shader/transition/circle_open.comp diff --git a/contrib/screen-13-fx/res/shader/transition/color_distance.comp b/crates/vk-graph-fx/res/shader/transition/color_distance.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/color_distance.comp rename to crates/vk-graph-fx/res/shader/transition/color_distance.comp diff --git a/contrib/screen-13-fx/res/shader/transition/color_phase.comp b/crates/vk-graph-fx/res/shader/transition/color_phase.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/color_phase.comp rename to crates/vk-graph-fx/res/shader/transition/color_phase.comp diff --git a/contrib/screen-13-fx/res/shader/transition/coord_from_in.comp b/crates/vk-graph-fx/res/shader/transition/coord_from_in.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/coord_from_in.comp rename to crates/vk-graph-fx/res/shader/transition/coord_from_in.comp diff --git a/contrib/screen-13-fx/res/shader/transition/crazy_parametric_fun.comp b/crates/vk-graph-fx/res/shader/transition/crazy_parametric_fun.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/crazy_parametric_fun.comp rename to crates/vk-graph-fx/res/shader/transition/crazy_parametric_fun.comp diff --git a/contrib/screen-13-fx/res/shader/transition/cross_warp.comp b/crates/vk-graph-fx/res/shader/transition/cross_warp.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/cross_warp.comp rename to crates/vk-graph-fx/res/shader/transition/cross_warp.comp diff --git a/contrib/screen-13-fx/res/shader/transition/cross_zoom.comp b/crates/vk-graph-fx/res/shader/transition/cross_zoom.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/cross_zoom.comp rename to crates/vk-graph-fx/res/shader/transition/cross_zoom.comp diff --git a/contrib/screen-13-fx/res/shader/transition/crosshatch.comp b/crates/vk-graph-fx/res/shader/transition/crosshatch.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/crosshatch.comp rename to crates/vk-graph-fx/res/shader/transition/crosshatch.comp diff --git a/contrib/screen-13-fx/res/shader/transition/cube.comp b/crates/vk-graph-fx/res/shader/transition/cube.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/cube.comp rename to crates/vk-graph-fx/res/shader/transition/cube.comp diff --git a/contrib/screen-13-fx/res/shader/transition/directional.comp b/crates/vk-graph-fx/res/shader/transition/directional.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/directional.comp rename to crates/vk-graph-fx/res/shader/transition/directional.comp diff --git a/contrib/screen-13-fx/res/shader/transition/directional_easing.comp b/crates/vk-graph-fx/res/shader/transition/directional_easing.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/directional_easing.comp rename to crates/vk-graph-fx/res/shader/transition/directional_easing.comp diff --git a/contrib/screen-13-fx/res/shader/transition/directional_warp.comp b/crates/vk-graph-fx/res/shader/transition/directional_warp.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/directional_warp.comp rename to crates/vk-graph-fx/res/shader/transition/directional_warp.comp diff --git a/contrib/screen-13-fx/res/shader/transition/directional_wipe.comp b/crates/vk-graph-fx/res/shader/transition/directional_wipe.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/directional_wipe.comp rename to crates/vk-graph-fx/res/shader/transition/directional_wipe.comp diff --git a/contrib/screen-13-fx/res/shader/transition/displacement.comp b/crates/vk-graph-fx/res/shader/transition/displacement.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/displacement.comp rename to crates/vk-graph-fx/res/shader/transition/displacement.comp diff --git a/contrib/screen-13-fx/res/shader/transition/doom_screen.comp b/crates/vk-graph-fx/res/shader/transition/doom_screen.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/doom_screen.comp rename to crates/vk-graph-fx/res/shader/transition/doom_screen.comp diff --git a/contrib/screen-13-fx/res/shader/transition/doorway.comp b/crates/vk-graph-fx/res/shader/transition/doorway.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/doorway.comp rename to crates/vk-graph-fx/res/shader/transition/doorway.comp diff --git a/contrib/screen-13-fx/res/shader/transition/dreamy.comp b/crates/vk-graph-fx/res/shader/transition/dreamy.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/dreamy.comp rename to crates/vk-graph-fx/res/shader/transition/dreamy.comp diff --git a/contrib/screen-13-fx/res/shader/transition/dreamy_zoom.comp b/crates/vk-graph-fx/res/shader/transition/dreamy_zoom.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/dreamy_zoom.comp rename to crates/vk-graph-fx/res/shader/transition/dreamy_zoom.comp diff --git a/contrib/screen-13-fx/res/shader/transition/fade.comp b/crates/vk-graph-fx/res/shader/transition/fade.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/fade.comp rename to crates/vk-graph-fx/res/shader/transition/fade.comp diff --git a/contrib/screen-13-fx/res/shader/transition/fade_color.comp b/crates/vk-graph-fx/res/shader/transition/fade_color.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/fade_color.comp rename to crates/vk-graph-fx/res/shader/transition/fade_color.comp diff --git a/contrib/screen-13-fx/res/shader/transition/fade_grayscale.comp b/crates/vk-graph-fx/res/shader/transition/fade_grayscale.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/fade_grayscale.comp rename to crates/vk-graph-fx/res/shader/transition/fade_grayscale.comp diff --git a/contrib/screen-13-fx/res/shader/transition/film_burn.comp b/crates/vk-graph-fx/res/shader/transition/film_burn.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/film_burn.comp rename to crates/vk-graph-fx/res/shader/transition/film_burn.comp diff --git a/contrib/screen-13-fx/res/shader/transition/flyeye.comp b/crates/vk-graph-fx/res/shader/transition/flyeye.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/flyeye.comp rename to crates/vk-graph-fx/res/shader/transition/flyeye.comp diff --git a/contrib/screen-13-fx/res/shader/transition/glitch_displace.comp b/crates/vk-graph-fx/res/shader/transition/glitch_displace.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/glitch_displace.comp rename to crates/vk-graph-fx/res/shader/transition/glitch_displace.comp diff --git a/contrib/screen-13-fx/res/shader/transition/glitch_memories.comp b/crates/vk-graph-fx/res/shader/transition/glitch_memories.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/glitch_memories.comp rename to crates/vk-graph-fx/res/shader/transition/glitch_memories.comp diff --git a/contrib/screen-13-fx/res/shader/transition/grid_flip.comp b/crates/vk-graph-fx/res/shader/transition/grid_flip.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/grid_flip.comp rename to crates/vk-graph-fx/res/shader/transition/grid_flip.comp diff --git a/contrib/screen-13-fx/res/shader/transition/heart.comp b/crates/vk-graph-fx/res/shader/transition/heart.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/heart.comp rename to crates/vk-graph-fx/res/shader/transition/heart.comp diff --git a/contrib/screen-13-fx/res/shader/transition/hexagonalize.comp b/crates/vk-graph-fx/res/shader/transition/hexagonalize.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/hexagonalize.comp rename to crates/vk-graph-fx/res/shader/transition/hexagonalize.comp diff --git a/contrib/screen-13-fx/res/shader/transition/inverted_page_curl.comp b/crates/vk-graph-fx/res/shader/transition/inverted_page_curl.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/inverted_page_curl.comp rename to crates/vk-graph-fx/res/shader/transition/inverted_page_curl.comp diff --git a/contrib/screen-13-fx/res/shader/transition/kaleidoscope.comp b/crates/vk-graph-fx/res/shader/transition/kaleidoscope.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/kaleidoscope.comp rename to crates/vk-graph-fx/res/shader/transition/kaleidoscope.comp diff --git a/contrib/screen-13-fx/res/shader/transition/left_right.comp b/crates/vk-graph-fx/res/shader/transition/left_right.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/left_right.comp rename to crates/vk-graph-fx/res/shader/transition/left_right.comp diff --git a/contrib/screen-13-fx/res/shader/transition/linear_blur.comp b/crates/vk-graph-fx/res/shader/transition/linear_blur.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/linear_blur.comp rename to crates/vk-graph-fx/res/shader/transition/linear_blur.comp diff --git a/contrib/screen-13-fx/res/shader/transition/luma.comp b/crates/vk-graph-fx/res/shader/transition/luma.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/luma.comp rename to crates/vk-graph-fx/res/shader/transition/luma.comp diff --git a/contrib/screen-13-fx/res/shader/transition/luminance_melt.comp b/crates/vk-graph-fx/res/shader/transition/luminance_melt.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/luminance_melt.comp rename to crates/vk-graph-fx/res/shader/transition/luminance_melt.comp diff --git a/contrib/screen-13-fx/res/shader/transition/morph.comp b/crates/vk-graph-fx/res/shader/transition/morph.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/morph.comp rename to crates/vk-graph-fx/res/shader/transition/morph.comp diff --git a/contrib/screen-13-fx/res/shader/transition/mosaic.comp b/crates/vk-graph-fx/res/shader/transition/mosaic.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/mosaic.comp rename to crates/vk-graph-fx/res/shader/transition/mosaic.comp diff --git a/contrib/screen-13-fx/res/shader/transition/multiply.comp b/crates/vk-graph-fx/res/shader/transition/multiply.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/multiply.comp rename to crates/vk-graph-fx/res/shader/transition/multiply.comp diff --git a/contrib/screen-13-fx/res/shader/transition/overexposure.comp b/crates/vk-graph-fx/res/shader/transition/overexposure.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/overexposure.comp rename to crates/vk-graph-fx/res/shader/transition/overexposure.comp diff --git a/contrib/screen-13-fx/res/shader/transition/perlin.comp b/crates/vk-graph-fx/res/shader/transition/perlin.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/perlin.comp rename to crates/vk-graph-fx/res/shader/transition/perlin.comp diff --git a/contrib/screen-13-fx/res/shader/transition/pinwheel.comp b/crates/vk-graph-fx/res/shader/transition/pinwheel.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/pinwheel.comp rename to crates/vk-graph-fx/res/shader/transition/pinwheel.comp diff --git a/contrib/screen-13-fx/res/shader/transition/pixelize.comp b/crates/vk-graph-fx/res/shader/transition/pixelize.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/pixelize.comp rename to crates/vk-graph-fx/res/shader/transition/pixelize.comp diff --git a/contrib/screen-13-fx/res/shader/transition/polar_function.comp b/crates/vk-graph-fx/res/shader/transition/polar_function.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/polar_function.comp rename to crates/vk-graph-fx/res/shader/transition/polar_function.comp diff --git a/contrib/screen-13-fx/res/shader/transition/polka_dots_curtain.comp b/crates/vk-graph-fx/res/shader/transition/polka_dots_curtain.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/polka_dots_curtain.comp rename to crates/vk-graph-fx/res/shader/transition/polka_dots_curtain.comp diff --git a/contrib/screen-13-fx/res/shader/transition/power_kaleido.comp b/crates/vk-graph-fx/res/shader/transition/power_kaleido.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/power_kaleido.comp rename to crates/vk-graph-fx/res/shader/transition/power_kaleido.comp diff --git a/contrib/screen-13-fx/res/shader/transition/radial.comp b/crates/vk-graph-fx/res/shader/transition/radial.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/radial.comp rename to crates/vk-graph-fx/res/shader/transition/radial.comp diff --git a/contrib/screen-13-fx/res/shader/transition/random_noisex.comp b/crates/vk-graph-fx/res/shader/transition/random_noisex.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/random_noisex.comp rename to crates/vk-graph-fx/res/shader/transition/random_noisex.comp diff --git a/contrib/screen-13-fx/res/shader/transition/random_squares.comp b/crates/vk-graph-fx/res/shader/transition/random_squares.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/random_squares.comp rename to crates/vk-graph-fx/res/shader/transition/random_squares.comp diff --git a/contrib/screen-13-fx/res/shader/transition/ripple.comp b/crates/vk-graph-fx/res/shader/transition/ripple.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/ripple.comp rename to crates/vk-graph-fx/res/shader/transition/ripple.comp diff --git a/contrib/screen-13-fx/res/shader/transition/rotate.comp b/crates/vk-graph-fx/res/shader/transition/rotate.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/rotate.comp rename to crates/vk-graph-fx/res/shader/transition/rotate.comp diff --git a/contrib/screen-13-fx/res/shader/transition/rotate_scale.comp b/crates/vk-graph-fx/res/shader/transition/rotate_scale.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/rotate_scale.comp rename to crates/vk-graph-fx/res/shader/transition/rotate_scale.comp diff --git a/contrib/screen-13-fx/res/shader/transition/scale_in.comp b/crates/vk-graph-fx/res/shader/transition/scale_in.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/scale_in.comp rename to crates/vk-graph-fx/res/shader/transition/scale_in.comp diff --git a/contrib/screen-13-fx/res/shader/transition/simple_zoom.comp b/crates/vk-graph-fx/res/shader/transition/simple_zoom.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/simple_zoom.comp rename to crates/vk-graph-fx/res/shader/transition/simple_zoom.comp diff --git a/contrib/screen-13-fx/res/shader/transition/squares_wire.comp b/crates/vk-graph-fx/res/shader/transition/squares_wire.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/squares_wire.comp rename to crates/vk-graph-fx/res/shader/transition/squares_wire.comp diff --git a/contrib/screen-13-fx/res/shader/transition/squeeze.comp b/crates/vk-graph-fx/res/shader/transition/squeeze.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/squeeze.comp rename to crates/vk-graph-fx/res/shader/transition/squeeze.comp diff --git a/contrib/screen-13-fx/res/shader/transition/stereo_viewer.comp b/crates/vk-graph-fx/res/shader/transition/stereo_viewer.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/stereo_viewer.comp rename to crates/vk-graph-fx/res/shader/transition/stereo_viewer.comp diff --git a/contrib/screen-13-fx/res/shader/transition/swap.comp b/crates/vk-graph-fx/res/shader/transition/swap.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/swap.comp rename to crates/vk-graph-fx/res/shader/transition/swap.comp diff --git a/contrib/screen-13-fx/res/shader/transition/swirl.comp b/crates/vk-graph-fx/res/shader/transition/swirl.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/swirl.comp rename to crates/vk-graph-fx/res/shader/transition/swirl.comp diff --git a/contrib/screen-13-fx/res/shader/transition/tangent_motion_blur.comp b/crates/vk-graph-fx/res/shader/transition/tangent_motion_blur.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/tangent_motion_blur.comp rename to crates/vk-graph-fx/res/shader/transition/tangent_motion_blur.comp diff --git a/contrib/screen-13-fx/res/shader/transition/top_bottom.comp b/crates/vk-graph-fx/res/shader/transition/top_bottom.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/top_bottom.comp rename to crates/vk-graph-fx/res/shader/transition/top_bottom.comp diff --git a/contrib/screen-13-fx/res/shader/transition/tv_static.comp b/crates/vk-graph-fx/res/shader/transition/tv_static.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/tv_static.comp rename to crates/vk-graph-fx/res/shader/transition/tv_static.comp diff --git a/contrib/screen-13-fx/res/shader/transition/undulating_burn_out.comp b/crates/vk-graph-fx/res/shader/transition/undulating_burn_out.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/undulating_burn_out.comp rename to crates/vk-graph-fx/res/shader/transition/undulating_burn_out.comp diff --git a/contrib/screen-13-fx/res/shader/transition/water_drop.comp b/crates/vk-graph-fx/res/shader/transition/water_drop.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/water_drop.comp rename to crates/vk-graph-fx/res/shader/transition/water_drop.comp diff --git a/contrib/screen-13-fx/res/shader/transition/wind.comp b/crates/vk-graph-fx/res/shader/transition/wind.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/wind.comp rename to crates/vk-graph-fx/res/shader/transition/wind.comp diff --git a/contrib/screen-13-fx/res/shader/transition/window_blinds.comp b/crates/vk-graph-fx/res/shader/transition/window_blinds.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/window_blinds.comp rename to crates/vk-graph-fx/res/shader/transition/window_blinds.comp diff --git a/contrib/screen-13-fx/res/shader/transition/window_slice.comp b/crates/vk-graph-fx/res/shader/transition/window_slice.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/window_slice.comp rename to crates/vk-graph-fx/res/shader/transition/window_slice.comp diff --git a/contrib/screen-13-fx/res/shader/transition/wipe_down.comp b/crates/vk-graph-fx/res/shader/transition/wipe_down.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/wipe_down.comp rename to crates/vk-graph-fx/res/shader/transition/wipe_down.comp diff --git a/contrib/screen-13-fx/res/shader/transition/wipe_left.comp b/crates/vk-graph-fx/res/shader/transition/wipe_left.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/wipe_left.comp rename to crates/vk-graph-fx/res/shader/transition/wipe_left.comp diff --git a/contrib/screen-13-fx/res/shader/transition/wipe_right.comp b/crates/vk-graph-fx/res/shader/transition/wipe_right.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/wipe_right.comp rename to crates/vk-graph-fx/res/shader/transition/wipe_right.comp diff --git a/contrib/screen-13-fx/res/shader/transition/wipe_up.comp b/crates/vk-graph-fx/res/shader/transition/wipe_up.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/wipe_up.comp rename to crates/vk-graph-fx/res/shader/transition/wipe_up.comp diff --git a/contrib/screen-13-fx/res/shader/transition/zoom_in_circles.comp b/crates/vk-graph-fx/res/shader/transition/zoom_in_circles.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/zoom_in_circles.comp rename to crates/vk-graph-fx/res/shader/transition/zoom_in_circles.comp diff --git a/contrib/screen-13-fx/res/shader/transition/zoom_left_wipe.comp b/crates/vk-graph-fx/res/shader/transition/zoom_left_wipe.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/zoom_left_wipe.comp rename to crates/vk-graph-fx/res/shader/transition/zoom_left_wipe.comp diff --git a/contrib/screen-13-fx/res/shader/transition/zoom_right_wipe.comp b/crates/vk-graph-fx/res/shader/transition/zoom_right_wipe.comp similarity index 100% rename from contrib/screen-13-fx/res/shader/transition/zoom_right_wipe.comp rename to crates/vk-graph-fx/res/shader/transition/zoom_right_wipe.comp diff --git a/contrib/screen-13-fx/src/bitmap_font.rs b/crates/vk-graph-fx/src/bitmap_font.rs similarity index 68% rename from contrib/screen-13-fx/src/bitmap_font.rs rename to crates/vk-graph-fx/src/bitmap_font.rs index 407de9b5..817750b9 100644 --- a/contrib/screen-13-fx/src/bitmap_font.rs +++ b/crates/vk-graph-fx/src/bitmap_font.rs @@ -2,10 +2,24 @@ use { anyhow::Context, bmfont::BMFont, bytemuck::{cast, cast_slice}, - glam::{vec3, Mat4}, - inline_spirv::include_spirv, - screen_13::prelude::*, + glam::{Mat4, vec3}, std::sync::Arc, + vk_graph::{ + Graph, + cmd::{LoadOp, StoreOp}, + driver::{ + ash::vk, + buffer::{Buffer, BufferInfo}, + device::Device, + graphic::{BlendInfo, GraphicPipeline, GraphicPipelineInfo}, + image::Image, + shader::{Shader, SpecializationMap}, + sync::AccessType, + }, + node::{AnyImageNode, ImageNode}, + pool::{Pool as _, lazy::LazyPool}, + }, + vk_shader_macros::include_glsl, }; type Color = [u8; 4]; @@ -22,50 +36,40 @@ fn color_to_unorm(color: Color) -> [u8; 16] { /// Holds a decoded bitmap Font. #[derive(Debug)] pub struct BitmapFont { - cache: HashPool, font: BMFont, pages: Vec>, - pipeline: Arc, + pipeline: GraphicPipeline, + pool: LazyPool, } impl BitmapFont { + /// Creates a bitmap font renderer from decoded font metadata and page images. pub fn new( - device: &Arc, + device: &Device, font: BMFont, pages: impl Into>>, ) -> anyhow::Result { - let cache = HashPool::new(device); + let pool = LazyPool::new(device); let pages = pages.into(); let num_pages = pages.len() as u32; - let pipeline = Arc::new( - GraphicPipeline::create( - device, - GraphicPipelineInfoBuilder::default().blend(BlendMode::ALPHA), - [ - Shader::new_vertex( - include_spirv!("res/shader/graphic/font.vert", vert).as_slice(), + let pipeline = GraphicPipeline::create( + device, + GraphicPipelineInfo::builder().blend(BlendInfo::ALPHA), + [ + Shader::new_vertex(include_glsl!("res/shader/graphic/font.vert").as_slice()), + Shader::new_fragment(include_glsl!("res/shader/graphic/font.frag").as_slice()) + .specialization( + SpecializationMap::new(num_pages.to_ne_bytes()).constant(0, 0, 4), ), - Shader::new_fragment( - include_spirv!("res/shader/graphic/font.frag", frag).as_slice(), - ) - .specialization_info(SpecializationInfo::new( - [vk::SpecializationMapEntry { - constant_id: 0, - offset: 0, - size: 4, - }], - num_pages.to_ne_bytes(), - )), - ], - ) - .context("Unable to create bitmap font pipeline")?, - ); + ], + ) + .context("Unable to create bitmap font pipeline")?; Ok(Self { - cache, font, pages, pipeline, + pool, }) } @@ -108,9 +112,10 @@ impl BitmapFont { (position, size) } + /// Prints text at the given position using the default scale factor of `1.0`. pub fn print( &mut self, - graph: &mut RenderGraph, + graph: &mut Graph, image: impl Into, x: f32, y: f32, @@ -121,10 +126,11 @@ impl BitmapFont { } // TODO: Better API, but not sure what, probably builder-something + /// Prints text at the given position using a caller-specified scale factor. #[allow(clippy::too_many_arguments)] pub fn print_scale( &mut self, - graph: &mut RenderGraph, + graph: &mut Graph, image: impl Into, x: f32, y: f32, @@ -136,10 +142,11 @@ impl BitmapFont { } // TODO: Better API, but not sure what, probably builder-something + /// Prints text with an optional scissor rectangle and caller-specified scale factor. #[allow(clippy::too_many_arguments)] pub fn print_scale_scissor( &mut self, - graph: &mut RenderGraph, + graph: &mut Graph, image: impl Into, x: f32, y: f32, @@ -151,7 +158,7 @@ impl BitmapFont { let color = color.into(); let image = image.into(); let text = text.as_ref(); - let image_info = graph.node_info(image); + let image_info = graph.resource(image).info; let transform = Mat4::from_translation(vec3(-1.0, -1.0, 0.0)) * Mat4::from_scale(vec3(2.0 * scale, 2.0 * scale, 1.0)) * Mat4::from_translation(vec3( @@ -162,12 +169,12 @@ impl BitmapFont { let vertex_buf_len = 120 * text.chars().count() as vk::DeviceSize; let mut vertex_buf = self - .cache - .lease(BufferInfo::host_mem( + .pool + .resource(BufferInfo::host_mem( vertex_buf_len, vk::BufferUsageFlags::VERTEX_BUFFER, )) - .unwrap(); + .expect("missing bitmap font vertex buffer"); let mut vertex_count = 0; @@ -198,44 +205,61 @@ impl BitmapFont { } } - let vertex_buf = graph.bind_node(vertex_buf); + let vertex_buf = graph.bind_resource(vertex_buf); let mut page_nodes: Vec = Vec::with_capacity(self.pages.len()); for page in self.pages.iter() { - page_nodes.push(graph.bind_node(page)); + page_nodes.push(graph.bind_resource(page)); } - let mut pass = graph - .begin_pass("text") + let mut cmd = graph + .begin_cmd() + .debug_name("text") .bind_pipeline(&self.pipeline) - .access_node(vertex_buf, AccessType::IndexBuffer) - .load_color(0, image) - .store_color(0, image); - - for (idx, page_node) in page_nodes.iter().enumerate() { - pass = pass.read_descriptor((0, [idx as _]), *page_node); + .resource_access(vertex_buf, AccessType::VertexBuffer) + .color_attachment_image(0, image, LoadOp::Load, StoreOp::Store); + + for (idx, page_node) in page_nodes.iter().copied().enumerate() { + let descriptor = (0, [idx as _]); + cmd.set_shader_resource_access( + descriptor, + page_node, + AccessType::FragmentShaderReadSampledImageOrUniformTexelBuffer, + ); } - pass.record_subpass(move |subpass, _| { + cmd.record_cmd(move |cmd| { if let Some((x, y, width, height)) = scissor { - subpass.set_scissor(x, y, width, height); + cmd.set_scissor( + 0, + &[vk::Rect2D { + offset: vk::Offset2D { x, y }, + extent: vk::Extent2D { width, height }, + }], + ); } - subpass - .push_constants(cast_slice(&transform.to_cols_array())) - .push_constants_offset(64, &(1.0 / image_info.width as f32).to_ne_bytes()) - .push_constants_offset(68, &(1.0 / image_info.height as f32).to_ne_bytes()) - .push_constants_offset(80, &color_to_unorm(color.solid())) - .push_constants_offset(96, &color_to_unorm(color.outline())) - .bind_vertex_buffer(vertex_buf) + cmd.push_constants(0, cast_slice(&transform.to_cols_array())) + .push_constants(64, &(1.0 / image_info.width as f32).to_ne_bytes()) + .push_constants(68, &(1.0 / image_info.height as f32).to_ne_bytes()) + .push_constants(80, &color_to_unorm(color.solid())) + .push_constants(96, &color_to_unorm(color.outline())) + .bind_vertex_buffer(0, vertex_buf, 0) .draw(vertex_count, 1, 0, 0); }); } } +/// Color selection modes for bitmap glyph rendering. +#[derive(Debug)] pub enum BitmapGlyphColor { + /// Render only the glyph outline color. Outline(Color), + + /// Render only the glyph fill color. Solid(Color), + + /// Render both glyph fill and outline colors. SolidOutline(Color, Color), } @@ -294,6 +318,7 @@ impl From<[u8; 4]> for BitmapGlyphColor { pub use bmfont::CharPosition as BitmapGlyph; +/// Common accessors for glyph geometry and atlas placement. pub trait Glyph { fn page_height(&self) -> u32; fn page_width(&self) -> u32; @@ -406,3 +431,48 @@ impl Glyph for BitmapGlyph { self.screen_rect.y as _ } } + +#[cfg(test)] +mod test { + use super::BitmapGlyphColor; + + #[test] + fn glyph_color_from_f32_rgb_defaults_alpha_to_opaque() { + let color = BitmapGlyphColor::from([0.5, 0.25, 1.0]); + + match color { + BitmapGlyphColor::Solid([127, 63, 255, 255]) => {} + other => panic!("unexpected glyph color: {other:?}"), + } + } + + #[test] + fn glyph_color_from_f32_rgba_clamps_values() { + let color = BitmapGlyphColor::from([2.0, -1.0, 0.5, 0.25]); + + match color { + BitmapGlyphColor::Solid([255, 0, 127, 63]) => {} + other => panic!("unexpected glyph color: {other:?}"), + } + } + + #[test] + fn glyph_color_from_u8_rgb_defaults_alpha_to_opaque() { + let color = BitmapGlyphColor::from([1, 2, 3]); + + match color { + BitmapGlyphColor::Solid([1, 2, 3, 255]) => {} + other => panic!("unexpected glyph color: {other:?}"), + } + } + + #[test] + fn glyph_color_from_u8_rgba_preserves_channels() { + let color = BitmapGlyphColor::from([1, 2, 3, 4]); + + match color { + BitmapGlyphColor::Solid([1, 2, 3, 4]) => {} + other => panic!("unexpected glyph color: {other:?}"), + } + } +} diff --git a/contrib/screen-13-fx/src/image_loader.rs b/crates/vk-graph-fx/src/image_loader.rs similarity index 67% rename from contrib/screen-13-fx/src/image_loader.rs rename to crates/vk-graph-fx/src/image_loader.rs index 7cabbe5f..2c757578 100644 --- a/contrib/screen-13-fx/src/image_loader.rs +++ b/crates/vk-graph-fx/src/image_loader.rs @@ -1,6 +1,24 @@ use { - super::BitmapFont, anyhow::Context, bmfont::BMFont, inline_spirv::include_spirv, log::info, - screen_13::prelude::*, std::sync::Arc, + super::BitmapFont, + anyhow::{Context, bail}, + bmfont::BMFont, + log::info, + std::sync::Arc, + vk_graph::{ + Graph, + driver::{ + DriverError, + ash::vk, + buffer::{Buffer, BufferInfo}, + compute::{ComputePipeline, ComputePipelineInfo}, + device::Device, + image::{Image, ImageInfo}, + shader::Shader, + sync::AccessType, + }, + pool::{Pool as _, hash::HashPool}, + }, + vk_shader_macros::include_glsl, }; #[cfg(debug_assertions)] @@ -9,9 +27,16 @@ use log::warn; /// Describes the channels and pixel stride of an image format #[derive(Clone, Copy, Debug)] pub enum ImageFormat { + /// Single-channel 8-bit image data. R8, + + /// Two-channel 8-bit image data. R8G8, + + /// Three-channel 8-bit image data. R8G8B8, + + /// Four-channel 8-bit image data. R8G8B8A8, } @@ -26,34 +51,37 @@ impl ImageFormat { } } +/// Helper for decoding CPU bitmap data into `vk-graph` images. #[derive(Debug)] pub struct ImageLoader { pool: HashPool, - _decode_r_rg: Arc, - decode_rgb_rgba: Arc, - pub device: Arc, + _decode_r_rg: ComputePipeline, + decode_rgb_rgba: ComputePipeline, + + /// The device used to create temporary buffers, images, and decode pipelines. + pub device: Device, } impl ImageLoader { - pub fn new(device: &Arc) -> Result { + /// Creates a new image loader and its internal decode pipelines. + pub fn new(device: &Device) -> Result { Ok(Self { pool: HashPool::new(device), - _decode_r_rg: Arc::new(ComputePipeline::create( + _decode_r_rg: ComputePipeline::create( device, ComputePipelineInfo::default(), Shader::new_compute( - include_spirv!("res/shader/compute/decode_bitmap_r_rg.comp", comp).as_slice(), + include_glsl!("res/shader/compute/decode_bitmap_r_rg.comp").as_slice(), ), - )?), - decode_rgb_rgba: Arc::new(ComputePipeline::create( + )?, + decode_rgb_rgba: ComputePipeline::create( device, ComputePipelineInfo::default(), Shader::new_compute( - include_spirv!("res/shader/compute/decode_bitmap_rgb_rgba.comp", comp) - .as_slice(), + include_glsl!("res/shader/compute/decode_bitmap_rgb_rgba.comp").as_slice(), ), - )?), - device: Arc::clone(device), + )?, + device: device.clone(), }) } @@ -104,11 +132,12 @@ impl ImageLoader { )) } + /// Decodes bitmap pixels into an image and uploads it through a temporary graph submission. #[allow(clippy::too_many_arguments)] pub fn decode_bitmap( &mut self, - queue_family_index: usize, - queue_index: usize, + queue_family_index: u32, + queue_index: u32, pixels: &[u8], format: ImageFormat, width: u32, @@ -133,16 +162,15 @@ impl ImageLoader { warn!("unused data"); } - let mut render_graph = RenderGraph::new(); - let image = - render_graph.bind_node(self.create_image(format, width, height, is_srgb, false)?); + let mut graph = Graph::default(); + let image = graph.bind_resource(self.create_image(format, width, height, is_srgb, false)?); // Fill the image from the temporary buffer match format { ImageFormat::R8 => { // This format requires a conversion info!("Converting R to RG"); - todo!() + bail!("unsupported bitmap decode format: R8") } ImageFormat::R8G8B8 => { // This format requires a conversion @@ -158,7 +186,7 @@ impl ImageLoader { //trace!("pixel_buf_len={pixel_buf_len} pixel_buf_stride={pixel_buf_stride}"); // Lease a temporary buffer from the cache pool - let mut pixel_buf = self.pool.lease(BufferInfo::host_mem( + let mut pixel_buf = self.pool.resource(BufferInfo::host_mem( pixel_buf_len, vk::BufferUsageFlags::STORAGE_BUFFER, ))?; @@ -180,34 +208,34 @@ impl ImageLoader { } } - let pixel_buf = render_graph.bind_node(pixel_buf); + let pixel_buf = graph.bind_resource(pixel_buf); // We create a temporary storage image because SRGB support isn't wide enough to // have SRGB storage images directly let temp_image = - render_graph.bind_node(self.create_image(format, width, height, false, true)?); + graph.bind_resource(self.create_image(format, width, height, false, true)?); // Copy host-local data in the buffer to the temporary buffer on the GPU and then // use a compute shader to decode it before copying it over the output image let dispatch_x = (width + 3) >> 2; let dispatch_y = height; - render_graph - .begin_pass("Decode RGB image") + graph + .begin_cmd() + .debug_name("Decode RGB image") .bind_pipeline(&self.decode_rgb_rgba) - .read_descriptor(0, pixel_buf) - .write_descriptor(1, temp_image) - .record_compute(move |compute, _| { - compute - .push_constants(&(pixel_buf_stride >> 2).to_ne_bytes()) + .shader_resource_access(0, pixel_buf, AccessType::ComputeShaderReadOther) + .shader_resource_access(1, temp_image, AccessType::ComputeShaderWrite) + .record_cmd(move |cmd| { + cmd.push_constants(0, &(pixel_buf_stride >> 2).to_ne_bytes()) .dispatch(dispatch_x, dispatch_y, 1); }) - .submit_pass() + .end_cmd() .copy_image(temp_image, image); } ImageFormat::R8G8 | ImageFormat::R8G8B8A8 => { // Lease a temporary buffer from the pool - let mut pixel_buf = self.pool.lease(BufferInfo::host_mem( + let mut pixel_buf = self.pool.resource(BufferInfo::host_mem( pixels.len() as _, vk::BufferUsageFlags::TRANSFER_SRC, ))?; @@ -218,24 +246,25 @@ impl ImageLoader { pixel_buf.copy_from_slice(pixels); } - let pixel_buf = render_graph.bind_node(pixel_buf); - render_graph.copy_buffer_to_image(pixel_buf, image); + let pixel_buf = graph.bind_resource(pixel_buf); + graph.copy_buffer_to_image(pixel_buf, image); } } - let image = render_graph.unbind_node(image); + let image = graph.resource(image).clone(); - render_graph - .resolve() - .submit(&mut self.pool, queue_family_index, queue_index)?; + graph + .into_submission() + .queue_submit(&mut self.pool, queue_family_index, queue_index)?; Ok(image) } + /// Decodes a linear-color bitmap into an image. pub fn decode_linear( &mut self, - queue_family_index: usize, - queue_index: usize, + queue_family_index: u32, + queue_index: u32, pixels: &[u8], format: ImageFormat, width: u32, @@ -252,10 +281,11 @@ impl ImageLoader { ) } + /// Decodes an sRGB bitmap into an image. pub fn decode_srgb( &mut self, - queue_family_index: usize, - queue_index: usize, + queue_family_index: u32, + queue_index: u32, pixels: &[u8], format: ImageFormat, width: u32, @@ -272,10 +302,11 @@ impl ImageLoader { ) } + /// Loads a bitmap font by decoding each supplied page image and pairing it with the font data. pub fn load_bitmap_font<'a>( &mut self, - queue_family_index: usize, - queue_index: usize, + queue_family_index: u32, + queue_index: u32, font: BMFont, pages: impl IntoIterator, ) -> anyhow::Result { @@ -296,3 +327,16 @@ impl ImageLoader { BitmapFont::new(&self.device, font, pages) } } + +#[cfg(test)] +mod test { + use super::ImageFormat; + + #[test] + fn image_format_stride_matches_channel_count() { + assert_eq!(ImageFormat::R8.stride(), 1); + assert_eq!(ImageFormat::R8G8.stride(), 2); + assert_eq!(ImageFormat::R8G8B8.stride(), 3); + assert_eq!(ImageFormat::R8G8B8A8.stride(), 4); + } +} diff --git a/contrib/screen-13-fx/src/lib.rs b/crates/vk-graph-fx/src/lib.rs similarity index 60% rename from contrib/screen-13-fx/src/lib.rs rename to crates/vk-graph-fx/src/lib.rs index 75f815fc..e5debcde 100644 --- a/contrib/screen-13-fx/src/lib.rs +++ b/crates/vk-graph-fx/src/lib.rs @@ -1,9 +1,7 @@ -pub mod prelude { - pub use super::{ - BitmapFont, BitmapGlyphColor, ComputePresenter, GraphicPresenter, ImageFormat, ImageLoader, - Transition, TransitionPipeline, - }; -} +//! Reusable effects and rendering utilities built on top of `vk-graph`. + +#![deny(missing_docs)] +#![deny(rustdoc::broken_intra_doc_links)] mod bitmap_font; mod image_loader; diff --git a/crates/vk-graph-fx/src/presenter.rs b/crates/vk-graph-fx/src/presenter.rs new file mode 100644 index 00000000..7f89404a --- /dev/null +++ b/crates/vk-graph-fx/src/presenter.rs @@ -0,0 +1,151 @@ +use { + bytemuck::cast_slice, + glam::{Mat4, vec3}, + vk_graph::{ + Graph, + cmd::{LoadOp, StoreOp}, + driver::{ + DriverError, + compute::{ComputePipeline, ComputePipelineInfo}, + device::Device, + graphic::{GraphicPipeline, GraphicPipelineInfo}, + shader::Shader, + sync::AccessType, + }, + node::{AnyImageNode, SwapchainImageNode}, + }, + vk_shader_macros::include_glsl, +}; + +/// Compute-based presenter for copying one or two images into a swapchain image. +pub struct ComputePresenter([ComputePipeline; 2]); + +impl ComputePresenter { + /// Creates compute pipelines used to present images into a swapchain target. + pub fn new(device: &Device) -> Result { + let pipeline1 = ComputePipeline::create( + device, + ComputePipelineInfo::default(), + Shader::new_compute(include_glsl!("res/shader/compute/present1.comp").as_slice()), + )?; + let pipeline2 = ComputePipeline::create( + device, + ComputePipelineInfo::default(), + Shader::new_compute(include_glsl!("res/shader/compute/present2.comp").as_slice()), + )?; + + Ok(Self([pipeline1, pipeline2])) + } + + /// Presents a single source image to the given swapchain image using a compute shader. + pub fn present_image( + &self, + graph: &mut Graph, + image: impl Into, + swapchain: SwapchainImageNode, + ) { + let image = image.into(); + // let image_info = graph.node_info(image); + let swapchain_info = graph.resource(swapchain).info; + + // TODO: Notice non-sRGB images and run a different pipeline + + graph + .begin_cmd() + .debug_name("present (from compute)") + .bind_pipeline(&self.0[0]) + .shader_resource_access(0, image, AccessType::ComputeShaderReadOther) + .shader_resource_access(1, swapchain, AccessType::ComputeShaderWrite) + .record_cmd(move |cmd| { + cmd.dispatch(swapchain_info.width, swapchain_info.height, 1); + }); + } + + /// Presents two stacked source images to the given swapchain image using a compute shader. + pub fn present_images( + &self, + graph: &mut Graph, + top_image: impl Into, + bottom_image: impl Into, + swapchain: SwapchainImageNode, + ) { + let top_image = top_image.into(); + let bottom_image = bottom_image.into(); + // let top_image_info = graph.node_info(top_image); + // let bottom_image_info = graph.node_info(bottom_image); + let swapchain_info = graph.resource(swapchain).info; + + // TODO: Notice non-sRGB images and run a different pipeline + + graph + .begin_cmd() + .debug_name("present (from compute)") + .bind_pipeline(&self.0[1]) + .shader_resource_access((0, [0]), top_image, AccessType::ComputeShaderReadOther) + .shader_resource_access((0, [1]), bottom_image, AccessType::ComputeShaderReadOther) + .shader_resource_access(1, swapchain, AccessType::ComputeShaderWrite) + .record_cmd(move |cmd| { + cmd.dispatch(swapchain_info.width, swapchain_info.height, 1); + }); + } +} + +/// Graphic-pipeline presenter for drawing an image into a swapchain image. +pub struct GraphicPresenter { + pipeline: GraphicPipeline, +} + +impl GraphicPresenter { + /// Creates the graphic pipeline used for fullscreen image presentation. + pub fn new(device: &Device) -> Result { + let pipeline = GraphicPipeline::create( + device, + GraphicPipelineInfo::default(), + [ + Shader::new_vertex(include_glsl!("res/shader/graphic/present.vert").as_slice()), + Shader::new_fragment(include_glsl!("res/shader/graphic/present.frag").as_slice()), + ], + )?; + + Ok(Self { pipeline }) + } + + /// Draws the given image into the swapchain image using a fullscreen graphic pass. + pub fn present_image( + &self, + graph: &mut Graph, + image: impl Into, + swapchain: SwapchainImageNode, + ) { + let image = image.into(); + let image_info = graph.resource(image).info; + let swapchain_info = graph.resource(swapchain).info; + + let (image_width, image_height) = (image_info.width as f32, image_info.height as f32); + let (swapchain_width, swapchain_height) = + (swapchain_info.width as f32, swapchain_info.height as f32); + + let scale = (swapchain_width / image_width).max(swapchain_height / image_height); + let transform = Mat4::from_scale(vec3( + scale * image_width / swapchain_width, + scale * image_height / swapchain_height, + 1.0, + )); + + graph + .begin_cmd() + .debug_name("present (from graphic)") + .bind_pipeline(&self.pipeline) + .shader_resource_access( + 0, + image, + AccessType::FragmentShaderReadSampledImageOrUniformTexelBuffer, + ) + .color_attachment_image(0, swapchain, LoadOp::DontCare, StoreOp::Store) + .record_cmd(move |cmd| { + // Draw a quad with implicit vertices (no buffer) + cmd.push_constants(0, cast_slice(&transform.to_cols_array())) + .draw(6, 1, 0, 0); + }); + } +} diff --git a/contrib/screen-13-fx/src/transition.rs b/crates/vk-graph-fx/src/transition.rs similarity index 63% rename from contrib/screen-13-fx/src/transition.rs rename to crates/vk-graph-fx/src/transition.rs index 7f2b5cd5..09fe8c09 100644 --- a/contrib/screen-13-fx/src/transition.rs +++ b/crates/vk-graph-fx/src/transition.rs @@ -3,12 +3,25 @@ // use. use { - inline_spirv::include_spirv, log::trace, - screen_13::prelude::*, - std::{collections::HashMap, sync::Arc}, + std::collections::HashMap, + vk_graph::{ + Graph, + driver::{ + ash::vk, + compute::{ComputePipeline, ComputePipelineInfo}, + device::Device, + image::ImageInfo, + shader::Shader, + sync::AccessType, + }, + node::{AnyImageNode, ImageLeaseNode}, + pool::{Pool as _, hash::HashPool}, + }, + vk_shader_macros::include_glsl, }; +#[allow(missing_docs)] #[derive(Clone, Copy, Debug)] pub enum Transition { Angular { @@ -95,7 +108,8 @@ pub enum Transition { /// Number of total bars/columns bars: i32, - /// Multiplier for speed ratio. 0 = no variation when going down, higher = some elements go much faster + /// Multiplier for speed ratio. 0 = no variation when going down, higher = some elements go + /// much faster amplitude: f32, /// Further variations in speed. 0 = no noise, 1 = super noisy (ignore frequency) @@ -104,7 +118,8 @@ pub enum Transition { /// Speed variation horizontally. the bigger the value, the shorter the waves frequency: f32, - /// How much the bars seem to "run" from the middle of the screen first (sticking to the sides). 0 = no drip, 1 = curved drip + /// How much the bars seem to "run" from the middle of the screen first (sticking to the + /// sides). 0 = no drip, 1 = curved drip drip_scale: f32, }, Doorway { @@ -128,7 +143,8 @@ pub enum Transition { }, Fade, FadeGrayscale { - /// if 0.0, the image directly turn grayscale, if 0.9, the grayscale transition phase is very important + /// if 0.0, the image directly turn grayscale, if 0.9, the grayscale transition phase is + /// very important intensity: f32, }, FilmBurn { @@ -381,16 +397,18 @@ impl Transition { } } +/// Cache and lazily create transition compute pipelines on demand. pub struct TransitionPipeline { cache: HashPool, - device: Arc, - pipelines: HashMap>, + device: Device, // TODO REMOVE + pipelines: HashMap, } impl TransitionPipeline { - pub fn new(device: &Arc) -> Self { + /// Creates an empty transition pipeline cache for the given device. + pub fn new(device: &Device) -> Self { let cache = HashPool::new(device); - let device = Arc::clone(device); + let device = device.clone(); let pipelines = Default::default(); Self { @@ -400,9 +418,10 @@ impl TransitionPipeline { } } + /// Applies a transition between two images and returns a pooled destination image. pub fn apply( &mut self, - render_graph: &mut RenderGraph, + graph: &mut Graph, a_image: impl Into, b_image: impl Into, transition: Transition, @@ -411,8 +430,8 @@ impl TransitionPipeline { let a_image = a_image.into(); let b_image = b_image.into(); - let a_info = render_graph.node_info(a_image); - let b_info = render_graph.node_info(b_image); + let a_info = graph.resource(a_image).info; + let b_info = graph.resource(b_image).info; let dest_info = ImageInfo::image_2d( a_info.width.max(b_info.width), @@ -423,23 +442,21 @@ impl TransitionPipeline { | vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::TRANSFER_SRC, ); - let dest_image = render_graph.bind_node(self.cache.lease(dest_info).unwrap()); - - self.apply_to( - render_graph, - a_image, - b_image, - dest_image, - transition, - progress, + let dest_image = graph.bind_resource( + self.cache + .resource(dest_info) + .expect("missing transition image"), ); + self.apply_to(graph, a_image, b_image, dest_image, transition, progress); + dest_image } + /// Applies a transition between two images into an existing destination image. pub fn apply_to( &mut self, - render_graph: &mut RenderGraph, + graph: &mut Graph, a_image: impl Into, b_image: impl Into, dest_image: impl Into, @@ -451,7 +468,7 @@ impl TransitionPipeline { let dest_image = dest_image.into(); let progress = progress.clamp(0.0, 1.0); - let dest_info = render_graph.node_info(dest_image); + let dest_info = graph.resource(dest_image).info; // Lazy-initialize the compute pipeline for this transition let transition_ty = transition.ty(); @@ -463,325 +480,279 @@ impl TransitionPipeline { extend_push_constants(transition, &mut push_consts); // TODO: Handle displacement and luma in an if case, below - render_graph - .begin_pass(format!("transition {transition_ty:?}")) - .bind_pipeline(&pipeline) - .read_descriptor(0, a_image) - .read_descriptor(1, b_image) - .write_descriptor(2, dest_image) - .record_compute(move |compute, _| { - compute.push_constants(push_consts.as_slice()); - compute.dispatch(dest_info.width, dest_info.height, 1); + graph + .begin_cmd() + .debug_name(format!("transition {transition_ty:?}")) + .bind_pipeline(pipeline) + .shader_resource_access(0, a_image, AccessType::ComputeShaderReadOther) + .shader_resource_access(1, b_image, AccessType::ComputeShaderReadOther) + .shader_resource_access(2, dest_image, AccessType::ComputeShaderWrite) + .record_cmd(move |cmd| { + cmd.push_constants(0, &push_consts) + .dispatch(dest_info.width, dest_info.height, 1); }); } - fn pipeline(&mut self, transition_ty: TransitionType) -> Arc { - let pipeline = self.pipelines.entry(transition_ty).or_insert_with(|| { + fn pipeline(&mut self, transition_ty: TransitionType) -> &ComputePipeline { + self.pipelines.entry(transition_ty).or_insert_with(|| { trace!("creating {transition_ty:?}"); - Arc::new( - ComputePipeline::create( - &self.device, - ComputePipelineInfo::default(), - Shader::new_compute(match transition_ty { - TransitionType::Angular => { - include_spirv!("res/shader/transition/angular.comp", comp).as_slice() - } - TransitionType::Bounce => { - include_spirv!("res/shader/transition/bounce.comp", comp).as_slice() - } - TransitionType::BowTieHorizontal => { - include_spirv!("res/shader/transition/bow_tie_horizontal.comp", comp) - .as_slice() - } - TransitionType::BowTieVertical => { - include_spirv!("res/shader/transition/bow_tie_vertical.comp", comp) - .as_slice() - } - TransitionType::BowTieWithParameter => include_spirv!( - "res/shader/transition/bow_tie_with_parameter.comp", - comp - ) - .as_slice(), - TransitionType::Burn => { - include_spirv!("res/shader/transition/burn.comp", comp).as_slice() - } - TransitionType::ButterflyWaveScrawler => include_spirv!( - "res/shader/transition/butterfly_wave_scrawler.comp", - comp - ) - .as_slice(), - TransitionType::CannabisLeaf => { - include_spirv!("res/shader/transition/cannabis_leaf.comp", comp) - .as_slice() - } - TransitionType::Circle => { - include_spirv!("res/shader/transition/circle.comp", comp).as_slice() - } - TransitionType::CircleCrop => { - include_spirv!("res/shader/transition/circle_crop.comp", comp) - .as_slice() - } - TransitionType::CircleOpen => { - include_spirv!("res/shader/transition/circle_open.comp", comp) - .as_slice() - } - TransitionType::ColorDistance => { - include_spirv!("res/shader/transition/color_distance.comp", comp) - .as_slice() - } - TransitionType::ColorPhase => { - include_spirv!("res/shader/transition/color_phase.comp", comp) - .as_slice() - } - TransitionType::CoordFromIn => { - include_spirv!("res/shader/transition/coord_from_in.comp", comp) - .as_slice() - } - TransitionType::CrazyParametricFun => { - include_spirv!("res/shader/transition/crazy_parametric_fun.comp", comp) - .as_slice() - } - TransitionType::Crosshatch => { - include_spirv!("res/shader/transition/crosshatch.comp", comp).as_slice() - } - TransitionType::CrossWarp => { - include_spirv!("res/shader/transition/cross_warp.comp", comp).as_slice() - } - TransitionType::CrossZoom => { - include_spirv!("res/shader/transition/cross_zoom.comp", comp).as_slice() - } - TransitionType::Cube => { - include_spirv!("res/shader/transition/cube.comp", comp).as_slice() - } - TransitionType::Directional => { - include_spirv!("res/shader/transition/directional.comp", comp) - .as_slice() - } - TransitionType::DirectionalEasing => { - include_spirv!("res/shader/transition/directional_easing.comp", comp) - .as_slice() - } - TransitionType::DirectionalWarp => { - include_spirv!("res/shader/transition/directional_warp.comp", comp) - .as_slice() - } - TransitionType::DirectionalWipe => { - include_spirv!("res/shader/transition/directional_wipe.comp", comp) - .as_slice() - } - TransitionType::Displacement => { - include_spirv!("res/shader/transition/displacement.comp", comp) - .as_slice() - } - TransitionType::DoomScreen => { - include_spirv!("res/shader/transition/doom_screen.comp", comp) - .as_slice() - } - TransitionType::Doorway => { - include_spirv!("res/shader/transition/doorway.comp", comp).as_slice() - } - TransitionType::Dreamy => { - include_spirv!("res/shader/transition/dreamy.comp", comp).as_slice() - } - TransitionType::DreamyZoom => { - include_spirv!("res/shader/transition/dreamy_zoom.comp", comp) - .as_slice() - } - TransitionType::FadeColor => { - include_spirv!("res/shader/transition/fade_color.comp", comp).as_slice() - } - TransitionType::Fade => { - include_spirv!("res/shader/transition/fade.comp", comp).as_slice() - } - TransitionType::FadeGrayscale => { - include_spirv!("res/shader/transition/fade_grayscale.comp", comp) - .as_slice() - } - TransitionType::FilmBurn => { - include_spirv!("res/shader/transition/film_burn.comp", comp).as_slice() - } - TransitionType::Flyeye => { - include_spirv!("res/shader/transition/flyeye.comp", comp).as_slice() - } - TransitionType::GlitchDisplace => { - include_spirv!("res/shader/transition/glitch_displace.comp", comp) - .as_slice() - } - TransitionType::GlitchMemories => { - include_spirv!("res/shader/transition/glitch_memories.comp", comp) - .as_slice() - } - TransitionType::GridFlip => { - include_spirv!("res/shader/transition/grid_flip.comp", comp).as_slice() - } - TransitionType::Heart => { - include_spirv!("res/shader/transition/heart.comp", comp).as_slice() - } - TransitionType::Hexagonalize => { - include_spirv!("res/shader/transition/hexagonalize.comp", comp) - .as_slice() - } - TransitionType::InvertedPageCurl => { - include_spirv!("res/shader/transition/inverted_page_curl.comp", comp) - .as_slice() - } - TransitionType::Kaleidoscope => { - include_spirv!("res/shader/transition/kaleidoscope.comp", comp) - .as_slice() - } - TransitionType::LeftRight => { - include_spirv!("res/shader/transition/left_right.comp", comp).as_slice() - } - TransitionType::LinearBlur => { - include_spirv!("res/shader/transition/linear_blur.comp", comp) - .as_slice() - } - TransitionType::Luma => { - include_spirv!("res/shader/transition/luma.comp", comp).as_slice() - } - TransitionType::LuminanceMelt => { - include_spirv!("res/shader/transition/luminance_melt.comp", comp) - .as_slice() - } - TransitionType::Morph => { - include_spirv!("res/shader/transition/morph.comp", comp).as_slice() - } - TransitionType::Mosaic => { - include_spirv!("res/shader/transition/mosaic.comp", comp).as_slice() - } - TransitionType::Multiply => { - include_spirv!("res/shader/transition/multiply.comp", comp).as_slice() - } - TransitionType::Overexposure => { - include_spirv!("res/shader/transition/overexposure.comp", comp) - .as_slice() - } - TransitionType::Perlin => { - include_spirv!("res/shader/transition/perlin.comp", comp).as_slice() - } - TransitionType::Pinwheel => { - include_spirv!("res/shader/transition/pinwheel.comp", comp).as_slice() - } - TransitionType::Pixelize => { - include_spirv!("res/shader/transition/pixelize.comp", comp).as_slice() - } - TransitionType::PolarFunction => { - include_spirv!("res/shader/transition/polar_function.comp", comp) - .as_slice() - } - TransitionType::PolkaDotsCurtain => { - include_spirv!("res/shader/transition/polka_dots_curtain.comp", comp) - .as_slice() - } - TransitionType::PowerKaleido => { - include_spirv!("res/shader/transition/power_kaleido.comp", comp) - .as_slice() - } - TransitionType::Radial => { - include_spirv!("res/shader/transition/radial.comp", comp).as_slice() - } - TransitionType::RandomNoisex => { - include_spirv!("res/shader/transition/random_noisex.comp", comp) - .as_slice() - } - TransitionType::RandomSquares => { - include_spirv!("res/shader/transition/random_squares.comp", comp) - .as_slice() - } - TransitionType::Ripple => { - include_spirv!("res/shader/transition/ripple.comp", comp).as_slice() - } - TransitionType::Rotate => { - include_spirv!("res/shader/transition/rotate.comp", comp).as_slice() - } - TransitionType::RotateScale => { - include_spirv!("res/shader/transition/rotate_scale.comp", comp) - .as_slice() - } - TransitionType::ScaleIn => { - include_spirv!("res/shader/transition/scale_in.comp", comp).as_slice() - } - TransitionType::SimpleZoom => { - include_spirv!("res/shader/transition/simple_zoom.comp", comp) - .as_slice() - } - TransitionType::SquaresWire => { - include_spirv!("res/shader/transition/squares_wire.comp", comp) - .as_slice() - } - TransitionType::Squeeze => { - include_spirv!("res/shader/transition/squeeze.comp", comp).as_slice() - } - TransitionType::StereoViewer => { - include_spirv!("res/shader/transition/stereo_viewer.comp", comp) - .as_slice() - } - TransitionType::Swap => { - include_spirv!("res/shader/transition/swap.comp", comp).as_slice() - } - TransitionType::Swirl => { - include_spirv!("res/shader/transition/swirl.comp", comp).as_slice() - } - TransitionType::TangentMotionBlur => { - include_spirv!("res/shader/transition/tangent_motion_blur.comp", comp) - .as_slice() - } - TransitionType::TopBottom => { - include_spirv!("res/shader/transition/top_bottom.comp", comp).as_slice() - } - TransitionType::TvStatic => { - include_spirv!("res/shader/transition/tv_static.comp", comp).as_slice() - } - TransitionType::UndulatingBurnOut => { - include_spirv!("res/shader/transition/undulating_burn_out.comp", comp) - .as_slice() - } - TransitionType::WaterDrop => { - include_spirv!("res/shader/transition/water_drop.comp", comp).as_slice() - } - TransitionType::Wind => { - include_spirv!("res/shader/transition/wind.comp", comp).as_slice() - } - TransitionType::WindowBlinds => { - include_spirv!("res/shader/transition/window_blinds.comp", comp) - .as_slice() - } - TransitionType::WindowSlice => { - include_spirv!("res/shader/transition/window_slice.comp", comp) - .as_slice() - } - TransitionType::WipeDown => { - include_spirv!("res/shader/transition/wipe_down.comp", comp).as_slice() - } - TransitionType::WipeLeft => { - include_spirv!("res/shader/transition/wipe_left.comp", comp).as_slice() - } - TransitionType::WipeRight => { - include_spirv!("res/shader/transition/wipe_right.comp", comp).as_slice() - } - TransitionType::WipeUp => { - include_spirv!("res/shader/transition/wipe_up.comp", comp).as_slice() - } - TransitionType::ZoomInCircles => { - include_spirv!("res/shader/transition/zoom_in_circles.comp", comp) - .as_slice() - } - TransitionType::ZoomLeftWipe => { - include_spirv!("res/shader/transition/zoom_left_wipe.comp", comp) - .as_slice() - } - TransitionType::ZoomRightWipe => { - include_spirv!("res/shader/transition/zoom_right_wipe.comp", comp) - .as_slice() - } - }), - ) - .unwrap(), + ComputePipeline::create( + &self.device, + ComputePipelineInfo::default(), + Shader::new_compute(match transition_ty { + TransitionType::Angular => { + include_glsl!("res/shader/transition/angular.comp").as_slice() + } + TransitionType::Bounce => { + include_glsl!("res/shader/transition/bounce.comp").as_slice() + } + TransitionType::BowTieHorizontal => { + include_glsl!("res/shader/transition/bow_tie_horizontal.comp").as_slice() + } + TransitionType::BowTieVertical => { + include_glsl!("res/shader/transition/bow_tie_vertical.comp").as_slice() + } + TransitionType::BowTieWithParameter => { + include_glsl!("res/shader/transition/bow_tie_with_parameter.comp",) + .as_slice() + } + TransitionType::Burn => { + include_glsl!("res/shader/transition/burn.comp").as_slice() + } + TransitionType::ButterflyWaveScrawler => { + include_glsl!("res/shader/transition/butterfly_wave_scrawler.comp",) + .as_slice() + } + TransitionType::CannabisLeaf => { + include_glsl!("res/shader/transition/cannabis_leaf.comp").as_slice() + } + TransitionType::Circle => { + include_glsl!("res/shader/transition/circle.comp").as_slice() + } + TransitionType::CircleCrop => { + include_glsl!("res/shader/transition/circle_crop.comp").as_slice() + } + TransitionType::CircleOpen => { + include_glsl!("res/shader/transition/circle_open.comp").as_slice() + } + TransitionType::ColorDistance => { + include_glsl!("res/shader/transition/color_distance.comp").as_slice() + } + TransitionType::ColorPhase => { + include_glsl!("res/shader/transition/color_phase.comp").as_slice() + } + TransitionType::CoordFromIn => { + include_glsl!("res/shader/transition/coord_from_in.comp").as_slice() + } + TransitionType::CrazyParametricFun => { + include_glsl!("res/shader/transition/crazy_parametric_fun.comp").as_slice() + } + TransitionType::Crosshatch => { + include_glsl!("res/shader/transition/crosshatch.comp").as_slice() + } + TransitionType::CrossWarp => { + include_glsl!("res/shader/transition/cross_warp.comp").as_slice() + } + TransitionType::CrossZoom => { + include_glsl!("res/shader/transition/cross_zoom.comp").as_slice() + } + TransitionType::Cube => { + include_glsl!("res/shader/transition/cube.comp").as_slice() + } + TransitionType::Directional => { + include_glsl!("res/shader/transition/directional.comp").as_slice() + } + TransitionType::DirectionalEasing => { + include_glsl!("res/shader/transition/directional_easing.comp").as_slice() + } + TransitionType::DirectionalWarp => { + include_glsl!("res/shader/transition/directional_warp.comp").as_slice() + } + TransitionType::DirectionalWipe => { + include_glsl!("res/shader/transition/directional_wipe.comp").as_slice() + } + TransitionType::Displacement => { + include_glsl!("res/shader/transition/displacement.comp").as_slice() + } + TransitionType::DoomScreen => { + include_glsl!("res/shader/transition/doom_screen.comp").as_slice() + } + TransitionType::Doorway => { + include_glsl!("res/shader/transition/doorway.comp").as_slice() + } + TransitionType::Dreamy => { + include_glsl!("res/shader/transition/dreamy.comp").as_slice() + } + TransitionType::DreamyZoom => { + include_glsl!("res/shader/transition/dreamy_zoom.comp").as_slice() + } + TransitionType::FadeColor => { + include_glsl!("res/shader/transition/fade_color.comp").as_slice() + } + TransitionType::Fade => { + include_glsl!("res/shader/transition/fade.comp").as_slice() + } + TransitionType::FadeGrayscale => { + include_glsl!("res/shader/transition/fade_grayscale.comp").as_slice() + } + TransitionType::FilmBurn => { + include_glsl!("res/shader/transition/film_burn.comp").as_slice() + } + TransitionType::Flyeye => { + include_glsl!("res/shader/transition/flyeye.comp").as_slice() + } + TransitionType::GlitchDisplace => { + include_glsl!("res/shader/transition/glitch_displace.comp").as_slice() + } + TransitionType::GlitchMemories => { + include_glsl!("res/shader/transition/glitch_memories.comp").as_slice() + } + TransitionType::GridFlip => { + include_glsl!("res/shader/transition/grid_flip.comp").as_slice() + } + TransitionType::Heart => { + include_glsl!("res/shader/transition/heart.comp").as_slice() + } + TransitionType::Hexagonalize => { + include_glsl!("res/shader/transition/hexagonalize.comp").as_slice() + } + TransitionType::InvertedPageCurl => { + include_glsl!("res/shader/transition/inverted_page_curl.comp").as_slice() + } + TransitionType::Kaleidoscope => { + include_glsl!("res/shader/transition/kaleidoscope.comp").as_slice() + } + TransitionType::LeftRight => { + include_glsl!("res/shader/transition/left_right.comp").as_slice() + } + TransitionType::LinearBlur => { + include_glsl!("res/shader/transition/linear_blur.comp").as_slice() + } + TransitionType::Luma => { + include_glsl!("res/shader/transition/luma.comp").as_slice() + } + TransitionType::LuminanceMelt => { + include_glsl!("res/shader/transition/luminance_melt.comp").as_slice() + } + TransitionType::Morph => { + include_glsl!("res/shader/transition/morph.comp").as_slice() + } + TransitionType::Mosaic => { + include_glsl!("res/shader/transition/mosaic.comp").as_slice() + } + TransitionType::Multiply => { + include_glsl!("res/shader/transition/multiply.comp").as_slice() + } + TransitionType::Overexposure => { + include_glsl!("res/shader/transition/overexposure.comp").as_slice() + } + TransitionType::Perlin => { + include_glsl!("res/shader/transition/perlin.comp").as_slice() + } + TransitionType::Pinwheel => { + include_glsl!("res/shader/transition/pinwheel.comp").as_slice() + } + TransitionType::Pixelize => { + include_glsl!("res/shader/transition/pixelize.comp").as_slice() + } + TransitionType::PolarFunction => { + include_glsl!("res/shader/transition/polar_function.comp").as_slice() + } + TransitionType::PolkaDotsCurtain => { + include_glsl!("res/shader/transition/polka_dots_curtain.comp").as_slice() + } + TransitionType::PowerKaleido => { + include_glsl!("res/shader/transition/power_kaleido.comp").as_slice() + } + TransitionType::Radial => { + include_glsl!("res/shader/transition/radial.comp").as_slice() + } + TransitionType::RandomNoisex => { + include_glsl!("res/shader/transition/random_noisex.comp").as_slice() + } + TransitionType::RandomSquares => { + include_glsl!("res/shader/transition/random_squares.comp").as_slice() + } + TransitionType::Ripple => { + include_glsl!("res/shader/transition/ripple.comp").as_slice() + } + TransitionType::Rotate => { + include_glsl!("res/shader/transition/rotate.comp").as_slice() + } + TransitionType::RotateScale => { + include_glsl!("res/shader/transition/rotate_scale.comp").as_slice() + } + TransitionType::ScaleIn => { + include_glsl!("res/shader/transition/scale_in.comp").as_slice() + } + TransitionType::SimpleZoom => { + include_glsl!("res/shader/transition/simple_zoom.comp").as_slice() + } + TransitionType::SquaresWire => { + include_glsl!("res/shader/transition/squares_wire.comp").as_slice() + } + TransitionType::Squeeze => { + include_glsl!("res/shader/transition/squeeze.comp").as_slice() + } + TransitionType::StereoViewer => { + include_glsl!("res/shader/transition/stereo_viewer.comp").as_slice() + } + TransitionType::Swap => { + include_glsl!("res/shader/transition/swap.comp").as_slice() + } + TransitionType::Swirl => { + include_glsl!("res/shader/transition/swirl.comp").as_slice() + } + TransitionType::TangentMotionBlur => { + include_glsl!("res/shader/transition/tangent_motion_blur.comp").as_slice() + } + TransitionType::TopBottom => { + include_glsl!("res/shader/transition/top_bottom.comp").as_slice() + } + TransitionType::TvStatic => { + include_glsl!("res/shader/transition/tv_static.comp").as_slice() + } + TransitionType::UndulatingBurnOut => { + include_glsl!("res/shader/transition/undulating_burn_out.comp").as_slice() + } + TransitionType::WaterDrop => { + include_glsl!("res/shader/transition/water_drop.comp").as_slice() + } + TransitionType::Wind => { + include_glsl!("res/shader/transition/wind.comp").as_slice() + } + TransitionType::WindowBlinds => { + include_glsl!("res/shader/transition/window_blinds.comp").as_slice() + } + TransitionType::WindowSlice => { + include_glsl!("res/shader/transition/window_slice.comp").as_slice() + } + TransitionType::WipeDown => { + include_glsl!("res/shader/transition/wipe_down.comp").as_slice() + } + TransitionType::WipeLeft => { + include_glsl!("res/shader/transition/wipe_left.comp").as_slice() + } + TransitionType::WipeRight => { + include_glsl!("res/shader/transition/wipe_right.comp").as_slice() + } + TransitionType::WipeUp => { + include_glsl!("res/shader/transition/wipe_up.comp").as_slice() + } + TransitionType::ZoomInCircles => { + include_glsl!("res/shader/transition/zoom_in_circles.comp").as_slice() + } + TransitionType::ZoomLeftWipe => { + include_glsl!("res/shader/transition/zoom_left_wipe.comp").as_slice() + } + TransitionType::ZoomRightWipe => { + include_glsl!("res/shader/transition/zoom_right_wipe.comp").as_slice() + } + }), ) - }); - - Arc::clone(pipeline) + .expect("invalid transition pipeline") + }) } } diff --git a/crates/vk-graph-hot/.github/img/plasma.png b/crates/vk-graph-hot/.github/img/plasma.png new file mode 100644 index 00000000..16ab8425 Binary files /dev/null and b/crates/vk-graph-hot/.github/img/plasma.png differ diff --git a/crates/vk-graph-hot/CHANGELOG.md b/crates/vk-graph-hot/CHANGELOG.md new file mode 100644 index 00000000..97d366ed --- /dev/null +++ b/crates/vk-graph-hot/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this crate will be documented in this file. + +This crate is versioned independently from `vk-graph`. + +## [Unreleased] + +## [0.1.0] - 2026-05-30 + +- Initial release. +- Supports `vk-graph` `0.14`. diff --git a/crates/vk-graph-hot/Cargo.toml b/crates/vk-graph-hot/Cargo.toml new file mode 100644 index 00000000..b54fe295 --- /dev/null +++ b/crates/vk-graph-hot/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "vk-graph-hot" +version = "0.1.0" +authors = ["John Wells "] +homepage = "https://github.com/attackgoat/vk-graph/crates/vk-graph-hot" +documentation = "https://docs.rs/vk-graph-hot" +keywords = ["gamedev", "vulkan"] +categories = ["game-development", "multimedia::images", "rendering::engine"] +description = "Hot-reloading shader pipelines for vk-graph" +edition.workspace = true +license.workspace = true +readme = "README.md" +repository.workspace = true + +[dependencies] +anyhow = "1.0" +derive_builder = "0.20" +log.workspace = true +notify = "8.2" +paste = "1.0" +vk-graph.workspace = true +vk-graph-window.workspace = true +shader-prepper = "0.3.0-pre.3" +shaderc = "0.10" + +[dev-dependencies] +clap = { version = "4.6", features = ["derive"] } +pretty_env_logger = "0.5" +tempfile = "3.20" +vk-graph.workspace = true diff --git a/contrib/screen-13-hot/README.md b/crates/vk-graph-hot/README.md similarity index 52% rename from contrib/screen-13-hot/README.md rename to crates/vk-graph-hot/README.md index a42cd39b..4049a268 100644 --- a/contrib/screen-13-hot/README.md +++ b/crates/vk-graph-hot/README.md @@ -1,24 +1,22 @@ -# Screen 13 Hot +# vk-graph Hot -Hot-reloading shader pipelines for _Screen 13_. Supports compute, graphic, and ray-trace shader +Hot-reloading shader pipelines for _vk-graph_. Supports compute, graphic, and ray-trace shader pipelines. +This crate is versioned independently from `vk-graph`. Based on shaderc. Feel free to submit PRs for other compilers. ## Quick Start -See the [example code](examples/README.md), +See the [example code](examples/README.md) for complete GLSL and HLSL hot-reload examples. ## Basic usage See the [GLSL](examples/glsl.rs) and [HLSL](examples/hlsl.rs) examples for usage - the hot pipelines -are drop-in replacements for the regular shader pipelines offered by _Screen 13_. +are drop-in replacements for the regular shader pipelines offered by _vk-graph_. -After creating a pipeline two functions are available, `hot` or `cold`. The result of each may be -bound to a render graph for any sort of regular use. - -- `hot()`: Returns the pipeline instance which includes any changes found on disk. -- `cold()`: Returns the most recent successful compilation without watching for changes. +Use `HotShader` with a file path and it will automatically update the created pipeline whenever the +files included in the source code change. ## Advanced usage @@ -26,6 +24,6 @@ There are a few options available when creating a `HotShader` instance, which is regular `Shader` instances. These options allow you to set compilation settings such as optimization level and warnings-as-errors, among other things. -## More infomation +## More information -Run `cargo doc --open` to view detailed API documentation and find available compilation options. \ No newline at end of file +Run `cargo doc --open` to view detailed API documentation and find available compilation options. diff --git a/contrib/screen-13-hot/examples/README.md b/crates/vk-graph-hot/examples/README.md similarity index 70% rename from contrib/screen-13-hot/examples/README.md rename to crates/vk-graph-hot/examples/README.md index 92dd719a..b87428e0 100644 --- a/contrib/screen-13-hot/examples/README.md +++ b/crates/vk-graph-hot/examples/README.md @@ -1,4 +1,4 @@ -# _Screen 13 Hot_ Example Code +# _vk-graph Hot_ Example Code ## Getting Started @@ -11,5 +11,5 @@ See the [README](../README.md) for more information. Example | Instructions | Preview --- | --- | :---: -[glsl.rs](glsl.rs) |
cargo run --example glsl
| Preview -[hlsl.rs](hlsl.rs) |
cargo run --example hlsl
| Preview \ No newline at end of file +[glsl.rs](glsl.rs) |
cargo run --example glsl
| Preview +[hlsl.rs](hlsl.rs) |
cargo run --example hlsl
| Preview diff --git a/contrib/screen-13-hot/examples/glsl.rs b/crates/vk-graph-hot/examples/glsl.rs similarity index 52% rename from contrib/screen-13-hot/examples/glsl.rs rename to crates/vk-graph-hot/examples/glsl.rs index b7f3d358..8e67fd49 100644 --- a/contrib/screen-13-hot/examples/glsl.rs +++ b/crates/vk-graph-hot/examples/glsl.rs @@ -1,13 +1,13 @@ use { clap::Parser, - screen_13::prelude::*, - screen_13_hot::prelude::*, - screen_13_window::{Window, WindowError}, std::path::PathBuf, + vk_graph::driver::{compute::ComputePipelineInfo, sync::AccessType}, + vk_graph_hot::{HotComputePipeline, HotShader}, + vk_graph_window::{Window, WindowError}, }; -/// This program draws a noise signal to the swapchain - make changes to fill_image.comp or the -/// noise.glsl file it includes to see those changes update while the program is still running. +/// This program draws a plasma animation to the swapchain - make changes to fill_image.comp or the +/// plasma.glsl file it includes to see those changes update while the program is still running. /// /// Run with RUST_LOG=info to get notification of shader compilations. fn main() -> Result<(), WindowError> { @@ -19,22 +19,24 @@ fn main() -> Result<(), WindowError> { // Create a compute pipeline - the same as normal except for "Hot" prefixes and we provide the // shader source code path instead of the shader source code bytes let cargo_manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let mut pipeline = HotComputePipeline::create( + let fill_image_path = cargo_manifest_dir.join("examples/res/fill_image.comp"); + let pipeline = HotComputePipeline::create( &window.device, ComputePipelineInfo::default(), - HotShader::new_compute(cargo_manifest_dir.join("examples/res/fill_image.comp")), + HotShader::from_path(fill_image_path), )?; let mut frame_index: u32 = 0; window.run(|frame| { frame - .render_graph - .begin_pass("make some noise") - .bind_pipeline(pipeline.hot()) - .write_descriptor(0, frame.swapchain_image) - .record_compute(move |compute, _| { - compute.push_constants(&frame_index.to_ne_bytes()).dispatch( + .graph + .begin_cmd() + .debug_name("oh that looks so cool") + .bind_pipeline(&pipeline) + .shader_resource_access(0, frame.swapchain_image, AccessType::ComputeShaderWrite) + .record_cmd(move |cmd| { + cmd.push_constants(0, &frame_index.to_ne_bytes()).dispatch( frame.width, frame.height, 1, diff --git a/contrib/screen-13-hot/examples/hlsl.rs b/crates/vk-graph-hot/examples/hlsl.rs similarity index 52% rename from contrib/screen-13-hot/examples/hlsl.rs rename to crates/vk-graph-hot/examples/hlsl.rs index c7192112..797f3e56 100644 --- a/contrib/screen-13-hot/examples/hlsl.rs +++ b/crates/vk-graph-hot/examples/hlsl.rs @@ -1,13 +1,16 @@ use { clap::Parser, - screen_13::prelude::*, - screen_13_hot::prelude::*, - screen_13_window::{Window, WindowError}, std::path::PathBuf, + vk_graph::{ + cmd::{LoadOp, StoreOp}, + driver::graphic::GraphicPipelineInfo, + }, + vk_graph_hot::{HotGraphicPipeline, HotShader}, + vk_graph_window::{Window, WindowError}, }; -/// This program draws a noise signal to the swapchain - make changes to fill_image.hlsl or the -/// noise.hlsl file it includes to see those changes update while the program is still running. +/// This program draws a plasma animation to the swapchain - make changes to fill_image.hlsl or the +/// plasma.hlsl file it includes to see those changes update while the program is still running. /// /// Run with RUST_LOG=info to get notification of shader compilations. fn main() -> Result<(), WindowError> { @@ -20,12 +23,12 @@ fn main() -> Result<(), WindowError> { // shader source code path instead of the shader source code bytes let cargo_manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let fill_image_path = cargo_manifest_dir.join("examples/res/fill_image.hlsl"); - let mut pipeline = HotGraphicPipeline::create( + let pipeline = HotGraphicPipeline::create( &window.device, GraphicPipelineInfo::default(), [ - HotShader::new_vertex(&fill_image_path).entry_name("vertex_main".to_string()), - HotShader::new_fragment(&fill_image_path).entry_name("fragment_main".to_string()), + HotShader::new_vertex(&fill_image_path).entry_name("vertex_main"), + HotShader::new_fragment(&fill_image_path).entry_name("fragment_main"), ], )?; @@ -33,16 +36,20 @@ fn main() -> Result<(), WindowError> { window.run(|frame| { frame - .render_graph - .begin_pass("make some noise") - .bind_pipeline(pipeline.hot()) - .clear_color(0, frame.swapchain_image) - .store_color(0, frame.swapchain_image) - .record_subpass(move |subpass, _| { - subpass - .push_constants_offset(0, &frame_index.to_ne_bytes()) - .push_constants_offset(4, &frame.width.to_ne_bytes()) - .push_constants_offset(8, &frame.height.to_ne_bytes()) + .graph + .begin_cmd() + .debug_name("neato colors") + .bind_pipeline(&pipeline) + .color_attachment_image( + 0, + frame.swapchain_image, + LoadOp::CLEAR_BLACK_ALPHA_ZERO, + StoreOp::Store, + ) + .record_cmd(move |cmd| { + cmd.push_constants(0, &frame_index.to_ne_bytes()) + .push_constants(4, &frame.width.to_ne_bytes()) + .push_constants(8, &frame.height.to_ne_bytes()) .draw(3, 1, 0, 0); }); diff --git a/contrib/screen-13-hot/examples/res/fill_image.comp b/crates/vk-graph-hot/examples/res/fill_image.comp similarity index 52% rename from contrib/screen-13-hot/examples/res/fill_image.comp rename to crates/vk-graph-hot/examples/res/fill_image.comp index b4b02949..506a3078 100644 --- a/contrib/screen-13-hot/examples/res/fill_image.comp +++ b/crates/vk-graph-hot/examples/res/fill_image.comp @@ -1,6 +1,6 @@ #version 460 core -#include "noise.glsl" +#include "plasma.glsl" layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; @@ -11,8 +11,17 @@ layout(push_constant) uniform PushConstants { layout(set = 0, binding = 0, rgba32f) restrict writeonly uniform image2D image; void main() { - uvec3 data = uvec3(gl_GlobalInvocationID.xy, push_const.frame_index); - vec4 color = vec4(hash(data), 1.0); + vec2 extent = vec2(1.0, 1024 / 768); + vec2 offset = vec2(gl_GlobalInvocationID.xy) / extent; + vec4 color = plasma( + offset, + extent, + float(push_const.frame_index) / 1440.0, + 0.0015, + 1.0, + 0.02, + vec3(0.2, 0.8, 0.5) + ); imageStore(image, ivec2(gl_GlobalInvocationID.xy), color); -} \ No newline at end of file +} diff --git a/contrib/screen-13-hot/examples/res/fill_image.hlsl b/crates/vk-graph-hot/examples/res/fill_image.hlsl similarity index 56% rename from contrib/screen-13-hot/examples/res/fill_image.hlsl rename to crates/vk-graph-hot/examples/res/fill_image.hlsl index 299bf34e..f03fd40e 100644 --- a/contrib/screen-13-hot/examples/res/fill_image.hlsl +++ b/crates/vk-graph-hot/examples/res/fill_image.hlsl @@ -1,4 +1,4 @@ -#include "noise.hlsl" +#include "plasma.hlsl" struct PushConst { uint frame_index; @@ -7,7 +7,7 @@ struct PushConst { }; [[vk::push_constant]] -cbuffer { +cbuffer PushConst { PushConst push_const; }; @@ -26,10 +26,17 @@ Vertex vertex_main(uint vertex_id: SV_VERTEXID) { } float4 fragment_main(Vertex vertex): SV_TARGET { - uint3 coord; - coord.x = uint(vertex.tex_coord.x * float(push_const.frame_width)); - coord.y = uint(vertex.tex_coord.y * float(push_const.frame_height)); - coord.z = push_const.frame_index; - - return float4(hash(coord), 1.0); + float2 extent = float2(1.0, float(push_const.frame_width) / float(push_const.frame_height)); + float2 offset = vertex.tex_coord * float(push_const.frame_width); + float4 color = plasma( + offset, + extent, + float(push_const.frame_index) / 12440.0, + 0.215, + 1.0, + 0.143, + float3(0.6, 0.3, 0.2) + ); + + return color; } diff --git a/crates/vk-graph-hot/examples/res/plasma.glsl b/crates/vk-graph-hot/examples/res/plasma.glsl new file mode 100644 index 00000000..f69e0598 --- /dev/null +++ b/crates/vk-graph-hot/examples/res/plasma.glsl @@ -0,0 +1,22 @@ +const float PI = 3.14159265; + +vec4 plasma( + vec2 offset, + vec2 extent, + float time, + float radius, + float alpha, + float size, + vec3 shift +) { + float color1 = (sin(dot(offset, vec2(sin(time * 3.0), cos(time * 3.0))) * 0.02 + time * 3.0) + 1.0) / 2.0; + vec2 center = extent / 2.0 + vec2(extent.x / 2.0 * sin(-time * 3.0) * radius, extent.y / 2.0 * cos(-time * 3.0) * radius); + float color2 = (cos(length(offset - center) * size) + 1.0) / 2.0; + float color = (color1 + color2) / 2.0; + + float red = (sin(PI * color / shift.r + time * 3.0) + 1.0) / 2.0; + float green = (sin(PI * color / shift.g + time * 3.0) + 1.0) / 2.0; + float blue = (sin(PI * color / shift.b + time * 3.0) + 1.0) / 2.0; + + return vec4(red, green, blue, alpha); +} diff --git a/crates/vk-graph-hot/examples/res/plasma.hlsl b/crates/vk-graph-hot/examples/res/plasma.hlsl new file mode 100644 index 00000000..c457f6cf --- /dev/null +++ b/crates/vk-graph-hot/examples/res/plasma.hlsl @@ -0,0 +1,22 @@ +static const float PI = 3.14159265; + +float4 plasma( + float2 offset, + float2 extent, + float time, + float radius, + float alpha, + float size, + float3 shift +) { + float color1 = (sin(dot(offset, float2(sin(time * 3.0), cos(time * 3.0))) * 0.02 + time * 3.0) + 1.0) / 2.0; + float2 center = extent / 2.0 + float2(extent.x / 2.0 * sin(-time * 3.0) * radius, extent.y / 2.0 * cos(-time * 3.0) * radius); + float color2 = (cos(length(offset - center) * size) + 1.0) / 2.0; + float color = (color1 + color2) / 2.0; + + float red = (sin(PI * color / shift.r + time * 3.0) + 1.0) / 2.0; + float green = (sin(PI * color / shift.g + time * 3.0) + 1.0) / 2.0; + float blue = (sin(PI * color / shift.b + time * 3.0) + 1.0) / 2.0; + + return float4(red, green, blue, alpha); +} diff --git a/crates/vk-graph-hot/src/compute.rs b/crates/vk-graph-hot/src/compute.rs new file mode 100644 index 00000000..cfa1ec9b --- /dev/null +++ b/crates/vk-graph-hot/src/compute.rs @@ -0,0 +1,78 @@ +//! Hot-reload compute pipeline support. + +use { + super::{ + HotPipeline, compile_shader_and_watch, create_watcher, pipeline, pipeline_handle, + shader::HotShader, + }, + log::info, + std::sync::{ + Arc, RwLock, + atomic::{AtomicBool, Ordering}, + }, + vk_graph::{ + cmd::{Command, Pipeline}, + driver::{ + DriverError, + compute::{ComputePipeline, ComputePipelineInfo}, + device::Device, + }, + }, +}; + +/// A compute pipeline wrapper that recompiles its shader when source files change. +#[derive(Debug)] +pub struct HotComputePipeline { + cache: RwLock>, + device: Device, + has_changes: Arc, + shader: HotShader, +} + +impl HotComputePipeline { + /// Creates a hot-reload compute pipeline from a single shader file. + pub fn create( + device: &Device, + info: impl Into, + shader: impl Into, + ) -> Result { + let shader = shader.into(); + + let has_changes = Default::default(); + let mut watcher = create_watcher(&has_changes); + + let compiled_shader = compile_shader_and_watch(&shader, &mut watcher)?; + + let pipeline = ComputePipeline::create(device, info, compiled_shader)?; + + Ok(Self { + cache: RwLock::new(HotPipeline { pipeline, watcher }), + device: device.clone(), + has_changes, + shader, + }) + } + + fn compile_shader_and_bind_cmd<'a>( + &self, + cmd: Command<'a>, + ) -> >::Command { + if self.has_changes.swap(false, Ordering::Relaxed) { + info!("Shader change detected"); + + let mut cache = self.cache_mut(); + + if let Ok(shader) = compile_shader_and_watch(&self.shader, &mut cache.watcher) + && let Ok(pipeline) = + ComputePipeline::create(&self.device, cache.pipeline.info(), shader) + { + cache.pipeline = pipeline; + } + } + + self.cache().pipeline.clone().bind_cmd(cmd) + } +} + +pipeline!(Compute); +pipeline_handle!(Compute); diff --git a/crates/vk-graph-hot/src/graphic.rs b/crates/vk-graph-hot/src/graphic.rs new file mode 100644 index 00000000..a5b6e743 --- /dev/null +++ b/crates/vk-graph-hot/src/graphic.rs @@ -0,0 +1,81 @@ +//! Hot-reload graphic pipeline support. + +use { + super::{HotPipeline, compile_shaders_and_watch, create_watcher, pipeline, shader::HotShader}, + log::info, + std::sync::{ + Arc, RwLock, + atomic::{AtomicBool, Ordering}, + }, + vk_graph::{ + cmd::{Command, Pipeline}, + driver::{ + DriverError, + device::Device, + graphic::{GraphicPipeline, GraphicPipelineInfo}, + }, + }, +}; + +/// A graphic pipeline wrapper that recompiles its shaders when source files change. +#[derive(Debug)] +pub struct HotGraphicPipeline { + cache: RwLock>, + device: Device, + has_changes: Arc, + shaders: Box<[HotShader]>, +} + +impl HotGraphicPipeline { + /// Creates a hot-reload graphic pipeline from one or more shader files. + pub fn create( + device: &Device, + info: impl Into, + shaders: impl IntoIterator, + ) -> Result + where + S: Into, + { + let shaders = shaders.into_iter().map(Into::into).collect::>(); + + let has_changes = Default::default(); + let mut watcher = create_watcher(&has_changes); + + let pipeline = { + GraphicPipeline::create( + device, + info, + compile_shaders_and_watch(&shaders, &mut watcher)?, + ) + }?; + + Ok(Self { + cache: RwLock::new(HotPipeline { pipeline, watcher }), + device: device.clone(), + has_changes, + shaders, + }) + } + + fn compile_shader_and_bind_cmd<'a>( + &self, + cmd: Command<'a>, + ) -> >::Command { + if self.has_changes.swap(false, Ordering::Relaxed) { + info!("Shader change detected"); + + let mut cache = self.cache_mut(); + + if let Ok(shaders) = compile_shaders_and_watch(&self.shaders, &mut cache.watcher) + && let Ok(pipeline) = + GraphicPipeline::create(&self.device, cache.pipeline.info(), shaders) + { + cache.pipeline = pipeline; + } + } + + self.cache().pipeline.clone().bind_cmd(cmd) + } +} + +pipeline!(Graphic); diff --git a/crates/vk-graph-hot/src/lib.rs b/crates/vk-graph-hot/src/lib.rs new file mode 100644 index 00000000..f56e918c --- /dev/null +++ b/crates/vk-graph-hot/src/lib.rs @@ -0,0 +1,368 @@ +//! Shader hot-reload support for `vk-graph` pipelines. + +#![deny(missing_docs)] +#![deny(rustdoc::broken_intra_doc_links)] + +mod compute; +mod graphic; +mod ray_trace; +mod shader; + +pub use self::{ + compute::HotComputePipeline, + graphic::HotGraphicPipeline, + ray_trace::HotRayTracePipeline, + shader::{HotShader, HotShaderBuilder}, +}; + +use { + log::{error, info}, + notify::{Event, EventKind, RecommendedWatcher, recommended_watcher}, + shader_prepper::{ + BoxedIncludeProviderError, IncludeProvider, ResolvedInclude, ResolvedIncludePath, + process_file, + }, + shaderc::{CompileOptions, Compiler, ShaderKind, SourceLanguage}, + std::{ + collections::HashSet, + fs::read_to_string, + io::{Error, ErrorKind}, + path::{Path, PathBuf}, + sync::{ + Arc, OnceLock, + atomic::{AtomicBool, Ordering}, + }, + }, + vk_graph::driver::{ + DriverError, + shader::{Shader, ShaderBuilder}, + }, +}; + +struct CompiledShader { + files_included: HashSet, + spirv_code: Vec, +} + +fn compile_shader( + path: impl AsRef, + entry_name: &str, + shader_kind: Option, + additional_opts: Option<&CompileOptions<'_>>, +) -> anyhow::Result { + info!("Compiling: {}", path.as_ref().display()); + + let path = path.as_ref().to_path_buf(); + let shader_kind = shader_kind.unwrap_or_else(|| guess_shader_kind(&path)); + + #[derive(Default)] + struct FileIncludeProvider(HashSet); + + impl IncludeProvider for FileIncludeProvider { + type IncludeContext = PathBuf; + + fn get_include( + &mut self, + path: &ResolvedIncludePath, + ) -> Result { + self.0.insert(PathBuf::from(&path.0)); + + Ok(read_to_string(&path.0)?) + } + + fn resolve_path( + &self, + path: &str, + context: &Self::IncludeContext, + ) -> Result, BoxedIncludeProviderError> { + let path = context.join(path); + + Ok(ResolvedInclude { + resolved_path: ResolvedIncludePath(path.to_str().unwrap_or_default().to_string()), + context: path + .parent() + .map(|path| path.to_path_buf()) + .unwrap_or_default(), + }) + } + } + + let mut file_include_provider = FileIncludeProvider::default(); + let source_code = process_file( + path.to_string_lossy().as_ref(), + &mut file_include_provider, + PathBuf::new(), + ) + .map_err(|err| { + error!("unable to process shader file: {err}"); + + Error::new(ErrorKind::InvalidData, err) + })? + .iter() + .map(|chunk| chunk.source.as_str()) + .collect::(); + let files_included = file_include_provider.0; + + static COMPILER: OnceLock = OnceLock::new(); + let spirv_code = COMPILER + .get_or_init(|| Compiler::new().expect("invalid shaderc compiler")) + .compile_into_spirv( + &source_code, + shader_kind, + &path.to_string_lossy(), + entry_name, + additional_opts, + ) + .inspect_err(|_| { + eprintln!("Shader: {}", path.display()); + + for (line_index, line) in source_code.split('\n').enumerate() { + let line_number = line_index + 1; + eprintln!("{line_number}: {line}"); + } + })? + .as_binary_u8() + .to_vec(); + + Ok(CompiledShader { + files_included, + spirv_code, + }) +} + +fn compile_shader_and_watch( + shader: &HotShader, + watcher: &mut RecommendedWatcher, +) -> Result { + let mut base_shader = Shader::new(shader.stage, shader.compile_and_watch(watcher)?.as_slice()); + + base_shader = base_shader.entry_name(shader.entry_name.clone()); + + if let Some(specialization) = &shader.specialization { + base_shader = base_shader.specialization(specialization.clone()); + } + + Ok(base_shader) +} + +fn compile_shaders_and_watch( + shaders: &[HotShader], + watcher: &mut RecommendedWatcher, +) -> Result, DriverError> { + shaders + .iter() + .map(|shader| compile_shader_and_watch(shader, watcher)) + .collect() +} + +fn create_watcher(has_changes: &Arc) -> RecommendedWatcher { + let has_changes = Arc::clone(has_changes); + + recommended_watcher(move |event: notify::Result| { + let event = event.unwrap_or_else(|_| Event::new(EventKind::Any)); + if matches!( + event.kind, + EventKind::Any | EventKind::Modify(_) | EventKind::Other + ) { + has_changes.store(true, Ordering::Relaxed); + } + }) + .expect("invalid shader watcher") +} + +fn guess_shader_kind(path: impl AsRef) -> ShaderKind { + match path + .as_ref() + .extension() + .map(|ext| ext.to_string_lossy().to_string()) + .unwrap_or_default() + .as_str() + { + "comp" => ShaderKind::Compute, + "task" => ShaderKind::Task, + "mesh" => ShaderKind::Mesh, + "vert" => ShaderKind::Vertex, + "geom" => ShaderKind::Geometry, + "tesc" => ShaderKind::TessControl, + "tese" => ShaderKind::TessEvaluation, + "frag" => ShaderKind::Fragment, + "rgen" => ShaderKind::RayGeneration, + "rahit" => ShaderKind::AnyHit, + "rchit" => ShaderKind::ClosestHit, + "rint" => ShaderKind::Intersection, + "rcall" => ShaderKind::Callable, + "rmiss" => ShaderKind::Miss, + _ => ShaderKind::InferFromSource, + } +} + +fn guess_shader_source_language(path: impl AsRef) -> Option { + match path + .as_ref() + .extension() + .map(|ext| ext.to_string_lossy().to_string()) + .unwrap_or_default() + .as_str() + { + "comp" | "task" | "mesh" | "vert" | "geom" | "tesc" | "tese" | "frag" | "rgen" + | "rahit" | "rchit" | "rint" | "rcall" | "rmiss" | "glsl" => Some(SourceLanguage::GLSL), + "hlsl" => Some(SourceLanguage::HLSL), + _ => None, + } +} + +#[cfg(test)] +mod test { + use super::{guess_shader_kind, guess_shader_source_language}; + use shaderc::{ShaderKind, SourceLanguage}; + + #[test] + fn guess_shader_kind_from_known_extensions() { + assert_eq!(guess_shader_kind("shader.comp"), ShaderKind::Compute); + assert_eq!(guess_shader_kind("shader.vert"), ShaderKind::Vertex); + assert_eq!(guess_shader_kind("shader.frag"), ShaderKind::Fragment); + assert_eq!(guess_shader_kind("shader.rgen"), ShaderKind::RayGeneration); + } + + #[test] + fn guess_shader_kind_defaults_to_infer_from_source() { + assert_eq!( + guess_shader_kind("shader.unknown"), + ShaderKind::InferFromSource + ); + assert_eq!(guess_shader_kind("shader"), ShaderKind::InferFromSource); + } + + #[test] + fn guess_shader_source_language_from_known_extensions() { + assert_eq!( + guess_shader_source_language("shader.comp"), + Some(SourceLanguage::GLSL) + ); + assert_eq!( + guess_shader_source_language("shader.glsl"), + Some(SourceLanguage::GLSL) + ); + assert_eq!( + guess_shader_source_language("shader.hlsl"), + Some(SourceLanguage::HLSL) + ); + } + + #[test] + fn guess_shader_source_language_returns_none_for_unknown_extensions() { + assert_eq!(guess_shader_source_language("shader.spv"), None); + assert_eq!(guess_shader_source_language("shader"), None); + } +} + +macro_rules! pipeline { + ($name:ident) => { + ::paste::paste! { + impl [] { + fn cache( + &self, + ) -> ::std::sync::RwLockReadGuard<'_, HotPipeline<[<$name Pipeline>]>> { + self.cache + .read() + .expect("poisoned hot pipeline lock") + } + + fn cache_mut( + &self, + ) -> ::std::sync::RwLockWriteGuard<'_, HotPipeline<[<$name Pipeline>]>> { + self.cache + .write() + .expect("poisoned hot pipeline lock") + } + } + + impl [] { + /// Gets the debugging name assigned to this pipeline, if one has been set. + pub fn debug_name(&self) -> Option { + self.cache() + .pipeline + .debug_name() + .map(ToOwned::to_owned) + } + + /// The device which owns this pipeline. + pub fn device(&self) -> &Device { + &self.device + } + + /// Gets the information used to create this object. + pub fn info(&self) -> [<$name PipelineInfo>] { + self.cache() + .pipeline + .info() + } + + /// Sets the debugging name assigned to this pipeline. + /// + /// _Note:_ The pipeline name may only be assigned once. + /// Subsequent calls will not update the previously set name value. + pub fn set_debug_name(&mut self, name: impl Into) { + self.cache_mut() + .pipeline + .set_debug_name(name); + } + + /// Sets the debugging name assigned to this pipeline. + /// + /// _Note:_ The pipeline name may only be assigned once. + /// Subsequent calls will not update the previously set name value. + pub fn with_debug_name(mut self, name: impl Into) -> Self { + self.set_debug_name(name); + + self + } + } + + impl<'a> Pipeline<'a> for [] { + type Command = <[<$name Pipeline>] as Pipeline<'a>>::Command; + + fn bind_cmd(self, cmd: Command<'a>) -> Self::Command { + self.compile_shader_and_bind_cmd(cmd) + } + } + + impl<'a> Pipeline<'a> for &'a [] { + type Command = <[<$name Pipeline>] as Pipeline<'a>>::Command; + + fn bind_cmd(self, cmd: Command<'a>) -> Self::Command { + self.compile_shader_and_bind_cmd(cmd) + } + } + + } + }; +} + +use pipeline; + +// pipeline!(Graphic); +// pipeline!(RayTrace); + +macro_rules! pipeline_handle { + ($name:ident) => { + ::paste::paste! { + impl [] { + /// The native Vulkan pipeline handle of this pipeline. + pub fn handle(&self) -> ::vk_graph::driver::ash::vk::Pipeline { + self.cache() + .pipeline + .handle() + } + } + } + }; +} + +use pipeline_handle; + +#[derive(Debug)] +struct HotPipeline { + pipeline: T, + watcher: RecommendedWatcher, +} diff --git a/crates/vk-graph-hot/src/ray_trace.rs b/crates/vk-graph-hot/src/ray_trace.rs new file mode 100644 index 00000000..f181332b --- /dev/null +++ b/crates/vk-graph-hot/src/ray_trace.rs @@ -0,0 +1,92 @@ +//! Hot-reload ray-trace pipeline support. + +use { + super::{ + HotPipeline, compile_shaders_and_watch, create_watcher, pipeline, pipeline_handle, + shader::HotShader, + }, + log::info, + std::sync::{ + Arc, RwLock, + atomic::{AtomicBool, Ordering}, + }, + vk_graph::{ + cmd::{Command, Pipeline}, + driver::{ + DriverError, + device::Device, + ray_trace::{RayTracePipeline, RayTracePipelineInfo, RayTraceShaderGroup}, + }, + }, +}; + +/// A ray-trace pipeline wrapper that recompiles its shaders when source files change. +#[derive(Debug)] +pub struct HotRayTracePipeline { + cache: RwLock>, + device: Device, + has_changes: Arc, + shader_groups: Box<[RayTraceShaderGroup]>, + shaders: Box<[HotShader]>, +} + +impl HotRayTracePipeline { + /// Creates a hot-reload ray-trace pipeline from shader files and shader groups. + pub fn create( + device: &Device, + info: impl Into, + shaders: impl IntoIterator, + shader_groups: impl IntoIterator, + ) -> Result + where + S: Into, + { + let shaders = shaders.into_iter().map(Into::into).collect::>(); + let shader_groups = shader_groups.into_iter().collect::>(); + + let has_changes = Default::default(); + let mut watcher = create_watcher(&has_changes); + + let pipeline = RayTracePipeline::create( + device, + info, + compile_shaders_and_watch(&shaders, &mut watcher)?, + shader_groups.iter().copied(), + )?; + + Ok(Self { + cache: RwLock::new(HotPipeline { pipeline, watcher }), + device: device.clone(), + has_changes, + shader_groups, + shaders, + }) + } + + fn compile_shader_and_bind_cmd<'a>( + &self, + cmd: Command<'a>, + ) -> >::Command { + if self.has_changes.swap(false, Ordering::Relaxed) { + info!("Shader change detected"); + + let mut cache = self.cache_mut(); + + if let Ok(shaders) = compile_shaders_and_watch(&self.shaders, &mut cache.watcher) + && let Ok(pipeline) = RayTracePipeline::create( + &self.device, + cache.pipeline.info(), + shaders, + self.shader_groups.iter().copied(), + ) + { + cache.pipeline = pipeline; + } + } + + self.cache().pipeline.clone().bind_cmd(cmd) + } +} + +pipeline!(RayTrace); +pipeline_handle!(RayTrace); diff --git a/contrib/screen-13-hot/src/shader.rs b/crates/vk-graph-hot/src/shader.rs similarity index 62% rename from contrib/screen-13-hot/src/shader.rs rename to crates/vk-graph-hot/src/shader.rs index c5bb685f..3f566d22 100644 --- a/contrib/screen-13-hot/src/shader.rs +++ b/crates/vk-graph-hot/src/shader.rs @@ -1,13 +1,15 @@ +//! Hot-reload shader descriptions and builder APIs. + pub use shaderc::{OptimizationLevel, SourceLanguage, SpirvVersion}; use { super::{compile_shader, guess_shader_source_language}, derive_builder::{Builder, UninitializedFieldError}, - log::{debug, error}, + log::{Level, debug, error, log_enabled}, notify::{RecommendedWatcher, RecursiveMode, Watcher}, - screen_13::prelude::*, shaderc::{CompileOptions, EnvVersion, ShaderKind, TargetEnv}, std::path::{Path, PathBuf}, + vk_graph::driver::{DriverError, ash::vk, shader::SpecializationMap}, }; /// Describes a shader program which runs on some pipeline stage. @@ -18,7 +20,7 @@ use { #[allow(missing_docs)] #[derive(Builder, Clone, Debug)] #[builder( - build_fn(private, name = "fallible_build", error = "HotShaderBuilderError"), + build_fn(private, name = "fallible_build", error = "UninitializedFieldError"), derive(Clone, Debug), pattern = "owned" )] @@ -26,7 +28,7 @@ pub struct HotShader { /// The name of the entry point which will be executed by this shader. /// /// The default value is `main`. - #[builder(default = "\"main\".to_owned()")] + #[builder(default = "\"main\".to_owned()", setter(into))] pub entry_name: String, /// Macro definitions. @@ -38,6 +40,7 @@ pub struct HotShader { pub optimization_level: Option, /// Shader source code path. + #[builder(setter(custom))] pub path: PathBuf, /// Sets the source language. @@ -50,8 +53,8 @@ pub struct HotShader { /// /// Basic usage (GLSL): /// - /// ``` - /// # inline_spirv::inline_spirv!(r#" + /// ```glsl + /// // fire.comp /// #version 460 core /// /// // Defaults to 6 if not set using HotShader specialization_info! @@ -63,33 +66,21 @@ pub struct HotShader { /// { /// // Code uses MY_COUNT number of my_samplers here /// } - /// # "#, comp); /// ``` /// /// ```no_run - /// # use std::sync::Arc; - /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::shader::{SpecializationInfo}; - /// # use screen_13_hot::shader::HotShader; - /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); - /// # let my_shader_code = [0u8; 1]; + /// use vk_graph::driver::shader::SpecializationMap; + /// use vk_graph_hot::HotShader; + /// /// // We instead specify 42 for MY_COUNT: - /// let shader = HotShader::new_fragment(my_shader_code.as_slice()) - /// .specialization_info(SpecializationInfo::new( - /// [vk::SpecializationMapEntry { - /// constant_id: 0, - /// offset: 0, - /// size: 4, - /// }], - /// 42u32.to_ne_bytes() - /// )); - /// # Ok(()) } + /// let shader = HotShader::new_compute("shaders/fire.comp") + /// .specialization( + /// SpecializationMap::new(42u32.to_ne_bytes()) + /// .constant(0, 0, 4) + /// ); /// ``` #[builder(default, setter(strip_option))] - pub specialization_info: Option, + pub specialization: Option, /// The shader stage this structure applies to. pub stage: vk::ShaderStageFlags, @@ -209,22 +200,34 @@ impl HotShader { Self::new(vk::ShaderStageFlags::TASK_EXT, path) } - /// Creates a new tesselation control shader. + /// Creates a new tessellation control shader. /// /// # Panics /// /// If the shader code is invalid. - pub fn new_tesselation_ctrl(path: impl AsRef) -> HotShaderBuilder { + pub fn new_tessellation_ctrl(path: impl AsRef) -> HotShaderBuilder { Self::new(vk::ShaderStageFlags::TESSELLATION_CONTROL, path) } - /// Creates a new tesselation evaluation shader. + #[deprecated = "use new_tessellation_ctrl function"] + #[doc(hidden)] + pub fn new_tesselation_ctrl(path: impl AsRef) -> HotShaderBuilder { + Self::new_tessellation_ctrl(path) + } + + /// Creates a new tessellation evaluation shader. /// /// # Panics /// /// If the shader code is invalid. - pub fn new_tesselation_eval(spirv: impl AsRef) -> HotShaderBuilder { - Self::new(vk::ShaderStageFlags::TESSELLATION_EVALUATION, spirv) + pub fn new_tessellation_eval(path: impl AsRef) -> HotShaderBuilder { + Self::new(vk::ShaderStageFlags::TESSELLATION_EVALUATION, path) + } + + #[deprecated = "use new_tessellation_eval function"] + #[doc(hidden)] + pub fn new_tesselation_eval(path: impl AsRef) -> HotShaderBuilder { + Self::new_tessellation_eval(path) } /// Creates a new vertex shader. @@ -240,25 +243,35 @@ impl HotShader { &self, watcher: &mut RecommendedWatcher, ) -> Result, DriverError> { - let shader_kind = match self.stage { - vk::ShaderStageFlags::ANY_HIT_KHR => ShaderKind::AnyHit, - vk::ShaderStageFlags::CALLABLE_KHR => ShaderKind::Callable, - vk::ShaderStageFlags::CLOSEST_HIT_KHR => ShaderKind::ClosestHit, - vk::ShaderStageFlags::COMPUTE => ShaderKind::Compute, - vk::ShaderStageFlags::FRAGMENT => ShaderKind::Fragment, - vk::ShaderStageFlags::GEOMETRY => ShaderKind::Geometry, - vk::ShaderStageFlags::INTERSECTION_KHR => ShaderKind::Intersection, - vk::ShaderStageFlags::MISS_KHR => ShaderKind::Miss, - vk::ShaderStageFlags::RAYGEN_KHR => ShaderKind::RayGeneration, - vk::ShaderStageFlags::TASK_EXT => ShaderKind::Task, - vk::ShaderStageFlags::TESSELLATION_CONTROL => ShaderKind::TessControl, - vk::ShaderStageFlags::TESSELLATION_EVALUATION => ShaderKind::TessEvaluation, - vk::ShaderStageFlags::VERTEX => ShaderKind::Vertex, - _ => unimplemented!("{:?}", self.stage), + let shader_kind = if self.stage == vk::ShaderStageFlags::empty() { + None + } else { + Some(match self.stage { + vk::ShaderStageFlags::ANY_HIT_KHR => ShaderKind::AnyHit, + vk::ShaderStageFlags::CALLABLE_KHR => ShaderKind::Callable, + vk::ShaderStageFlags::CLOSEST_HIT_KHR => ShaderKind::ClosestHit, + vk::ShaderStageFlags::COMPUTE => ShaderKind::Compute, + vk::ShaderStageFlags::FRAGMENT => ShaderKind::Fragment, + vk::ShaderStageFlags::GEOMETRY => ShaderKind::Geometry, + vk::ShaderStageFlags::INTERSECTION_KHR => ShaderKind::Intersection, + vk::ShaderStageFlags::MISS_KHR => ShaderKind::Miss, + vk::ShaderStageFlags::RAYGEN_KHR => ShaderKind::RayGeneration, + vk::ShaderStageFlags::TASK_EXT => ShaderKind::Task, + vk::ShaderStageFlags::TESSELLATION_CONTROL => ShaderKind::TessControl, + vk::ShaderStageFlags::TESSELLATION_EVALUATION => ShaderKind::TessEvaluation, + vk::ShaderStageFlags::VERTEX => ShaderKind::Vertex, + _ => { + error!( + "unsupported shader stage for shaderc kind inference: {:?}", + self.stage + ); + return Err(DriverError::Unsupported); + } + }) }; - let mut additional_opts = CompileOptions::new().ok_or_else(|| { - error!("Unable to initialize compiler options"); + let mut additional_opts = CompileOptions::new().map_err(|err| { + error!("unable to initialize compiler options: {err:?}"); DriverError::Unsupported })?; @@ -296,11 +309,15 @@ impl HotShader { let res = compile_shader( &self.path, &self.entry_name, - Some(shader_kind), + shader_kind, Some(&additional_opts), ) .map_err(|err| { - error!("Unable to compile shader {}: {err}", self.path.display()); + if !log_enabled!(Level::Error) { + panic!("unable to compile shader {}: {err}", self.path.display()); + } + + error!("unable to compile shader {}: {err}", self.path.display()); DriverError::InvalidData })?; @@ -309,7 +326,7 @@ impl HotShader { watcher .watch(&path, RecursiveMode::NonRecursive) .map_err(|err| { - error!("Unable to watch file: {err}"); + error!("unable to watch file: {err}"); DriverError::Unsupported })?; @@ -317,6 +334,15 @@ impl HotShader { Ok(res.spirv_code) } + + /// Creates a shader using a shader kind inferred from the source code. + /// + /// # Panics + /// + /// If the shader code is invalid. + pub fn from_path(path: impl AsRef) -> HotShaderBuilder { + HotShaderBuilder::default().path(path) + } } impl From for HotShader { @@ -329,9 +355,7 @@ impl From for HotShader { impl HotShaderBuilder { /// Specifies a shader with the given `stage` and shader path values. pub fn new(stage: vk::ShaderStageFlags, path: impl AsRef) -> Self { - Self::default() - .stage(stage) - .path(path.as_ref().to_path_buf()) + Self::default().stage(stage).path(path) } /// Builds a new `HotShader`. @@ -341,8 +365,13 @@ impl HotShaderBuilder { #[cfg(target_os = "macos")] let this = this.macro_definition("MOLTEN_VK", Some("1".to_string())); - this.fallible_build() - .expect("All required fields set at initialization") + let mut this = this; + + if this.stage.is_none() { + this.stage = Some(vk::ShaderStageFlags::empty()); + } + + this.fallible_build().expect("invalid hot shader") } /// Defines a single macro. @@ -351,26 +380,56 @@ impl HotShaderBuilder { key: impl Into, value: impl Into>, ) -> Self { - if self.macro_definitions.is_none() || self.macro_definitions.as_ref().unwrap().is_none() { - self.macro_definitions = Some(Some(vec![])); - } + let macro_definitions = self + .macro_definitions + .get_or_insert_with(|| Some(Vec::new())) + .get_or_insert_with(Vec::new); + macro_definitions.push((key.into(), value.into())); - self.macro_definitions - .as_mut() - .unwrap() - .as_mut() - .unwrap() - .push((key.into(), value.into())); + self + } + /// Shader source code path. + pub fn path(mut self, path: impl AsRef) -> Self { + self.path = Some(path.as_ref().to_owned()); self } } -#[derive(Debug)] -struct HotShaderBuilderError; +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn new_compute_sets_stage_and_path() { + let shader = HotShader::new_compute("shader.comp").build(); + + assert_eq!(shader.stage, vk::ShaderStageFlags::COMPUTE); + assert_eq!(shader.path, PathBuf::from("shader.comp")); + assert_eq!(shader.entry_name, "main"); + } + + #[test] + fn from_path_defaults_to_empty_stage_for_inference() { + let shader = HotShader::from_path("shader.glsl").build(); + + assert!(shader.stage.is_empty()); + assert_eq!(shader.path, PathBuf::from("shader.glsl")); + } -impl From for HotShaderBuilderError { - fn from(_: UninitializedFieldError) -> Self { - Self + #[test] + fn macro_definition_accumulates_values() { + let shader = HotShader::new_fragment("shader.frag") + .macro_definition("FOO", Some("1".to_owned())) + .macro_definition("BAR", None::) + .build(); + + assert_eq!( + shader.macro_definitions, + Some(vec![ + ("FOO".to_owned(), Some("1".to_owned())), + ("BAR".to_owned(), None), + ]) + ); } } diff --git a/crates/vk-graph-imgui/CHANGELOG.md b/crates/vk-graph-imgui/CHANGELOG.md new file mode 100644 index 00000000..97d366ed --- /dev/null +++ b/crates/vk-graph-imgui/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this crate will be documented in this file. + +This crate is versioned independently from `vk-graph`. + +## [Unreleased] + +## [0.1.0] - 2026-05-30 + +- Initial release. +- Supports `vk-graph` `0.14`. diff --git a/crates/vk-graph-imgui/Cargo.toml b/crates/vk-graph-imgui/Cargo.toml new file mode 100644 index 00000000..029c4962 --- /dev/null +++ b/crates/vk-graph-imgui/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "vk-graph-imgui" +version = "0.1.0" +authors = ["John Wells "] +description = "Dear ImGui renderer integration for vk-graph" +documentation = "https://docs.rs/vk-graph-imgui" +homepage = "https://github.com/attackgoat/vk-graph/tree/main/crates/vk-graph-imgui" +keywords = ["imgui", "gamedev", "vulkan"] +categories = ["game-development", "multimedia::images", "rendering::engine"] +edition.workspace = true +license.workspace = true +readme = "README.md" +repository.workspace = true + +[dependencies] +bytemuck = "1.25" +imgui = "0.12" +imgui-winit-support = "0.13" +log.workspace = true +vk-graph.workspace = true +vk-shader-macros = "0.2" diff --git a/crates/vk-graph-imgui/README.md b/crates/vk-graph-imgui/README.md new file mode 100644 index 00000000..3bb590e8 --- /dev/null +++ b/crates/vk-graph-imgui/README.md @@ -0,0 +1,9 @@ +# _vk-graph_ _Dear imGui_ Integration + +Renderer integration for [Dear ImGui](https://github.com/imgui-rs/imgui-rs) on top of `vk-graph`. +This crate is versioned independently from `vk-graph`. + +Preview + +See the [example code](../../examples/README.md) and the workspace +[guide book](https://attackgoat.github.io/vk-graph). diff --git a/contrib/screen-13-imgui/res/font/mplus-1p/LICENSE b/crates/vk-graph-imgui/res/font/mplus-1p/LICENSE similarity index 100% rename from contrib/screen-13-imgui/res/font/mplus-1p/LICENSE rename to crates/vk-graph-imgui/res/font/mplus-1p/LICENSE diff --git a/contrib/screen-13-imgui/res/font/mplus-1p/mplus-1p-regular.ttf b/crates/vk-graph-imgui/res/font/mplus-1p/mplus-1p-regular.ttf similarity index 100% rename from contrib/screen-13-imgui/res/font/mplus-1p/mplus-1p-regular.ttf rename to crates/vk-graph-imgui/res/font/mplus-1p/mplus-1p-regular.ttf diff --git a/contrib/screen-13-imgui/res/font/roboto/LICENSE b/crates/vk-graph-imgui/res/font/roboto/LICENSE similarity index 100% rename from contrib/screen-13-imgui/res/font/roboto/LICENSE rename to crates/vk-graph-imgui/res/font/roboto/LICENSE diff --git a/contrib/screen-13-imgui/res/font/roboto/roboto-regular.ttf b/crates/vk-graph-imgui/res/font/roboto/roboto-regular.ttf similarity index 100% rename from contrib/screen-13-imgui/res/font/roboto/roboto-regular.ttf rename to crates/vk-graph-imgui/res/font/roboto/roboto-regular.ttf diff --git a/contrib/screen-13-imgui/res/shader/imgui.frag b/crates/vk-graph-imgui/res/shader/imgui.frag similarity index 100% rename from contrib/screen-13-imgui/res/shader/imgui.frag rename to crates/vk-graph-imgui/res/shader/imgui.frag diff --git a/contrib/screen-13-imgui/res/shader/imgui.vert b/crates/vk-graph-imgui/res/shader/imgui.vert similarity index 100% rename from contrib/screen-13-imgui/res/shader/imgui.vert rename to crates/vk-graph-imgui/res/shader/imgui.vert diff --git a/contrib/screen-13-imgui/src/lib.rs b/crates/vk-graph-imgui/src/lib.rs similarity index 51% rename from contrib/screen-13-imgui/src/lib.rs rename to crates/vk-graph-imgui/src/lib.rs index af12d4e3..40a8205f 100644 --- a/contrib/screen-13-imgui/src/lib.rs +++ b/crates/vk-graph-imgui/src/lib.rs @@ -1,9 +1,17 @@ +//! Dear ImGui renderer integration for `vk-graph`. + +#![deny(missing_docs)] +#![deny(rustdoc::broken_intra_doc_links)] + +/// Common imports for applications using the ImGui integration. pub mod prelude { - pub use super::{imgui, Condition, ImGui, Ui}; + pub use super::{Condition, ImGui, Ui, imgui}; } pub use imgui::{self, Condition, Ui}; +type DrawCmdInfo = (usize, [f32; 4], usize, usize); + use { bytemuck::cast_slice, imgui::{Context, DrawCmd, DrawCmdParams}, @@ -11,36 +19,74 @@ use { winit::{event::Event, window::Window}, {HiDpiMode, WinitPlatform}, }, - inline_spirv::include_spirv, - screen_13::prelude::*, + log::warn, std::{sync::Arc, time::Duration}, + vk_graph::{ + Graph, + cmd::{LoadOp, StoreOp}, + driver::{ + ash::vk, + buffer::{Buffer, BufferInfo}, + device::Device, + graphic::{BlendInfo, GraphicPipeline, GraphicPipelineInfo}, + image::{Image, ImageInfo}, + shader::Shader, + sync::AccessType, + }, + node::ImageLeaseNode, + pool::{Lease, Pool}, + }, + vk_shader_macros::include_glsl, }; +/// Dear ImGui renderer state backed by `vk-graph` resources. #[derive(Debug)] pub struct ImGui { context: Context, font_atlas_image: Option>>, - pipeline: Arc, + pipeline: GraphicPipeline, platform: WinitPlatform, } +fn supported_draw_cmd(draw_cmd: DrawCmd) -> Option { + match draw_cmd { + DrawCmd::Elements { + count, + cmd_params: + DrawCmdParams { + clip_rect, + idx_offset, + vtx_offset, + .. + }, + } => Some((count, clip_rect, idx_offset, vtx_offset)), + DrawCmd::ResetRenderState => { + warn!("unsupported imgui draw command: reset render state"); + None + } + DrawCmd::RawCallback { .. } => { + warn!("unsupported imgui draw command: raw callback"); + None + } + } +} + impl ImGui { - pub fn new(device: &Arc) -> Self { + /// Creates a new ImGui renderer for the given device. + pub fn new(device: &Device) -> Self { let mut context = Context::create(); let platform = WinitPlatform::new(&mut context); - let pipeline = Arc::new( - GraphicPipeline::create( - device, - GraphicPipelineInfoBuilder::default() - .blend(BlendMode::PRE_MULTIPLIED_ALPHA) - .cull_mode(vk::CullModeFlags::NONE), - [ - Shader::new_vertex(include_spirv!("res/shader/imgui.vert", vert).as_slice()), - Shader::new_fragment(include_spirv!("res/shader/imgui.frag", frag).as_slice()), - ], - ) - .unwrap(), - ); + let pipeline = GraphicPipeline::create( + device, + GraphicPipelineInfo::builder() + .blend(BlendInfo::PRE_MULTIPLIED_ALPHA) + .cull_mode(vk::CullModeFlags::NONE), + [ + Shader::new_vertex(include_glsl!("res/shader/imgui.vert").as_slice()), + Shader::new_fragment(include_glsl!("res/shader/imgui.frag").as_slice()), + ], + ) + .expect("invalid imgui pipeline"); Self { context, @@ -50,15 +96,17 @@ impl ImGui { } } - // TODO: This produces an image which is RGBA8 UNORM and has STORAGE set. *We* don't need storage here and should instead ask the user what settings to give the output image..... + // TODO: This produces an image which is RGBA8 UNORM and has STORAGE set. *We* don't need + // storage here and should instead ask the user what settings to give the output image..... + /// Builds a frame, records the necessary draw commands, and returns the rendered image. pub fn draw

( &mut self, dt: f32, events: &[Event<()>], window: &Window, pool: &mut P, - render_graph: &mut RenderGraph, - ui_func: impl FnOnce(&mut Ui, &mut P, &mut RenderGraph), + graph: &mut Graph, + ui_func: impl FnOnce(&mut Ui, &mut P, &mut Graph), ) -> ImageLeaseNode where P: Pool + Pool, @@ -69,7 +117,7 @@ impl ImGui { .attach_window(self.context.io_mut(), window, HiDpiMode::Default); if self.font_atlas_image.is_none() || self.platform.hidpi_factor() != hidpi { - self.lease_font_atlas_image(pool, render_graph); + self.lease_font_atlas_image(pool, graph); } let io = self.context.io_mut(); @@ -81,19 +129,19 @@ impl ImGui { self.platform .prepare_frame(io, window) - .expect("Unable to prepare ImGui frame"); + .expect("invalid imgui frame"); // Let the caller draw the GUI let ui = self.context.frame(); - ui_func(ui, pool, render_graph); + ui_func(ui, pool, graph); self.platform.prepare_render(ui, window); let draw_data = self.context.render(); - let image = render_graph.bind_node({ + let image = graph.bind_resource({ let mut image = pool - .lease(ImageInfo::image_2d( + .resource(ImageInfo::image_2d( window.inner_size().width, window.inner_size().height, vk::Format::R8G8B8A8_UNORM, @@ -101,46 +149,51 @@ impl ImGui { | vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::STORAGE | vk::ImageUsageFlags::TRANSFER_DST - | vk::ImageUsageFlags::TRANSFER_SRC, // TODO: Make TRANSFER_SRC an "extra flags" + | vk::ImageUsageFlags::TRANSFER_SRC, /* TODO: Make TRANSFER_SRC an + * "extra flags" */ )) - .unwrap(); - image.as_mut().name = Some("ImGui Output".to_string()); + .expect("missing imgui output image"); + image.name = Some("ImGui Output".to_string()); image }); - let font_atlas_image = render_graph.bind_node(self.font_atlas_image.as_ref().unwrap()); + let font_atlas_image = graph.bind_resource( + self.font_atlas_image + .as_ref() + .expect("missing imgui font atlas image"), + ); let display_pos = draw_data.display_pos; let framebuffer_scale = draw_data.framebuffer_scale; - if draw_data.draw_lists_count() == 0 { - render_graph.clear_color_image(image); + graph.clear_color_image(image, [0f32; 4]); + if draw_data.draw_lists_count() == 0 { return image; } for draw_list in draw_data.draw_lists() { let indices = cast_slice(draw_list.idx_buffer()); let mut index_buf = pool - .lease(BufferInfo::host_mem( + .resource(BufferInfo::host_mem( indices.len() as _, vk::BufferUsageFlags::INDEX_BUFFER, )) - .unwrap(); + .expect("missing imgui index buffer"); { Buffer::mapped_slice_mut(&mut index_buf)[0..indices.len()].copy_from_slice(indices); } - let index_buf = render_graph.bind_node(index_buf); + let index_buf = graph.bind_resource(index_buf); let vertices = draw_list.vtx_buffer(); let vertex_buf_len = vertices.len() * 20; let mut vertex_buf = pool - .lease(BufferInfo::host_mem( + .resource(BufferInfo::host_mem( vertex_buf_len as _, vk::BufferUsageFlags::VERTEX_BUFFER, )) - .unwrap(); + .expect("missing imgui vertex buffer"); { let vertex_buf = Buffer::mapped_slice_mut(&mut vertex_buf); @@ -152,23 +205,11 @@ impl ImGui { } } - let vertex_buf = render_graph.bind_node(vertex_buf); + let vertex_buf = graph.bind_resource(vertex_buf); let draw_cmds = draw_list .commands() - .map(|draw_cmd| match draw_cmd { - DrawCmd::Elements { - count, - cmd_params: - DrawCmdParams { - clip_rect, - idx_offset, - vtx_offset, - .. - }, - } => (count, clip_rect, idx_offset, vtx_offset), - _ => unimplemented!(), - }) + .filter_map(supported_draw_cmd) .collect::>(); let window_width = @@ -176,20 +217,23 @@ impl ImGui { let window_height = self.platform.hidpi_factor() as f32 / window.inner_size().height as f32; - render_graph - .begin_pass("imgui") + graph + .begin_cmd() + .debug_name("imgui") .bind_pipeline(&self.pipeline) - .access_node(index_buf, AccessType::IndexBuffer) - .access_node(vertex_buf, AccessType::VertexBuffer) - .read_descriptor(0, font_atlas_image) - .clear_color(0, image) - .store_color(0, image) - .record_subpass(move |subpass, _| { - subpass - .push_constants_offset(0, &window_width.to_ne_bytes()) - .push_constants_offset(4, &window_height.to_ne_bytes()) - .bind_index_buffer(index_buf, vk::IndexType::UINT16) - .bind_vertex_buffer(vertex_buf); + .resource_access(index_buf, AccessType::IndexBuffer) + .resource_access(vertex_buf, AccessType::VertexBuffer) + .shader_resource_access( + 0, + font_atlas_image, + AccessType::FragmentShaderReadSampledImageOrUniformTexelBuffer, + ) + .color_attachment_image(0, image, LoadOp::Load, StoreOp::Store) + .record_cmd(move |cmd| { + cmd.push_constants(0, &window_width.to_ne_bytes()) + .push_constants(4, &window_height.to_ne_bytes()) + .bind_index_buffer(index_buf, 0, vk::IndexType::UINT16) + .bind_vertex_buffer(0, vertex_buf, 0); for (index_count, clip_rect, first_index, vertex_offset) in draw_cmds { let clip_rect = [ @@ -202,8 +246,14 @@ impl ImGui { let y = clip_rect[1].floor() as i32; let width = (clip_rect[2] - clip_rect[0]).ceil() as u32; let height = (clip_rect[3] - clip_rect[1]).ceil() as u32; - subpass.set_scissor(x, y, width, height); - subpass.draw_indexed( + cmd.set_scissor( + 0, + &[vk::Rect2D { + offset: vk::Offset2D { x, y }, + extent: vk::Extent2D { width, height }, + }], + ) + .draw_indexed( index_count as _, 1, first_index as _, @@ -217,7 +267,7 @@ impl ImGui { image } - fn lease_font_atlas_image

(&mut self, pool: &mut P, render_graph: &mut RenderGraph) + fn lease_font_atlas_image

(&mut self, pool: &mut P, graph: &mut Graph) where P: Pool + Pool, { @@ -255,36 +305,62 @@ impl ImGui { let texture = fonts.build_rgba32_texture(); // TODO: Fix fb channel writes and use alpha8! let temp_buf_len = texture.data.len(); let mut temp_buf = pool - .lease(BufferInfo::host_mem( + .resource(BufferInfo::host_mem( temp_buf_len as _, vk::BufferUsageFlags::TRANSFER_SRC, )) - .unwrap(); + .expect("missing imgui font atlas buffer"); { let temp_buf = Buffer::mapped_slice_mut(&mut temp_buf); temp_buf[0..temp_buf_len].copy_from_slice(texture.data); } - let temp_buf = render_graph.bind_node(temp_buf); - let image = render_graph.bind_node({ - let mut image = pool - .lease(ImageInfo::image_2d( - texture.width, - texture.height, - vk::Format::R8G8B8A8_UNORM, - vk::ImageUsageFlags::SAMPLED - | vk::ImageUsageFlags::STORAGE - | vk::ImageUsageFlags::TRANSFER_DST, - )) - .unwrap(); - image.as_mut().name = Some("ImGui Font Atlas".to_string()); + let temp_buf = graph.bind_resource(temp_buf); + let image = graph.bind_resource( + pool.resource(ImageInfo::image_2d( + texture.width, + texture.height, + vk::Format::R8G8B8A8_UNORM, + vk::ImageUsageFlags::SAMPLED + | vk::ImageUsageFlags::STORAGE + | vk::ImageUsageFlags::TRANSFER_DST, + )) + .expect("missing imgui font atlas image") + .debug_name("ImGui Font Atlas"), + ); - image - }); + graph.copy_buffer_to_image(temp_buf, image); - render_graph.copy_buffer_to_image(temp_buf, image); + self.font_atlas_image = Some(graph.resource(image).clone()); + } +} + +#[cfg(test)] +mod test { + use super::supported_draw_cmd; + use imgui::{DrawCmd, DrawCmdParams, TextureId}; + + #[test] + fn supported_draw_cmd_extracts_element_draws() { + let draw_cmd = DrawCmd::Elements { + count: 42, + cmd_params: DrawCmdParams { + clip_rect: [1.0, 2.0, 3.0, 4.0], + texture_id: TextureId::new(7), + vtx_offset: 5, + idx_offset: 6, + }, + }; + + assert_eq!( + supported_draw_cmd(draw_cmd), + Some((42, [1.0, 2.0, 3.0, 4.0], 6, 5)) + ); + } - self.font_atlas_image = Some(render_graph.unbind_node(image)); + #[test] + fn supported_draw_cmd_skips_reset_render_state() { + assert_eq!(supported_draw_cmd(DrawCmd::ResetRenderState), None); } } diff --git a/crates/vk-graph-window/CHANGELOG.md b/crates/vk-graph-window/CHANGELOG.md new file mode 100644 index 00000000..97d366ed --- /dev/null +++ b/crates/vk-graph-window/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this crate will be documented in this file. + +This crate is versioned independently from `vk-graph`. + +## [Unreleased] + +## [0.1.0] - 2026-05-30 + +- Initial release. +- Supports `vk-graph` `0.14`. diff --git a/crates/vk-graph-window/Cargo.toml b/crates/vk-graph-window/Cargo.toml new file mode 100644 index 00000000..7a120ebc --- /dev/null +++ b/crates/vk-graph-window/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "vk-graph-window" +version = "0.1.0" +authors = ["John Wells "] +description = "winit window and swapchain integration for vk-graph" +documentation = "https://docs.rs/vk-graph-window" +homepage = "https://github.com/attackgoat/vk-graph/tree/main/crates/vk-graph-window" +keywords = ["gamedev", "vulkan", "winit"] +categories = ["game-development", "multimedia::images", "rendering::engine"] +edition.workspace = true +license.workspace = true +readme = "README.md" +repository.workspace = true + +[dependencies] +derive_builder = "0.20" +log.workspace = true +profiling.workspace = true +read-only.workspace = true +vk-graph.workspace = true +winit.workspace = true + +[dev-dependencies] +pretty_env_logger = "0.5" diff --git a/crates/vk-graph-window/README.md b/crates/vk-graph-window/README.md new file mode 100644 index 00000000..26828f88 --- /dev/null +++ b/crates/vk-graph-window/README.md @@ -0,0 +1,14 @@ +# _vk-graph_ Window Helper + +This crate provides `winit` integration for creating windows and rendering frames with +`vk-graph`. +This crate is versioned independently from `vk-graph`. + +```sh +cargo run --example hello_world +``` + +See the workspace [guide book](https://attackgoat.github.io/vk-graph) and +[examples](../../examples/README.md) for end-to-end usage. + +hello_world.rs diff --git a/contrib/screen-13-window/examples/hello_world.rs b/crates/vk-graph-window/examples/hello_world.rs similarity index 54% rename from contrib/screen-13-window/examples/hello_world.rs rename to crates/vk-graph-window/examples/hello_world.rs index 93abe253..8950a147 100644 --- a/contrib/screen-13-window/examples/hello_world.rs +++ b/crates/vk-graph-window/examples/hello_world.rs @@ -1,4 +1,4 @@ -use screen_13_window::{Window, WindowError}; +use vk_graph_window::{Window, WindowError}; /// This example requires a color graphics adapter. fn main() -> Result<(), WindowError> { @@ -6,7 +6,7 @@ fn main() -> Result<(), WindowError> { Window::new()?.run(|frame| { frame - .render_graph - .clear_color_image_value(frame.swapchain_image, [100u8, 149, 237]); + .graph + .clear_color_image(frame.swapchain_image, [100u8, 149, 237, 255]); }) } diff --git a/contrib/screen-13-window/src/frame.rs b/crates/vk-graph-window/src/frame.rs similarity index 84% rename from contrib/screen-13-window/src/frame.rs rename to crates/vk-graph-window/src/frame.rs index 7773181e..28b53f37 100644 --- a/contrib/screen-13-window/src/frame.rs +++ b/crates/vk-graph-window/src/frame.rs @@ -1,9 +1,5 @@ use { - screen_13::{ - driver::device::Device, - graph::{node::SwapchainImageNode, RenderGraph}, - }, - std::sync::Arc, + vk_graph::{Graph, driver::device::Device, node::SwapchainImageNode}, winit::{dpi::PhysicalPosition, event::Event, window::Window}, }; @@ -21,10 +17,10 @@ pub fn set_cursor_position(window: &Window, x: u32, y: u32) { window.set_cursor_position(position).unwrap_or_default(); } -/// A request to render a single frame to the provided render graph. +/// A request to render a single frame to the provided graph. pub struct FrameContext<'a> { /// The device this frame belongs to. - pub device: &'a Arc, + pub device: &'a Device, /// A slice of events that have occurred since the previous frame. pub events: &'a [Event<()>], @@ -32,10 +28,10 @@ pub struct FrameContext<'a> { /// The height, in pixels, of the current frame. pub height: u32, - /// A render graph which rendering commands should be recorded into. + /// A graph which rendering commands should be recorded into. /// /// Make sure to write to `swapchain_image` as part of this graph. - pub render_graph: &'a mut RenderGraph, + pub graph: &'a mut Graph, /// A pre-bound image node for the swapchain image to be drawn. pub swapchain_image: SwapchainImageNode, diff --git a/contrib/screen-13-window/src/lib.rs b/crates/vk-graph-window/src/lib.rs similarity index 68% rename from contrib/screen-13-window/src/lib.rs rename to crates/vk-graph-window/src/lib.rs index 5a291d19..b8099460 100644 --- a/contrib/screen-13-window/src/lib.rs +++ b/crates/vk-graph-window/src/lib.rs @@ -1,22 +1,28 @@ +//! `winit` window, event loop, and swapchain helpers for `vk-graph`. + +#![deny(missing_docs)] +#![deny(rustdoc::broken_intra_doc_links)] + mod frame; +pub mod swapchain; -pub use self::frame::FrameContext; +pub use {self::frame::FrameContext, winit}; use { - log::{info, trace, warn}, - screen_13::{ + self::swapchain::{Swapchain, SwapchainError, SwapchainInfo}, + log::{error, info, trace, warn}, + std::{error, fmt, ops::Deref}, + vk_graph::{ + Graph, driver::{ + DriverError, ash::vk, device::{Device, DeviceInfo}, surface::Surface, - swapchain::{Swapchain, SwapchainInfo}, - DriverError, }, - graph::RenderGraph, pool::hash::HashPool, - Display, DisplayError, DisplayInfoBuilder, }, - std::{error, fmt, sync::Arc}, + winit::raw_window_handle::{DisplayHandle, HandleError, HasDisplayHandle}, winit::{ application::ApplicationHandler, error::EventLoopError, @@ -27,6 +33,9 @@ use { }, }; +/// A closure type for picking surface formats. +pub type SurfaceFormatFn = dyn Fn(&[vk::SurfaceFormatKHR]) -> vk::SurfaceFormatKHR; + /// Describes a screen mode for display. #[derive(Clone, Copy, Debug)] pub enum FullscreenMode { @@ -37,22 +46,41 @@ pub enum FullscreenMode { Exclusive, } -// #[derive(Debug)] +/// A convenience wrapper that owns a `winit` event loop and a compatible `vk-graph` device. +#[read_only::embed] pub struct Window { data: WindowData, - pub device: Arc, - event_loop: EventLoop<()>, + + /// A device which is compatible with this window. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub device: Device, + + #[readonly] + pub(self) event_loop: EventLoop<()>, +} + +impl Deref for ReadOnlyWindow { + type Target = EventLoop<()>; + + fn deref(&self) -> &Self::Target { + &self.event_loop + } } impl Window { + /// Creates a window using the default [`WindowBuilder`] configuration. pub fn new() -> Result { Self::builder().build() } + /// Creates a builder for configuring a [`Window`] before construction. pub fn builder() -> WindowBuilder { - WindowBuilder::default() + Default::default() } + /// Runs the application event loop and invokes `draw_fn` for each rendered frame. pub fn run(self, draw_fn: F) -> Result<(), WindowError> where F: FnMut(FrameContext), @@ -60,18 +88,18 @@ impl Window { struct Application { active_window: Option, data: WindowData, - device: Arc, + device: Device, draw_fn: F, error: Option, primary_monitor: Option, } impl Application { - fn create_display( + fn create_swapchain( &mut self, window: &winit::window::Window, - ) -> Result { - let surface = Surface::create(&self.device, &window)?; + ) -> Result { + let surface = Surface::create(&self.device, window, window)?; let surface_formats = Surface::formats(&surface)?; let surface_format = self .data @@ -83,36 +111,45 @@ impl Window { let mut swapchain_info = SwapchainInfo::new(window_size.width, window_size.height, surface_format) - .to_builder(); + .into_builder() + .command_buffer_count(self.data.cmd_buf_count); - if let Some(image_count) = self.data.image_count { - swapchain_info = swapchain_info.desired_image_count(image_count); + if let Some(min_image_count) = self.data.min_image_count { + swapchain_info = swapchain_info.min_image_count(min_image_count); } - if let Some(v_sync) = self.data.v_sync { - swapchain_info = if v_sync { - swapchain_info.present_modes(vec![ - vk::PresentModeKHR::FIFO_RELAXED, - vk::PresentModeKHR::FIFO, - ]) + let v_sync = self.data.v_sync.unwrap_or_default(); + let present_modes = Surface::present_modes(&surface)?; + if !present_modes.is_empty() { + let best_modes = if v_sync { + [vk::PresentModeKHR::FIFO_RELAXED, vk::PresentModeKHR::FIFO].as_slice() } else { - swapchain_info.present_modes(vec![ - vk::PresentModeKHR::MAILBOX, - vk::PresentModeKHR::IMMEDIATE, - ]) + [vk::PresentModeKHR::MAILBOX, vk::PresentModeKHR::IMMEDIATE].as_slice() }; + + swapchain_info = swapchain_info.present_mode( + best_modes + .iter() + .copied() + .find(|best| present_modes.contains(best)) + .or_else(|| { + warn!("requested present modes unsupported: {best_modes:?}"); + + present_modes.first().copied() + }) + .ok_or_else(|| { + error!("display does not support presentation"); + + DriverError::Unsupported + })?, + ); } - let swapchain = Swapchain::new(&self.device, surface, swapchain_info)?; - let display = Display::new( - &self.device, - swapchain, - DisplayInfoBuilder::default().command_buffer_count(self.data.cmd_buf_count), - )?; + let swapchain = Swapchain::new(surface, swapchain_info)?; - trace!("created display"); + trace!("created swapchain"); - Ok(display) + Ok(swapchain) } fn window_mode_attributes( @@ -141,8 +178,9 @@ impl Window { monitor.video_modes().find(|mode| { let mode_size = mode.size(); - // Don't pick a mode which has greater resolution than the monitor is - // currently using: it causes a panic on x11 in winit + // Don't pick a mode with greater resolution + // than the monitor. + // It can panic on x11 in winit. mode_size.height <= monitor_size.height && mode_size.width <= monitor_size.width }) @@ -160,7 +198,7 @@ impl Window { winit::window::Fullscreen::Exclusive(video_mode) } else { - warn!("Using borderless fullscreen"); + warn!("unsupported exclusive fullscreen mode"); inner_size = None; @@ -187,7 +225,11 @@ impl Window { where F: FnMut(FrameContext), { - fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + if event_loop.exiting() { + return; + } + if let Some(ActiveWindow { window, .. }) = self.active_window.as_ref() { window.request_redraw(); } @@ -214,7 +256,7 @@ impl Window { let window = match event_loop.create_window(self.data.attributes.clone()) { Err(err) => { - warn!("Unable to create window: {err}"); + warn!("unable to create window: {err}"); self.error = Some(EventLoopError::Os(err).into()); event_loop.exit(); @@ -223,9 +265,9 @@ impl Window { } Ok(res) => res, }; - let display = match self.create_display(&window) { + let swapchain = match self.create_swapchain(&window) { Err(err) => { - warn!("Unable to create swapchain: {err}"); + warn!("unable to create swapchain: {err}"); self.error = Some(err.into()); event_loop.exit(); @@ -234,12 +276,12 @@ impl Window { } Ok(res) => res, }; - let display_pool = HashPool::new(&self.device); + let swapchain_pool = HashPool::new(&self.device); self.active_window = Some(ActiveWindow { - display, - display_pool, - display_resize: None, + swapchain, + swapchain_pool, + swapchain_resize: None, events: vec![], window, }); @@ -260,21 +302,34 @@ impl Window { if let Some(active_window) = self.active_window.as_mut() { match &event { WindowEvent::CloseRequested => { + if event_loop.exiting() { + return; + } + info!("close requested"); event_loop.exit(); } WindowEvent::RedrawRequested => { - let draw = active_window.draw(&self.device, &mut self.draw_fn); + if event_loop.exiting() { + return; + } - profiling::finish_frame!(); + match active_window.draw(&self.device, &mut self.draw_fn) { + Ok(true) => {} + res => { + if let Err(err) = res { + self.error = Some(WindowError::Swapchain(err)); + } - if !draw.unwrap() { - event_loop.exit(); + event_loop.exit(); + } } + + profiling::finish_frame!(); } WindowEvent::Resized(size) => { - active_window.display_resize = Some((size.width, size.height)); + active_window.swapchain_resize = Some((size.width, size.height)); } _ => (), } @@ -287,9 +342,9 @@ impl Window { } struct ActiveWindow { - display: Display, - display_pool: HashPool, - display_resize: Option<(u32, u32)>, + swapchain: Swapchain, + swapchain_pool: HashPool, + swapchain_resize: Option<(u32, u32)>, events: Vec>, window: winit::window::Window, } @@ -297,20 +352,20 @@ impl Window { impl ActiveWindow { fn draw( &mut self, - device: &Arc, + device: &Device, mut f: impl FnMut(FrameContext), - ) -> Result { - if let Some((width, height)) = self.display_resize.take() { - let mut swapchain_info = self.display.swapchain_info(); + ) -> Result { + if let Some((width, height)) = self.swapchain_resize.take() { + let mut swapchain_info = self.swapchain.info; swapchain_info.width = width; swapchain_info.height = height; - self.display.set_swapchain_info(swapchain_info); + self.swapchain.set_info(swapchain_info); } - if let Some(swapchain_image) = self.display.acquire_next_image()? { - let mut render_graph = RenderGraph::new(); - let swapchain_image = render_graph.bind_node(swapchain_image); - let swapchain_info = self.display.swapchain_info(); + if let Some(swapchain_image) = self.swapchain.acquire_next_image()? { + let mut graph = Graph::default(); + let swapchain_image = graph.bind_resource(swapchain_image); + let swapchain_info = self.swapchain.info; let mut will_exit = false; @@ -320,7 +375,7 @@ impl Window { device, events: &self.events, height: swapchain_info.height, - render_graph: &mut render_graph, + graph: &mut graph, swapchain_image, width: swapchain_info.width, will_exit: &mut will_exit, @@ -336,8 +391,8 @@ impl Window { } self.window.pre_present_notify(); - self.display - .present_image(&mut self.display_pool, render_graph, swapchain_image, 0) + self.swapchain + .present_image(&mut self.swapchain_pool, graph, swapchain_image, 0) .inspect_err(|err| { warn!("unable to present swapchain image: {err}"); })?; @@ -354,19 +409,19 @@ impl Window { let mut app = Application { active_window: None, data: self.data, - device: self.device, + device: self.read_only.device, draw_fn, error: None, primary_monitor: None, }; - self.event_loop.run_app(&mut app)?; + self.read_only.event_loop.run_app(&mut app)?; if let Some(ActiveWindow { - display, window, .. + swapchain, window, .. }) = app.active_window.take() { - drop(display); + drop(swapchain); drop(window); } @@ -380,38 +435,39 @@ impl Window { } } -impl AsRef> for Window { - fn as_ref(&self) -> &EventLoop<()> { - &self.event_loop +impl HasDisplayHandle for Window { + fn display_handle(&self) -> Result, HandleError> { + self.event_loop.display_handle() } } +/// Builder for configuring and constructing a [`Window`]. pub struct WindowBuilder { attributes: WindowAttributes, cmd_buf_count: usize, device_info: DeviceInfo, - image_count: Option, - surface_format_fn: Option vk::SurfaceFormatKHR>>, + min_image_count: Option, + surface_format_fn: Option>, v_sync: Option, window_mode_override: Option>, } impl WindowBuilder { + /// Builds the window, event loop, and compatible `vk-graph` device. pub fn build(self) -> Result { let event_loop = EventLoop::new()?; - let device = Arc::new(Device::create_display(self.device_info, &event_loop)?); + let device = Device::try_from_display(&event_loop, self.device_info)?; Ok(Window { data: WindowData { attributes: self.attributes, cmd_buf_count: self.cmd_buf_count, - image_count: self.image_count, + min_image_count: self.min_image_count, surface_format_fn: self.surface_format_fn, v_sync: self.v_sync, window_mode_override: self.window_mode_override, }, - device, - event_loop, + read_only: ReadOnlyWindow { device, event_loop }, }) } @@ -430,7 +486,7 @@ impl WindowBuilder { /// Enables Vulkan graphics debugging layers. /// - /// _NOTE:_ Any valdation warnings or errors will cause the current thread to park itself after + /// _NOTE:_ Any validation warnings or errors will cause the current thread to park itself after /// describing the error using the `log` crate. This makes it easy to attach a debugger and see /// what is causing the issue directly. /// @@ -453,11 +509,13 @@ impl WindowBuilder { self } - /// The desired, but not guaranteed, number of images that will be in the created swapchain. + /// The minimum number of presentable images that the application needs. The implementation will + /// either create the swapchain with at least that many images, or it will fail to create the + /// swapchain. /// - /// More images introduces more display lag, but smoother animation. - pub fn desired_image_count(mut self, count: u32) -> Self { - self.image_count = Some(count); + /// More images introduce more display lag, but smoother animation. + pub fn min_image_count(mut self, count: u32) -> Self { + self.min_image_count = Some(count); self } @@ -511,7 +569,7 @@ impl fmt::Debug for WindowBuilder { .field("attributes", &self.attributes) .field("cmd_buffer_count", &self.cmd_buf_count) .field("device_info", &self.device_info) - .field("image_count", &self.image_count) + .field("min_image_count", &self.min_image_count) .field( "surface_format_fn", &self.surface_format_fn.as_ref().map(|_| ()), @@ -528,7 +586,7 @@ impl Default for WindowBuilder { attributes: Default::default(), cmd_buf_count: 5, device_info: Default::default(), - image_count: None, + min_image_count: None, surface_format_fn: None, v_sync: None, window_mode_override: None, @@ -539,16 +597,21 @@ impl Default for WindowBuilder { struct WindowData { attributes: WindowAttributes, cmd_buf_count: usize, - image_count: Option, - surface_format_fn: Option vk::SurfaceFormatKHR>>, + min_image_count: Option, + surface_format_fn: Option>, v_sync: Option, window_mode_override: Option>, } +/// Errors produced while creating or running a [`Window`]. #[derive(Debug)] pub enum WindowError { + /// A Vulkan or `vk-graph` driver error occurred. Driver(DriverError), + /// `winit` failed to create or run the event loop. EventLoop(EventLoopError), + /// An window system integration or swapchain presentation error occurred. + Swapchain(SwapchainError), } impl error::Error for WindowError { @@ -556,6 +619,7 @@ impl error::Error for WindowError { Some(match self { Self::Driver(err) => err, Self::EventLoop(err) => err, + Self::Swapchain(err) => err, }) } } @@ -565,6 +629,7 @@ impl fmt::Display for WindowError { match self { Self::Driver(err) => err.fmt(f), Self::EventLoop(err) => err.fmt(f), + Self::Swapchain(err) => err.fmt(f), } } } diff --git a/crates/vk-graph-window/src/swapchain.rs b/crates/vk-graph-window/src/swapchain.rs new file mode 100644 index 00000000..46edcf96 --- /dev/null +++ b/crates/vk-graph-window/src/swapchain.rs @@ -0,0 +1,668 @@ +//! Window swapchain creation, acquisition, and presentation helpers. + +use { + derive_builder::{Builder, UninitializedFieldError}, + log::{trace, warn}, + std::{ + error::Error, + fmt::{Debug, Formatter}, + ops::Deref, + slice, + thread::panicking, + time::Instant, + }, + vk_graph::{ + Graph, + driver::{ + DriverError, + ash::{self, vk}, + cmd_buf::{CommandBuffer, CommandBufferInfo}, + descriptor_set::{DescriptorPool, DescriptorPoolInfo}, + device::Device, + image::Image, + render_pass::{RenderPass, RenderPassInfo}, + surface::Surface, + swapchain::{self, SwapchainImage}, + sync::{AccessType, ImageBarrier, ImageLayout, cmd::pipeline_barrier}, + }, + node::SwapchainImageNode, + pool::Pool, + }, +}; + +fn create_semaphore(device: &ash::Device) -> Result { + let create_info = vk::SemaphoreCreateInfo::default(); + let allocation_callbacks = None; + + unsafe { device.create_semaphore(&create_info, allocation_callbacks) }.map_err(|err| { + warn!("unable to create semaphore: {err}"); + + DriverError::OutOfMemory + }) +} + +const fn image_access_layout(access: AccessType) -> ImageLayout { + if matches!(access, AccessType::Present | AccessType::ComputeShaderWrite) { + ImageLayout::General + } else { + ImageLayout::Optimal + } +} + +/// A physical display interface. +#[read_only::embed] +pub struct Swapchain { + exec_idx: usize, + execs: Box<[Execution]>, + image_execs: Vec, + + /// Information used to create this resource. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub info: SwapchainInfo, + + /// The swapchain which supports this display. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub swapchain: swapchain::Swapchain, +} + +impl Deref for ReadOnlySwapchain { + type Target = swapchain::Swapchain; + + fn deref(&self) -> &Self::Target { + &self.swapchain + } +} + +impl Swapchain { + /// Constructs a new `Swapchain` object. + pub fn new(surface: Surface, info: impl Into) -> Result { + let info = info.into(); + + assert_ne!(info.command_buffer_count, 0); + + let swapchain_info: swapchain::SwapchainInfo = info.into(); + let swapchain = swapchain::Swapchain::new(surface, swapchain_info)?; + + let mut execs = Vec::with_capacity(info.command_buffer_count as _); + for _ in 0..info.command_buffer_count { + let cmd = CommandBuffer::create( + &swapchain.surface.device, + CommandBufferInfo::new(info.queue_family_index), + )?; + let swapchain_acquired = create_semaphore(&swapchain.surface.device)?; + let swapchain_rendered = create_semaphore(&swapchain.surface.device)?; + + execs.push(Execution { + cmd, + swapchain_acquired, + swapchain_rendered, + }); + } + let execs = execs.into_boxed_slice(); + + Ok(Self { + exec_idx: info.command_buffer_count, + execs, + image_execs: Default::default(), + read_only: ReadOnlySwapchain { info, swapchain }, + }) + } + + /// Acquires the next available swapchain image for rendering. + /// + /// This is a high-level wrapper around [`Swapchain::acquire_next_image`]. + /// It manages the internal execution slot, fence, and semaphore lifecycle automatically. + /// + /// The returned [`SwapchainImage`] (if `Some`) has already had its acquire semaphore + /// submitted to the associated queue for this frame. The caller should record and + /// submit rendering commands that depend on this image in the current frame's graph. + /// + /// # Returns + /// + /// * `Ok(Some(image))` — An image was acquired and may be rendered to. + /// * `Ok(None)` — The swapchain is suboptimal (e.g. the window was resized). Render nothing for + /// this frame and call [`acquire_next_image`][Self::acquire_next_image] again next frame. + /// * `Err(SwapchainError::DeviceLost)` — The Vulkan device has been lost. + /// * `Err(SwapchainError::Driver(_))` — The surface was lost or another unrecoverable driver + /// error occurred. + /// + /// # Internal behavior + /// + /// Each call advances to the next internal execution slot (round-robin across + /// a fixed number of submissions in flight). Before acquiring, the method waits + /// on the fence for the previous submission in that slot, ensuring at most one + /// in-flight submission per slot. It then calls the core + /// [`acquire_next_image`](vk_graph::driver::swapchain::Swapchain::acquire_next_image) + /// with an infinite timeout ([`u64::MAX`]) and the slot's acquire semaphore. + /// + /// After successful acquisition the slot index is recorded against the returned + /// image index so that future waits are correctly associated with the right + /// submission. + /// + /// # Errors + /// + /// Returns `Ok(None)` instead of surfacing a suboptimal-state error; the window-level + /// API collapses suboptimal into a non-error signal so the draw loop can skip + /// the frame gracefully. + pub fn acquire_next_image(&mut self) -> Result, SwapchainError> { + self.exec_idx += 1; + self.exec_idx %= self.execs.len(); + let exec = &mut self.execs[self.exec_idx]; + + exec.cmd.wait_until_executed().inspect_err(|err| { + warn!("unable to wait for swapchain fence: {err}"); + })?; + + Device::reset_fences(&exec.cmd.device, slice::from_ref(&exec.cmd.fence))?; + + let acquire_next_image = self + .read_only + .swapchain + .acquire_next_image(u64::MAX, exec.swapchain_acquired); + + if let Err(err) = acquire_next_image { + warn!("unable to acquire next swapchain image: {err:?}"); + } + + let swapchain_image = match acquire_next_image { + Err(swapchain::SwapchainError::DeviceLost) => Err(SwapchainError::DeviceLost), + Err(swapchain::SwapchainError::Suboptimal) => return Ok(None), + Err(swapchain::SwapchainError::SurfaceLost) => { + warn!("invalid swapchain surface state: surface lost"); + Err(SwapchainError::Driver(DriverError::InvalidData)) + } + Ok(swapchain_image) => Ok(swapchain_image), + }?; + + while swapchain_image.index >= self.image_execs.len() as u32 { + self.image_execs.push(0); + } + + self.image_execs[swapchain_image.index as usize] = self.exec_idx; + + Ok(Some(swapchain_image)) + } + + /// Displays the given swapchain image using passes specified in `graph`, if possible. + #[profiling::function] + pub fn present_image

( + &mut self, + pool: &mut P, + graph: Graph, + swapchain_image: SwapchainImageNode, + queue_index: u32, + ) -> Result<(), SwapchainError> + where + P: Pool + Pool, + { + trace!("present_image"); + + let mut submission = graph.into_submission(); + let wait_dst_stage_mask = submission.node_stages(swapchain_image); + + // The swapchain should have been written to, otherwise it would be noise and that's a panic + assert!( + !wait_dst_stage_mask.is_empty(), + "uninitialized swapchain image: write something each frame!", + ); + + let image_idx = submission.resource(swapchain_image).index; + let exec_idx = self.image_execs[image_idx as usize]; + let exec = &mut self.execs[exec_idx]; + + debug_assert!(!exec.cmd.has_executed().unwrap()); + + let started = Instant::now(); + + Device::begin_command_buffer( + &exec.cmd.device, + exec.cmd.handle, + &vk::CommandBufferBeginInfo::default() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT), + )?; + + // submission.record_node_dependencies(&mut *self.pool, cmd, swapchain_image)?; + submission.queue_resource(swapchain_image, pool, &mut exec.cmd)?; + + { + let swapchain_image = submission.resource(swapchain_image); + for (access, range) in Image::access( + swapchain_image, + AccessType::Present, + vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_array_layer: 0, + base_mip_level: 0, + layer_count: 1, + level_count: 1, + }, + ) { + trace!( + "image {:?} {:?}->{:?}", + swapchain_image, + access, + AccessType::Present, + ); + + // Force a presentation layout transition + pipeline_barrier( + &exec.cmd.device, + exec.cmd.handle, + None, + &[], + slice::from_ref(&ImageBarrier { + previous_accesses: slice::from_ref(&access), + previous_layout: image_access_layout(access), + next_accesses: slice::from_ref(&AccessType::Present), + next_layout: image_access_layout(AccessType::Present), + discard_contents: false, + src_queue_family_index: vk::QUEUE_FAMILY_IGNORED, + dst_queue_family_index: vk::QUEUE_FAMILY_IGNORED, + image: swapchain_image.handle, + range, + }), + ); + } + } + + // We may have unresolved nodes; things like copies that happen after present or operations + // before present which use nodes that are unused in the remainder of the graph. + // These operations are still important, but they don't need to wait for any of the above + // things so we do them last + submission.submit_cmd_buf(pool, &mut exec.cmd)?; + + Device::with_queue( + &exec.cmd.device, + self.read_only.info.queue_family_index, + queue_index, + |queue| { + Device::end_command_buffer(&exec.cmd.device, exec.cmd.handle)?; + Device::queue_submit( + &exec.cmd.device, + queue, + slice::from_ref( + &vk::SubmitInfo::default() + .command_buffers(slice::from_ref(&exec.cmd.handle)) + .wait_semaphores(slice::from_ref(&exec.swapchain_acquired)) + .wait_dst_stage_mask(slice::from_ref(&wait_dst_stage_mask)) + .signal_semaphores(slice::from_ref(&exec.swapchain_rendered)), + ), + exec.cmd.fence, + ) + }, + )?; + + let elapsed = Instant::now() - started; + trace!("🔜🔜🔜 vkQueueSubmit took {} μs", elapsed.as_micros(),); + + let swapchain_image = submission.resource(swapchain_image).clone(); + + self.read_only.swapchain.present_image( + swapchain_image, + slice::from_ref(&exec.swapchain_rendered), + self.read_only.info.queue_family_index, + queue_index, + )?; + + // Store the resolved graph because it contains bindings, pooled resources, and other shared + // resources that need to be kept alive until the fence is waited upon. + exec.cmd.drop_after_executed(submission); + + Ok(()) + } + + /// Updates the information which controls the swapchain. + /// + /// Previously acquired swapchain images should be discarded after calling this function. + pub fn set_info(&mut self, info: impl Into) { + let info = info.into(); + + self.read_only.swapchain.set_info(info); + self.read_only.info.height = info.height; + self.read_only.info.min_image_count = info.min_image_count; + self.read_only.info.present_mode = info.present_mode; + self.read_only.info.surface = info.surface; + self.read_only.info.width = info.width; + } +} + +impl Debug for Swapchain { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str("Swapchain") + } +} + +impl Drop for Swapchain { + fn drop(&mut self) { + if panicking() { + return; + } + + let idle = unsafe { self.execs[0].cmd.device.device_wait_idle() }; + if idle.is_err() { + warn!("unable to wait for device"); + + return; + } + + for batch in &mut self.execs { + // Wait for presentation to stop + let present = batch.cmd.wait_until_executed(); + if present.is_err() { + warn!("unable to wait for queue"); + + continue; + } + + unsafe { + batch + .cmd + .device + .destroy_semaphore(batch.swapchain_acquired, None); + batch + .cmd + .device + .destroy_semaphore(batch.swapchain_rendered, None); + } + } + } +} + +/// Describes error conditions relating to physical displays. +#[derive(Clone, Copy, Debug)] +pub enum SwapchainError { + /// Unrecoverable device error; must destroy this device and display and start a new one. + DeviceLost, + + /// Recoverable driver error. + Driver(DriverError), +} + +impl Error for SwapchainError {} + +impl From<()> for SwapchainError { + fn from(_: ()) -> Self { + Self::DeviceLost + } +} + +impl From for SwapchainError { + fn from(err: DriverError) -> Self { + Self::Driver(err) + } +} + +impl std::fmt::Display for SwapchainError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +/// Information used to create a [`Swapchain`] instance. +#[derive(Builder, Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[builder( + build_fn(private, name = "fallible_build", error = "SwapchainInfoBuilderError"), + derive(Clone, Copy, Debug), + pattern = "owned" +)] +pub struct SwapchainInfo { + /// The number of command buffers to use for image submissions. + /// + /// Generally one more than the swapchain image count is best. + #[builder(default = "4")] + pub command_buffer_count: usize, + + /// The initial height of the surface. + pub height: u32, + + /// The minimum number of presentable images that the application needs. The implementation + /// will either create the swapchain with at least that many images, or it will fail to + /// create the swapchain. + /// + /// More images introduce more display lag, but smoother animation. + #[builder(default = "2")] + pub min_image_count: u32, + + /// `vk::PresentModeKHR` determines timing and queueing with which frames are actually + /// displayed to the user. + /// + /// `vk::PresentModeKHR::FIFO` - Presentation frames are kept in a First-In-First-Out queue + /// approximately 3 frames long. Every vertical blanking period, the presentation engine + /// will pop a frame off the queue to display. If there is no frame to display, it will + /// present the same frame again until the next vblank. + /// + /// When a present command is executed on the GPU, the presented image is added on the queue. + /// + /// * **Tearing:** No tearing will be observed. + /// * **Also known as**: "Vsync On" + /// + /// `vk::PresentModeKHR::FIFO_RELAXED` - Presentation frames are kept in a First-In-First-Out + /// queue approximately 3 frames long. Every vertical blanking period, the presentation + /// engine will pop a frame off the queue to display. If there is no frame to display, it + /// will present the same frame until there is a frame in the queue. The moment there is a + /// frame in the queue, it will immediately pop the frame off the queue. + /// + /// When a present command is executed on the GPU, the presented image is added on the queue. + /// + /// * **Tearing**: Tearing will be observed if frames last more than one vblank as the front + /// buffer. + /// * **Also known as**: "Adaptive Vsync" + /// + /// `vk::PresentModeKHR::IMMEDIATE` - Presentation frames are not queued at all. The moment a + /// present command is executed on the GPU, the presented image is swapped onto the front + /// buffer immediately. + /// + /// * **Tearing**: Tearing can be observed. + /// * **Also known as**: "Vsync Off" + /// + /// `vk::PresentModeKHR::MAILBOX` - Presentation frames are kept in a single-frame queue. Every + /// vertical blanking period, the presentation engine will pop a frame from the queue. If + /// there is no frame to display, it will present the same frame again until the next + /// vblank. + /// + /// When a present command is executed on the GPU, the frame will be put into the queue. + /// If there was already a frame in the queue, the new frame will _replace_ the old frame. + /// on the queue. + /// + /// * **Tearing**: No tearing will be observed. + /// * **Also known as**: "Fast Vsync" + #[builder(default = vk::PresentModeKHR::MAILBOX)] + pub present_mode: vk::PresentModeKHR, + + /// The device queue family which will be used to submit and present images. + #[builder(default = "0")] + pub queue_family_index: u32, + + /// The format and color space of the surface. + pub surface: vk::SurfaceFormatKHR, + + /// The initial width of the surface. + pub width: u32, +} + +impl SwapchainInfo { + /// Specifies a default swapchain with the given `width`, `height` and `format` values. + #[inline(always)] + pub fn new(width: u32, height: u32, surface: vk::SurfaceFormatKHR) -> SwapchainInfo { + Self { + command_buffer_count: 4, + height, + min_image_count: 2, + present_mode: vk::PresentModeKHR::MAILBOX, + queue_family_index: 0, + surface, + width, + } + } + + /// Converts a `SwapchainInfo` into a `SwapchainInfoBuilder`. + pub fn into_builder(self) -> SwapchainInfoBuilder { + SwapchainInfoBuilder { + command_buffer_count: Some(self.command_buffer_count), + height: Some(self.height), + min_image_count: Some(self.min_image_count), + present_mode: Some(self.present_mode), + queue_family_index: Some(self.queue_family_index), + surface: Some(self.surface), + width: Some(self.width), + } + } + + #[deprecated = "use into_builder function"] + #[doc(hidden)] + pub fn to_builder(self) -> SwapchainInfoBuilder { + self.into_builder() + } +} + +impl From for SwapchainInfo { + fn from(info: SwapchainInfoBuilder) -> Self { + info.build() + } +} + +impl From for swapchain::SwapchainInfo { + fn from(val: SwapchainInfo) -> Self { + swapchain::SwapchainInfoBuilder::default() + .height(val.height) + .min_image_count(val.min_image_count) + .present_mode(val.present_mode) + .surface(val.surface) + .width(val.width) + .build() + } +} + +impl SwapchainInfoBuilder { + /// Builds a new `SwapchainInfo`. + /// + /// # Panics + /// + /// If any of the following values have not been set this function will panic. + /// + /// * `command_buffer_count` + /// * `width` + /// * `height` + /// * `surface` + #[inline(always)] + pub fn build(self) -> SwapchainInfo { + let info = match self.fallible_build() { + Err(SwapchainInfoBuilderError(err)) => panic!("{err}"), + Ok(info) => info, + }; + + assert_ne!( + info.command_buffer_count, 0, + "Field value invalid: command_buffer_count" + ); + + info + } +} + +#[derive(Debug)] +struct SwapchainInfoBuilderError(UninitializedFieldError); + +impl From for SwapchainInfoBuilderError { + fn from(err: UninitializedFieldError) -> Self { + Self(err) + } +} + +struct Execution { + cmd: CommandBuffer, + swapchain_acquired: vk::Semaphore, + swapchain_rendered: vk::Semaphore, +} + +#[cfg(test)] +mod test { + use super::*; + + type Info = SwapchainInfo; + type Builder = SwapchainInfoBuilder; + + #[test] + pub fn swapchain_info() { + let info = Info { + command_buffer_count: 42, + height: 123, + min_image_count: 99, + present_mode: vk::PresentModeKHR::IMMEDIATE, + queue_family_index: 16, + surface: vk::SurfaceFormatKHR::default() + .format(vk::Format::R8G8B8A8_UNORM) + .color_space(vk::ColorSpaceKHR::PASS_THROUGH_EXT), + width: 88, + }; + let builder = info.into_builder().build(); + + assert_eq!(info, builder); + } + + #[test] + pub fn swapchain_info_builder() { + let info = Info { + command_buffer_count: 42, + height: 123, + min_image_count: 99, + present_mode: vk::PresentModeKHR::IMMEDIATE, + queue_family_index: 16, + surface: vk::SurfaceFormatKHR::default() + .format(vk::Format::R8G8B8A8_UNORM) + .color_space(vk::ColorSpaceKHR::PASS_THROUGH_EXT), + width: 88, + }; + let builder = Builder::default() + .command_buffer_count(42) + .height(123) + .min_image_count(99) + .present_mode(vk::PresentModeKHR::IMMEDIATE) + .queue_family_index(16) + .surface( + vk::SurfaceFormatKHR::default() + .format(vk::Format::R8G8B8A8_UNORM) + .color_space(vk::ColorSpaceKHR::PASS_THROUGH_EXT), + ) + .width(88) + .build(); + + assert_eq!(info, builder); + } + + #[test] + #[should_panic(expected = "Field value invalid: command_buffer_count")] + pub fn swapchain_info_builder_uninit_command_buffer_count() { + Builder::default() + .height(1) + .surface(vk::SurfaceFormatKHR::default()) + .width(1) + .command_buffer_count(0) + .build(); + } + + #[test] + #[should_panic(expected = "Field not initialized: height")] + pub fn swapchain_info_builder_uninit_height() { + Builder::default().build(); + } + + #[test] + #[should_panic(expected = "Field not initialized: surface")] + pub fn swapchain_info_builder_uninit_surface() { + Builder::default().height(1).build(); + } + + #[test] + #[should_panic(expected = "Field not initialized: width")] + pub fn swapchain_info_builder_uninit_width() { + Builder::default() + .height(1) + .surface(vk::SurfaceFormatKHR::default()) + .build(); + } +} diff --git a/examples/README.md b/examples/README.md index a0c5d97e..e115e3c5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,8 +1,8 @@ -# _Screen 13_ Example Code +# _vk-graph_ Example Code ## Getting Started -A helpful [getting started](getting-started.md) guide is available which describes basic _Screen 13_ +A helpful [guide](https://attackgoat.github.io/vk-graph) is available which describes _vk-graph_ types and functions. See the [README](../README.md) for more information. @@ -17,7 +17,7 @@ Example | Instructions | Preview [min_max.rs](min_max.rs) |

cargo run --example min_max
| _See console output_ [mip_compute.rs](mip_compute.rs) |
cargo run --example mip_compute
| _See console output_ [subgroup_ops.rs](subgroup_ops.rs) |
cargo run --example subgroup_ops
| _See console output_ -[hello_world.rs](../contrib/screen-13-window/examples/hello_world.rs) | _See [screen-13-window](../contrib/screen-13-window/README.md)_ | hello_world.rs +[hello_world.rs](../crates/vk-graph-window/examples/hello_world.rs) | _See [vk-graph-window](../crates/vk-graph-window/README.md)_ | hello_world.rs [app.rs](app.rs) |
cargo run --example app
| app.rs [triangle.rs](triangle.rs) |
cargo run --example triangle
| triangle.rs [vertex_layout.rs](vertex_layout.rs) |
cargo run --example vertex_layout
| vertex_layout.rs @@ -35,16 +35,16 @@ Example | Instructions | Preview [vsm_omni.rs](vsm_omni.rs) |
cargo run --example vsm_omni
Variance shadow mapping for omni/point lights | vsm_omni.rs [ray_omni.rs](ray_omni.rs) |
cargo run --example ray_omni
Ray query for omni/point lights | ray_omni.rs [transitions.rs](transitions.rs) |
cargo run --example transitions
| transitions.rs -[skeletal-anim/](skeletal-anim/src/main.rs) |
cargo run --manifest-path examples/skeletal-anim/Cargo.toml
Skeletal mesh animation using GLTF | skeletal-anim -[shader-toy/](shader-toy/src/main.rs) |
cargo run --manifest-path examples/shader-toy/Cargo.toml
| shader-toy -[vr/](vr/src/main.rs) |
cargo run --manifest-path examples/vr/Cargo.toml
| vr +[skeletal-anim/](skeletal-anim/src/main.rs) |
cargo run -p skeletal-anim
Skeletal mesh animation using GLTF | skeletal-anim +[shader-toy/](shader-toy/src/main.rs) |
cargo run -p shader-toy
| shader-toy +[vr/](vr/src/main.rs) |
cargo run -p vr
| vr ## Additional Examples The following packages offer examples for specific cases not listed here: -- [contrib/screen-13-hot](../contrib/screen-13-hot/examples/README.md): Shader pipeline hot-reload +- [crates/vk-graph-hot](../crates/vk-graph-hot/examples/README.md): Shader pipeline hot-reload - [attackgoat/mood](https://github.com/attackgoat/mood): FPS game prototype with level loading and multiple rendering backends - [attackgoat/jw-basic](https://github.com/attackgoat/jw-basic): BASIC interpreter with graphics - commands powered by _Screen 13_ + commands powered by _vk-graph_ diff --git a/examples/aliasing.rs b/examples/aliasing.rs index 26ba27fb..e2ccf17e 100644 --- a/examples/aliasing.rs +++ b/examples/aliasing.rs @@ -1,34 +1,40 @@ use { + ash::vk, clap::Parser, - screen_13::{ - pool::alias::{Alias, AliasPool}, - prelude::*, - }, std::sync::Arc, + vk_graph::{ + Graph, + driver::{ + DriverError, + device::{Device, DeviceInfo}, + image::ImageInfo, + }, + pool::{cache::Cache, hash::HashPool}, + }, }; /// This example demonstrates resource aliasing. Aliasing is a memory-efficiency optimization that -/// may be used anywhere resources are leased and used in a render graph. Aliasing allows complex +/// may be used anywhere resources are requested and used in a graph. Aliasing allows complex /// graphs to require fewer individual resources. /// /// The performance overhead of aliasing is an atomic load for each actively aliased item and one /// check per active alias to see if it is compatible with the requested resource. /// /// Acceleration structures, buffers and images may be "aliased" by different parts of any one or -/// more render graphs. The process involves wrapping any pool type (FifoPool, LazyPool, HashPool) -/// in an AliasPool container. AliasPool offers an alias(..) function which operates exactly the -/// same as a regular pool lease(..) except that the result is wrapped in an Arc<>. +/// more graphs. The process involves wrapping any pool type (FifoPool, LazyPool, HashPool) +/// in a Cache container. Cache tags let you create independent aliasing groups, and each tagged +/// view offers `resource(...)` for aliasing requests that return an `Arc<>`. /// -/// AliasPool derefs to the base pool type and so leasing may be used normally too. +/// Cache derefs to the base pool type so pooled resources may be requested normally too. fn main() -> Result<(), DriverError> { pretty_env_logger::init(); let args = Args::parse(); - let device_info = DeviceInfoBuilder::default().debug(args.debug); - let device = Arc::new(Device::create_headless(device_info)?); + let device_info = DeviceInfo::builder().debug(args.debug); + let device = Device::create(device_info)?; - // We wrap HashPool in an AliasPool container to enable resource aliasing - let mut pool = AliasPool::new(HashPool::new(&device)); + // We wrap HashPool in a Cache container to enable resource aliasing + let mut cache = Cache::new(HashPool::new(&device)); // This is the information we will use to alias image1 and image2 let image_info = ImageInfo::image_2d( @@ -38,17 +44,32 @@ fn main() -> Result<(), DriverError> { vk::ImageUsageFlags::TRANSFER_SRC | vk::ImageUsageFlags::TRANSFER_DST, ); + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + enum CacheTag { + Shadow, + Ui, + } + // Any two compatible images aliased from the same pool will be the same physical image - let image1 = pool.alias(image_info)?; - let image2 = pool.alias(image_info)?; + let image1 = cache.tag(CacheTag::Shadow).resource(image_info)?; + let image2 = cache.tag(CacheTag::Shadow).resource(image_info)?; assert!(Arc::ptr_eq(&image1, &image2)); - let mut render_graph = RenderGraph::new(); + let mut graph = Graph::default(); + + // Binding these images to any single graph will produce the same physical nodes + let image1_node = graph.bind_resource(&image1); + let image2_node = graph.bind_resource(&image2); + assert_eq!(image1_node, image2_node); + + // Even if re-bound + assert_eq!(image2_node, graph.bind_resource(&image2)); - // Binding these images to any render graph will produce the same physical nodes - let image1 = render_graph.bind_node(image1); - let image2 = render_graph.bind_node(image2); - assert_eq!(image1, image2); + // To be clear: other graphs will produce different nodes + // but they *may* be equal because they're just usizes + if image2_node == Graph::default().bind_resource(&image2) { + log::debug!("Nodes are just numbers, man") + } // Let's make up some different, yet compatible, image information: let image_info = ImageInfo::image_2d( @@ -59,12 +80,12 @@ fn main() -> Result<(), DriverError> { ); // We alias the compatible information and still produce the same physical image and node - let image3 = render_graph.bind_node(pool.alias(image_info)?); - assert_eq!(image1, image3); + let image3_node = graph.bind_resource(cache.tag(CacheTag::Shadow).resource(image_info)?); + assert_eq!(image1_node, image3_node); - // Using the same information for a new LEASE will generate an entirely different image!! - let image4 = render_graph.bind_node(pool.lease(image_info)?); - assert_ne!(image1, image4); + // Using a different tag for the same request produces an entirely different image. + let image4_node = graph.bind_resource(cache.tag(CacheTag::Ui).resource(image_info)?); + assert_ne!(image1_node, image4_node); Ok(()) } diff --git a/examples/app.rs b/examples/app.rs index 653695a4..48947008 100644 --- a/examples/app.rs +++ b/examples/app.rs @@ -3,17 +3,15 @@ mod profile_with_puffin; use { clap::Parser, log::error, - screen_13::{ - Display, DisplayError, DisplayInfo, + vk_graph::{ + Graph, driver::{ device::{Device, DeviceInfoBuilder}, surface::Surface, - swapchain::{Swapchain, SwapchainInfo}, }, - graph::RenderGraph, pool::hash::HashPool, }, - std::sync::Arc, + vk_graph_window::swapchain::{Swapchain, SwapchainError, SwapchainInfo}, winit::{ application::ApplicationHandler, error::EventLoopError, @@ -39,30 +37,35 @@ impl ApplicationHandler for Application { } fn resumed(&mut self, event_loop: &ActiveEventLoop) { - let window_attributes = Window::default_attributes().with_title("Screen 13"); + let window_attributes = Window::default_attributes().with_title("vk-graph"); let window = event_loop.create_window(window_attributes).unwrap(); let args = Args::parse(); let device_info = DeviceInfoBuilder::default().debug(args.debug); - let device = Arc::new(Device::create_display(device_info, &window).unwrap()); + let device = Device::try_from_display(&window, device_info).unwrap(); - let surface = Surface::create(&device, &window).unwrap(); + let surface = Surface::create(&device, &window, &window).unwrap(); let surface_formats = Surface::formats(&surface).unwrap(); let surface_format = Surface::linear_or_default(&surface_formats); let window_size = window.inner_size(); + let present_mode = Surface::present_modes(&surface) + .unwrap() + .first() + .copied() + .unwrap(); let swapchain = Swapchain::new( - &device, surface, - SwapchainInfo::new(window_size.width, window_size.height, surface_format), + SwapchainInfo::new(window_size.width, window_size.height, surface_format) + .into_builder() + .present_mode(present_mode), ) .unwrap(); - let display_pool = HashPool::new(&device); - let display = Display::new(&device, swapchain, DisplayInfo::default()).unwrap(); + let swapchain_pool = HashPool::new(&device); self.0 = Some(Context { - display, - display_pool, + swapchain, + swapchain_pool, window, }); } @@ -78,10 +81,10 @@ impl ApplicationHandler for Application { match event { WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::Resized(size) => { - let mut swapchain_info = context.display.swapchain_info(); + let mut swapchain_info = context.swapchain.info; swapchain_info.width = size.width; swapchain_info.height = size.height; - context.display.set_swapchain_info(swapchain_info); + context.swapchain.set_info(swapchain_info); } WindowEvent::RedrawRequested => { if let Err(err) = context.draw() { @@ -106,23 +109,23 @@ struct Args { } struct Context { - display: Display, - display_pool: HashPool, + swapchain: Swapchain, + swapchain_pool: HashPool, window: Window, } impl Context { - fn draw(&mut self) -> Result<(), DisplayError> { - if let Some(swapchain_image) = self.display.acquire_next_image()? { - let mut render_graph = RenderGraph::new(); - let swapchain_image = render_graph.bind_node(swapchain_image); + fn draw(&mut self) -> Result<(), SwapchainError> { + if let Some(swapchain_image) = self.swapchain.acquire_next_image()? { + let mut graph = Graph::default(); + let swapchain_image = graph.bind_resource(swapchain_image); // Rendering goes here! - render_graph.clear_color_image_value(swapchain_image, [1.0, 0.0, 1.0]); + graph.clear_color_image(swapchain_image, [1.0, 0.0, 1.0, 1.0]); self.window.pre_present_notify(); - self.display - .present_image(&mut self.display_pool, render_graph, swapchain_image, 0)?; + self.swapchain + .present_image(&mut self.swapchain_pool, graph, swapchain_image, 0)?; } Ok(()) diff --git a/examples/bindless.rs b/examples/bindless.rs index 9612e93f..a91654a5 100644 --- a/examples/bindless.rs +++ b/examples/bindless.rs @@ -1,12 +1,25 @@ mod profile_with_puffin; use { + ash::vk, bytemuck::{Pod, Zeroable, cast_slice}, clap::Parser, - inline_spirv::inline_spirv, - screen_13::prelude::*, - screen_13_window::{WindowBuilder, WindowError}, std::sync::Arc, + vk_graph::{ + Graph, + cmd::{LoadOp, StoreOp}, + driver::{ + DriverError, + buffer::Buffer, + device::Device, + graphic::{GraphicPipeline, GraphicPipelineInfo}, + image::{Image, ImageInfo}, + }, + pool::lazy::LazyPool, + }, + vk_graph_window::{Window, WindowError}, + vk_shader_macros::glsl, + vk_sync::AccessType, winit::dpi::LogicalSize, }; @@ -15,7 +28,7 @@ fn main() -> Result<(), WindowError> { profile_with_puffin::init(); let args = Args::parse(); - let window = WindowBuilder::default() + let window = Window::builder() .debug(args.debug) .window(|window| window.with_inner_size(LogicalSize::new(512, 512))) .build()?; @@ -24,35 +37,44 @@ fn main() -> Result<(), WindowError> { let draw_buf = create_indirect_buffer(&window.device)?; window.run(|frame| { - let draw_buf_node = frame.render_graph.bind_node(&draw_buf); + let draw_buf_node = frame.graph.bind_resource(&draw_buf); - let mut pass = frame - .render_graph - .begin_pass("Test") + let mut cmd = frame + .graph + .begin_cmd() + .debug_name("Test") .bind_pipeline(&pipeline) - .access_node(draw_buf_node, AccessType::IndirectBuffer); + .resource_access(draw_buf_node, AccessType::IndirectBuffer); for (idx, image) in images.iter().enumerate() { - let image_node = pass.bind_node(image); - pass = pass.read_descriptor((0, [idx as u32]), image_node); + let image = cmd.bind_resource(image); + cmd.set_shader_resource_access( + (0, [idx as u32]), + image, + AccessType::FragmentShaderReadSampledImageOrUniformTexelBuffer, + ); } - pass.clear_color(0, frame.swapchain_image) - .store_color(0, frame.swapchain_image) - .record_subpass(move |subpass, _| { - subpass.draw_indirect(draw_buf_node, 0, 64, 16); - }); + cmd.color_attachment_image( + 0, + frame.swapchain_image, + LoadOp::CLEAR_BLACK_ALPHA_ZERO, + StoreOp::Store, + ) + .record_cmd(move |cmd| { + cmd.draw_indirect(draw_buf_node, 0, 64, 16); + }); }) } -fn create_images(device: &Arc) -> Result>, DriverError> { +fn create_images(device: &Device) -> Result>, DriverError> { let mut textures = Vec::with_capacity(64); let (b, a) = (0.0, 1.0); - let mut graph = RenderGraph::new(); + let mut graph = Graph::default(); for y in 0..8 { for x in 0..8 { - let texture = Arc::new(Image::create( + let texture = graph.bind_resource(Image::create( device, ImageInfo::image_2d( 100, @@ -61,22 +83,21 @@ fn create_images(device: &Arc) -> Result>, DriverError> { vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST, ), )?); - let texture_node = graph.bind_node(&texture); let r = y as f32 / 7.0; let g = x as f32 / 7.0; - graph.clear_color_image_value(texture_node, [r, g, b, a]); - textures.push(texture); + graph.clear_color_image(texture, [r, g, b, a]); + textures.push(graph.resource(texture).clone()); } } let mut pool = LazyPool::new(device); - graph.resolve().submit(&mut pool, 0, 0)?; + graph.into_submission().queue_submit(&mut pool, 0, 0)?; Ok(textures) } -fn create_indirect_buffer(device: &Arc) -> Result, DriverError> { +fn create_indirect_buffer(device: &Device) -> Result, DriverError> { let mut draw_cmds = Vec::with_capacity(64); for first_instance in 0..64 { draw_cmds.push(DrawIndirectCommand { @@ -94,64 +115,60 @@ fn create_indirect_buffer(device: &Arc) -> Result, DriverErr Ok(draw_buf) } -fn create_graphic_pipeline(device: &Arc) -> Result, DriverError> { - Ok(Arc::new(GraphicPipeline::create( +fn create_graphic_pipeline(device: &Device) -> Result { + GraphicPipeline::create( device, GraphicPipelineInfo::default(), [ - Shader::new_vertex( - inline_spirv!( - r#" - #version 460 core - - const vec2 QUAD[] = { - vec2(0, 0), - vec2(0, 1), - vec2(1, 1), - vec2(0, 0), - vec2(1, 1), - vec2(1, 0), - }; - - layout(location = 0) out uint instance_index_out; - - void main() { - uint x = gl_InstanceIndex % 8; - uint y = gl_InstanceIndex / 8; - - vec2 scale = vec2(1.0 / 8.0); - vec2 offset = vec2((float(x) - 4.0) * scale.x, (float(y) - 4.0) * scale.y); - - gl_Position = vec4(QUAD[gl_VertexIndex] * scale + offset, 0, 1); - instance_index_out = gl_InstanceIndex; - } - "#, - vert - ) - .as_slice(), - ), - Shader::new_fragment( - inline_spirv!( - r#" - #version 460 core - #extension GL_EXT_nonuniform_qualifier : require - - layout(set = 0, binding = 0) uniform sampler2D sampler_nnr[]; - - layout(location = 0) in flat uint instance_index; - - layout(location = 0) out vec4 color_out; - - void main() { - color_out = texture(sampler_nnr[nonuniformEXT(instance_index)], vec2(0.5, 0.5)); - } - "#, - frag - ) - .as_slice(), - ), + glsl!( + r#" + #version 460 core + #pragma shader_stage(vertex) + + const vec2 QUAD[] = { + vec2(0, 0), + vec2(0, 1), + vec2(1, 1), + vec2(0, 0), + vec2(1, 1), + vec2(1, 0), + }; + + layout(location = 0) out uint instance_index_out; + + void main() { + uint x = gl_InstanceIndex % 8; + uint y = gl_InstanceIndex / 8; + + vec2 scale = vec2(1.0 / 8.0); + vec2 offset = vec2((float(x) - 4.0) * scale.x, (float(y) - 4.0) * scale.y); + + gl_Position = vec4(QUAD[gl_VertexIndex] * scale + offset, 0, 1); + instance_index_out = gl_InstanceIndex; + } + "# + ) + .as_slice(), + glsl!( + r#" + #version 460 core + #extension GL_EXT_nonuniform_qualifier : require + #pragma shader_stage(fragment) + + layout(set = 0, binding = 0) uniform sampler2D sampler_nnr[]; + + layout(location = 0) in flat uint instance_index; + + layout(location = 0) out vec4 color_out; + + void main() { + color_out = texture(sampler_nnr[nonuniformEXT(instance_index)], vec2(0.5, 0.5)); + } + "# + ) + .as_slice(), ], - )?)) + ) } #[derive(Parser)] diff --git a/examples/cpu_readback.rs b/examples/cpu_readback.rs index 09ec55cc..06bf8d77 100644 --- a/examples/cpu_readback.rs +++ b/examples/cpu_readback.rs @@ -1,7 +1,16 @@ use { + ash::vk, clap::Parser, - screen_13::prelude::*, - std::{sync::Arc, time::Instant}, + std::time::Instant, + vk_graph::{ + Graph, + driver::{ + DriverError, + buffer::Buffer, + device::{Device, DeviceInfo}, + }, + pool::hash::HashPool, + }, }; /// Example demonstrating the steps to take when reading the results of buffer or image operations @@ -11,45 +20,45 @@ fn main() -> Result<(), DriverError> { // For this example we create a headless device, but the same thing works using a window let args = Args::parse(); - let device_info = DeviceInfoBuilder::default().debug(args.debug); - let device = Arc::new(Device::create_headless(device_info)?); + let device_info = DeviceInfo::builder().debug(args.debug); + let device = Device::create(device_info)?; - let mut render_graph = RenderGraph::new(); + let mut graph = Graph::default(); - let src_buf = render_graph.bind_node(Buffer::create_from_slice( + let src_buf = graph.bind_resource(Buffer::create_from_slice( &device, vk::BufferUsageFlags::TRANSFER_SRC, - [1, 2, 3, 4], + &[1, 2, 3, 4], )?); - let dst_buf = render_graph.bind_node(Buffer::create_from_slice( + let dst_buf = graph.bind_resource(Buffer::create_from_slice( &device, vk::BufferUsageFlags::TRANSFER_DST, - [0, 0, 0, 0], + &[0, 0, 0, 0], )?); // We are using the GPU to copy data, but the same thing works if you're executing a pipeline // such as a ComputePipeline to run some shader code which writes to a buffer or image. It is // important to note that dst_buf does not contain the new data until we submit this render // graph and wait on the result - render_graph.copy_buffer(src_buf, dst_buf); + graph.copy_buffer(src_buf, dst_buf); - // This line is optional - just bind a reference of Arc or a leased buffer so you retain - // the actual buffer for later use and you could then remove this unbind_node line - let dst_buf = render_graph.unbind_node(dst_buf); + // This line is optional - just bind a borrow of Arc or a leased buffer so you retain + // the actual buffer for later use and you could then remove this line + let dst_buf = graph.resource(dst_buf).clone(); // Resolve and wait (or you can check has_executed without blocking) - alternatively you might // use device.queue_wait_idle(0) or device.device_wait_idle() - but those block on larger scopes - let mut cmd_buf = render_graph - .resolve() - .submit(&mut HashPool::new(&device), 0, 0)?; + let mut cmd = graph + .into_submission() + .queue_submit(&mut HashPool::new(&device), 0, 0)?; - println!("Has executed? {}", cmd_buf.has_executed()?); + println!("Has executed? {}", cmd.has_executed()?); let started = Instant::now(); - cmd_buf.wait_until_executed()?; + cmd.wait_until_executed()?; assert!( - cmd_buf.has_executed()?, + cmd.has_executed()?, "We checked above - so this will always be true" ); println!("Waited {}μs", (Instant::now() - started).as_micros()); diff --git a/examples/debugger.rs b/examples/debugger.rs index e319332a..3d4334cb 100644 --- a/examples/debugger.rs +++ b/examples/debugger.rs @@ -1,3 +1,11 @@ +use ash::vk; +use vk_graph::driver::{ + compute::{ComputePipeline, ComputePipelineInfo}, + image::{Image, ImageInfo}, + shader::Shader, +}; +use vk_sync::AccessType; + /* This example details some common debugging techniques you might find helpful when something goes wrong. @@ -5,7 +13,7 @@ I hope you enjoy this choose-your-own-debugger adventure! First you will want to read this: - https://github.com/attackgoat/screen-13/blob/master/examples/getting-started.md + https://github.com/attackgoat/vk-graph/blob/master/examples/getting-started.md Enter your "name" to begin: cargo run --example debugger @@ -23,8 +31,8 @@ To continue, uncomment line 30. */ -fn main() -> Result<(), screen_13_window::WindowError> { - use {log::debug, screen_13::prelude::*, screen_13_window::Window, std::sync::Arc}; +fn main() -> Result<(), vk_graph_window::WindowError> { + use {log::debug, vk_graph_window::Window}; // 👋, 🌎! //pretty_env_logger::init(); @@ -44,7 +52,7 @@ fn main() -> Result<(), screen_13_window::WindowError> { Note: This callback runs each time the operating system requests a new window image and it expects you to render something to `frame.swapchain_image` using - `frame.render_graph`. Note that this scope is infalliable. You may create additional + `frame.graph`. Note that this scope is infalliable. You may create additional images and graphs if you choose. You can resolve multiple render graphs per frame - but you only need to do that if you have a hot-section that is part of a VERY large graph. @@ -52,7 +60,7 @@ fn main() -> Result<(), screen_13_window::WindowError> { When something goes wrong, it is probably *not* during this frame closure. The reason is that during this scope nearly everything is deferred until frame resolution where we try to schedule the work and get it displayed on the screen. - Typically, as here, we let Screen 13 handle all graph resolution (no code or + Typically, as here, we let vk-graph handle all graph resolution (no code or concerns here) - but it is valid to control the process manually, see the available functions in the API docs. @@ -80,7 +88,7 @@ fn main() -> Result<(), screen_13_window::WindowError> { } - Run `cargo run --example debugger` - You should see the PID in the console output - - Enter the VS Code Debugger; click `[>] Attach (screen-13)` + - Enter the VS Code Debugger; click `[>] Attach (vk-graph)` - Enter the PID - In the call stack pane, select the first thread; pause it - You are now parked on a syscall @@ -100,7 +108,7 @@ fn main() -> Result<(), screen_13_window::WindowError> { It is left as an excerise to the reader to determine *what* might have gone wrong here. */ #[allow(unused_variables)] - let image = frame.render_graph.bind_node( + let image = frame.graph.bind_resource( Image::create( frame.device, ImageInfo::image_2d( @@ -112,7 +120,7 @@ fn main() -> Result<(), screen_13_window::WindowError> { ) .unwrap(), ); - let image = frame.render_graph.bind_node( + let image = frame.graph.bind_resource( Image::create( frame.device, ImageInfo::image_2d( @@ -126,32 +134,31 @@ fn main() -> Result<(), screen_13_window::WindowError> { ); // Note: This is just for example - let compute_pipeline = Arc::new( - ComputePipeline::create( - frame.device, - ComputePipelineInfo::default(), - Shader::new_compute( - inline_spirv::inline_spirv!( - r#" - #version 460 core - - layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; - - layout(set = 0, binding = 42, rgba8) restrict readonly uniform image2D an_image; - - void main() {/* TODO: 📈...💰! */} - "#, - comp - ) - .as_slice(), - ), - ) - .unwrap(), - ); + let compute_pipeline = ComputePipeline::create( + frame.device, + ComputePipelineInfo::default(), + Shader::new_compute( + vk_shader_macros::glsl!( + r#" + #version 460 core + #pragma shader_stage(compute) + + layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; + + layout(set = 0, binding = 42, rgba8) + restrict readonly uniform image2D an_image; + + void main() {/* TODO: 📈...💰! */} + "# + ) + .as_slice(), + ), + ) + .unwrap(); /* Case #2: - We are about to record a compute pass which causes Screen 13 to panic + We are about to record a compute pass which causes vk-graph to panic Note: You'll see a panic here: thread 'main' panicked at 'uninitialized swapchain image ...' @@ -177,12 +184,13 @@ fn main() -> Result<(), screen_13_window::WindowError> { .write_descriptor(42, frame.swapchain_image) */ frame - .render_graph - .begin_pass("This doesn't look good...") + .graph + .begin_cmd() + .debug_name("This doesn't look good...") .bind_pipeline(&compute_pipeline) - .write_descriptor(42, image) - .record_compute(|compute, _| { - compute.dispatch(1024, 1024, 1); + .shader_resource_access(42, image, AccessType::ComputeShaderWrite) + .record_cmd(|cmd| { + cmd.dispatch(1024, 1024, 1); }); // Growing tired of your advenutes, you signal that it is time to close the window and exit @@ -205,7 +213,7 @@ fn main() -> Result<(), screen_13_window::WindowError> { Where to next? Fire up RenderDoc, capture a frame and have fun! But beware - RenderDoc does a replay of the capture it created; and it resubmits things ever so slightly differently at times - you most likely will NOT see any synchronization issues in RenderDoc if you DO see - them in Screen 13. + them in vk-graph. If you ever get stuck, switch between `vkconfig` settings of API dump and synchronization; those usually say exactly what is going wrong, and usually you need to use multiple layers diff --git a/examples/egui.rs b/examples/egui.rs index 2af1da8d..ab35502d 100644 --- a/examples/egui.rs +++ b/examples/egui.rs @@ -1,8 +1,15 @@ mod profile_with_puffin; use { - clap::Parser, screen_13::prelude::*, screen_13_egui::prelude::*, - screen_13_window::WindowBuilder, winit::dpi::LogicalSize, + ash::vk, + clap::Parser, + vk_graph::{ + driver::image::ImageInfo, + pool::{Pool as _, lazy::LazyPool}, + }, + vk_graph_egui::{Egui, egui}, + vk_graph_window::Window, + winit::dpi::LogicalSize, }; fn main() -> anyhow::Result<()> { @@ -10,19 +17,19 @@ fn main() -> anyhow::Result<()> { profile_with_puffin::init(); let args = Args::parse(); - let window = WindowBuilder::default() + let window = Window::builder() .debug(args.debug) .v_sync(false) .window(|window| window.with_inner_size(LogicalSize::new(1024, 768))) .build()?; - let mut egui = Egui::new(&window.device, window.as_ref()); + let mut egui = Egui::new(&window.device, &window); let mut cache = LazyPool::new(&window.device); window.run(|frame| { - let img = frame.render_graph.bind_node( + let img = frame.graph.bind_resource( cache - .lease(ImageInfo::image_2d( + .resource(ImageInfo::image_2d( 100, 100, vk::Format::R8G8B8A8_UNORM, @@ -30,12 +37,10 @@ fn main() -> anyhow::Result<()> { )) .unwrap(), ); + frame.graph.clear_color_image(img, [0., 1., 0., 1.]); frame - .render_graph - .clear_color_image_value(img, [0., 1., 0., 1.]); - frame - .render_graph - .clear_color_image_value(frame.swapchain_image, [0., 0., 0., 1.]); + .graph + .clear_color_image(frame.swapchain_image, [0., 0., 0., 1.]); let id = egui.register_texture(img); @@ -43,7 +48,7 @@ fn main() -> anyhow::Result<()> { frame.window, frame.events, frame.swapchain_image, - frame.render_graph, + frame.graph, |ui| { egui::Window::new("Test") .resizable(true) diff --git a/examples/font_bmp.rs b/examples/font_bmp.rs index b5be0d5e..12add40c 100644 --- a/examples/font_bmp.rs +++ b/examples/font_bmp.rs @@ -1,23 +1,33 @@ mod profile_with_puffin; use { + ash::vk, bmfont::{BMFont, OrdinateOrientation}, clap::Parser, image::ImageReader, - inline_spirv::inline_spirv, - screen_13::prelude::*, - screen_13_fx::*, - screen_13_window::WindowBuilder, - std::{io::Cursor, sync::Arc, time::Instant}, + std::{io::Cursor, time::Instant}, + vk_graph::{ + driver::{ + compute::{ComputePipeline, ComputePipelineInfo}, + image::ImageInfo, + physical_device::Vulkan11Properties, + shader::{Shader, SpecializationMap}, + }, + pool::{Pool as _, hash::HashPool}, + }, + vk_graph_fx::*, + vk_graph_window::Window, + vk_shader_macros::glsl, + vk_sync::AccessType, }; fn main() -> anyhow::Result<()> { pretty_env_logger::init(); profile_with_puffin::init(); - // Standard Screen 13 stuff + // Standard vk-graph stuff let args = Args::parse(); - let window = WindowBuilder::default().debug(args.debug).build()?; + let window = Window::builder().debug(args.debug).build()?; let display = GraphicPresenter::new(&window.device)?; let mut image_loader = ImageLoader::new(&window.device)?; let mut pool = HashPool::new(&window.device); @@ -48,81 +58,79 @@ fn main() -> anyhow::Result<()> { // A neato smoke effect just for fun let Vulkan11Properties { subgroup_size, .. } = window.device.physical_device.properties_v1_1; let start_time = Instant::now(); - let smoke_pipeline = Arc::new(ComputePipeline::create(&window.device, + let smoke_pipeline = ComputePipeline::create( + &window.device, ComputePipelineInfo::default(), Shader::new_compute( - inline_spirv!( - r#" - // Derived from https://www.shadertoy.com/view/Xl2XWz - #version 460 core - - layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; - - layout(push_constant) uniform PushConstants { - layout(offset = 0) float time; - } push_const; - - layout(set = 0, binding = 0, rgba32f) restrict writeonly uniform image2D image; - - float smoothNoise(vec2 p) { - vec2 i = floor(p); - p -= i; - p *= p * (3 - p - p); - - return dot( - mat2(fract(sin(vec4(0, 1, 27, 28) + i.x + i.y * 27) * 1e5)) * vec2(1 - p.y, p.y), - vec2(1 - p.x, p.x) - ); - } - - float fractalNoise(vec2 p) { - return smoothNoise(p) * 0.57 + smoothNoise(p * 2.45) * 0.28 + smoothNoise(p * 6) * 0.15; - } - - float warpedNoise(vec2 p) { - vec2 m = vec2(sin(push_const.time * 0.5), cos(push_const.time * 0.5)); - float x = fractalNoise(p + m); - float y = fractalNoise(p + m.yx + x); - float z = fractalNoise(p - m - x + y); - vec3 w = vec3(x, y, z); - - return fractalNoise(p + w.xy + w.yz + w.zx + length(w) * 0.25); - } - - void main() { - vec2 uv = vec2(gl_GlobalInvocationID.xy) / imageSize(image).y; - float n1 = warpedNoise(uv * 5); - float n2 = warpedNoise(uv * 5 + 0.04); - float bump1 = max(n2 - n1, 0.0) / 0.02 * 0.7071; - float bump2 = max(n1 - n2, 0.1) / 0.04 * 0.7071; - bump1 = bump1 * bump1 * 0.5 + pow(bump1, 4) * 0.5; - bump2 = bump2 * bump2 * 0.5 + pow(bump2, 4) * 0.5; - vec3 col = vec3(n1 * n1 * 0.7, n1, n1 * n1 * 0.4) - * n1 * n1 - * (vec3(0.25, 0.5, 1) - * bump1 * 0.2 - + vec3(1) * bump2 * 0.2 + 0.75); - vec4 fragColor = vec4(sqrt(max(col, 0.)), 1); - - imageStore(image, ivec2(gl_GlobalInvocationID.xy), fragColor); - } - "#, - comp, - vulkan1_2 + glsl!( + r#" + // Derived from https://www.shadertoy.com/view/Xl2XWz + #version 460 core + #pragma shader_stage(compute) + + layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; + + layout(push_constant) uniform PushConstants { + layout(offset = 0) float time; + } push_const; + + layout(set = 0, binding = 0, rgba32f) restrict writeonly uniform image2D image; + + float smoothNoise(vec2 p) { + vec2 i = floor(p); + p -= i; + p *= p * (3 - p - p); + + return dot( + mat2(fract(sin(vec4(0, 1, 27, 28) + i.x + i.y * 27) * 1e5)) + * vec2(1 - p.y, p.y), + vec2(1 - p.x, p.x) + ); + } + + float fractalNoise(vec2 p) { + return smoothNoise(p) * 0.57 + + smoothNoise(p * 2.45) * 0.28 + + smoothNoise(p * 6) * 0.15; + } + + float warpedNoise(vec2 p) { + vec2 m = vec2(sin(push_const.time * 0.5), cos(push_const.time * 0.5)); + float x = fractalNoise(p + m); + float y = fractalNoise(p + m.yx + x); + float z = fractalNoise(p - m - x + y); + vec3 w = vec3(x, y, z); + + return fractalNoise(p + w.xy + w.yz + w.zx + length(w) * 0.25); + } + + void main() { + vec2 uv = vec2(gl_GlobalInvocationID.xy) / imageSize(image).y; + float n1 = warpedNoise(uv * 5); + float n2 = warpedNoise(uv * 5 + 0.04); + float bump1 = max(n2 - n1, 0.0) / 0.02 * 0.7071; + float bump2 = max(n1 - n2, 0.1) / 0.04 * 0.7071; + bump1 = bump1 * bump1 * 0.5 + pow(bump1, 4) * 0.5; + bump2 = bump2 * bump2 * 0.5 + pow(bump2, 4) * 0.5; + vec3 col = vec3(n1 * n1 * 0.7, n1, n1 * n1 * 0.4) + * n1 * n1 + * (vec3(0.25, 0.5, 1) + * bump1 * 0.2 + + vec3(1) * bump2 * 0.2 + 0.75); + vec4 fragColor = vec4(sqrt(max(col, 0.)), 1); + + imageStore(image, ivec2(gl_GlobalInvocationID.xy), fragColor); + } + "#, + ) + .as_slice(), ) - .as_slice()).specialization_info(SpecializationInfo { - data: subgroup_size.to_ne_bytes().to_vec(), - map_entries: vec![vk::SpecializationMapEntry { - constant_id: 0, - offset: 0, - size: 4, - }], - }), - )?); + .specialization(SpecializationMap::new(subgroup_size.to_ne_bytes()).constant(0, 0, 4)), + )?; window.run(|frame| { - let image_node = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let image_node = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( 320, 200, vk::Format::R8G8B8A8_UNORM, @@ -137,26 +145,26 @@ fn main() -> anyhow::Result<()> { // Fill the image with a smoke effect let elapsed_time = Instant::now() - start_time; frame - .render_graph - .begin_pass("smoke") + .graph + .begin_cmd() + .debug_name("smoke") .bind_pipeline(&smoke_pipeline) - .write_descriptor(0, image_node) - .record_compute(move |compute, _| { - compute - .push_constants(&elapsed_time.as_secs_f32().to_ne_bytes()) + .shader_resource_access(0, image_node, AccessType::ComputeShaderWrite) + .record_cmd(move |cmd| { + cmd.push_constants(0, &elapsed_time.as_secs_f32().to_ne_bytes()) .dispatch(frame.width.div_ceil(subgroup_size), frame.height, 1); }); // Print some text onto the image - let text = "Screen 13"; + let text = "vk-graph"; let (_offset, [width, height]) = small_10px_font.measure(text); let scale = 4.0; let x = 320f32 * 0.5 / scale - width as f32 * 0.5; let y = 200f32 * 0.5 / scale - height as f32 * 0.5; let color = [196, 172, 230u8]; - small_10px_font.print_scale(frame.render_graph, image_node, x, y, color, text, scale); + small_10px_font.print_scale(frame.graph, image_node, x, y, color, text, scale); - display.present_image(frame.render_graph, image_node, frame.swapchain_image); + display.present_image(frame.graph, image_node, frame.swapchain_image); })?; Ok(()) diff --git a/examples/fuzzer.rs b/examples/fuzzer.rs index 0fb7a741..148ffce3 100644 --- a/examples/fuzzer.rs +++ b/examples/fuzzer.rs @@ -22,21 +22,41 @@ Also helpful to run with valgrind: */ use { + ash::vk, clap::Parser, - inline_spirv::inline_spirv, log::debug, rand::{Rng, rng, seq::IndexedRandom}, - screen_13::prelude::*, - screen_13_window::{FrameContext, WindowBuilder, WindowError}, - std::{mem::size_of, sync::Arc}, + std::mem::size_of, + vk_graph::{ + cmd::{BuildAccelerationStructureInfo, LoadOp, StoreOp}, + driver::{ + accel_struct::{ + AccelerationStructure, AccelerationStructureGeometry, + AccelerationStructureGeometryData, AccelerationStructureGeometryInfo, + AccelerationStructureInfo, DeviceOrHostAddress, + }, + buffer::{Buffer, BufferInfo}, + compute::{ComputePipeline, ComputePipelineInfo}, + device::Device, + graphic::{DepthStencilInfo, GraphicPipeline, GraphicPipelineInfo, StencilMode}, + image::{ImageInfo, SampleCount}, + physical_device::Vulkan10Limits, + render_pass::ResolveMode, + shader::{Shader, SpecializationMap}, + }, + pool::{Pool as _, hash::HashPool}, + }, + vk_graph_window::{FrameContext, Window, WindowError}, + vk_shader_macros::glsl, + vk_sync::AccessType, }; type Operation = fn(&mut FrameContext, &mut HashPool); static OPERATIONS: &[Operation] = &[ - record_compute_array_bind, - record_compute_bindless, - record_compute_no_op, + record_pipeline_array_bind, + record_pipeline_bindless, + record_pipeline_no_op, record_graphic_bindless, record_graphic_load_store, record_graphic_msaa_depth_stencil, @@ -57,14 +77,14 @@ fn main() -> Result<(), WindowError> { let mut rng = rng(); - let screen_13 = WindowBuilder::default().debug(true).build()?; - let mut pool = HashPool::new(&screen_13.device); + let vk_graph = Window::builder().debug(true).build()?; + let mut pool = HashPool::new(&vk_graph.device); let mut frame_count = 0; let args = Args::parse(); - screen_13.run(|mut frame| { + vk_graph.run(|mut frame| { if frame_count == args.frame_count { *frame.will_exit = true; return; @@ -76,7 +96,9 @@ fn main() -> Result<(), WindowError> { let clear_before: bool = rng.random(); if clear_before { - frame.render_graph.clear_color_image(frame.swapchain_image); + frame + .graph + .clear_color_image(frame.swapchain_image, [0f32; 4]); } for _ in 0..args.ops_per_frame { @@ -85,7 +107,9 @@ fn main() -> Result<(), WindowError> { } if !clear_before { - frame.render_graph.clear_color_image(frame.swapchain_image); + frame + .graph + .clear_color_image(frame.swapchain_image, [0f32; 4]); } })?; @@ -100,7 +124,7 @@ fn record_accel_struct_builds(frame: &mut FrameContext, pool: &mut HashPool) { // Vertex buffer for a triangle let vertex_buf = { let mut buf = pool - .lease(BufferInfo::host_mem( + .resource(BufferInfo::host_mem( 36, vk::BufferUsageFlags::ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_KHR | vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS @@ -109,19 +133,19 @@ fn record_accel_struct_builds(frame: &mut FrameContext, pool: &mut HashPool) { .unwrap(); // Vertex 1 - Buffer::copy_from_slice(&mut buf, 0, 0f32.to_ne_bytes()); - Buffer::copy_from_slice(&mut buf, 4, 0f32.to_ne_bytes()); - Buffer::copy_from_slice(&mut buf, 8, 0f32.to_ne_bytes()); + buf.copy_from_slice(0, 0f32.to_ne_bytes().as_slice()); + buf.copy_from_slice(4, 0f32.to_ne_bytes().as_slice()); + buf.copy_from_slice(8, 0f32.to_ne_bytes().as_slice()); // Vertex 2 - Buffer::copy_from_slice(&mut buf, 12, 1f32.to_ne_bytes()); - Buffer::copy_from_slice(&mut buf, 16, 1f32.to_ne_bytes()); - Buffer::copy_from_slice(&mut buf, 20, 0f32.to_ne_bytes()); + buf.copy_from_slice(12, 1f32.to_ne_bytes().as_slice()); + buf.copy_from_slice(16, 1f32.to_ne_bytes().as_slice()); + buf.copy_from_slice(20, 0f32.to_ne_bytes().as_slice()); // Vertex 3 - Buffer::copy_from_slice(&mut buf, 24, 2f32.to_ne_bytes()); - Buffer::copy_from_slice(&mut buf, 28, 0f32.to_ne_bytes()); - Buffer::copy_from_slice(&mut buf, 32, 0f32.to_ne_bytes()); + buf.copy_from_slice(24, 2f32.to_ne_bytes().as_slice()); + buf.copy_from_slice(28, 0f32.to_ne_bytes().as_slice()); + buf.copy_from_slice(32, 0f32.to_ne_bytes().as_slice()); buf }; @@ -129,7 +153,7 @@ fn record_accel_struct_builds(frame: &mut FrameContext, pool: &mut HashPool) { // Index buffer for a single triangle let index_buf = { let mut buf = pool - .lease(BufferInfo::host_mem( + .resource(BufferInfo::host_mem( 6, vk::BufferUsageFlags::ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_KHR | vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS @@ -137,9 +161,9 @@ fn record_accel_struct_builds(frame: &mut FrameContext, pool: &mut HashPool) { )) .unwrap(); - Buffer::copy_from_slice(&mut buf, 0, 0u16.to_ne_bytes()); - Buffer::copy_from_slice(&mut buf, 2, 1u16.to_ne_bytes()); - Buffer::copy_from_slice(&mut buf, 4, 2u16.to_ne_bytes()); + buf.copy_from_slice(0, 0u16.to_ne_bytes().as_slice()); + buf.copy_from_slice(2, 1u16.to_ne_bytes().as_slice()); + buf.copy_from_slice(4, 2u16.to_ne_bytes().as_slice()); buf }; @@ -149,13 +173,11 @@ fn record_accel_struct_builds(frame: &mut FrameContext, pool: &mut HashPool) { max_primitive_count: 1, flags: vk::GeometryFlagsKHR::OPAQUE, geometry: AccelerationStructureGeometryData::Triangles { - index_addr: DeviceOrHostAddress::DeviceAddress(Buffer::device_address(&index_buf)), + index_addr: DeviceOrHostAddress::DeviceAddress(index_buf.device_address()), index_type: vk::IndexType::UINT16, max_vertex: 3, transform_addr: None, - vertex_addr: DeviceOrHostAddress::DeviceAddress(Buffer::device_address( - &vertex_buf, - )), + vertex_addr: DeviceOrHostAddress::DeviceAddress(vertex_buf.device_address()), vertex_format: vk::Format::R32G32B32_SFLOAT, vertex_stride: 12, }, @@ -188,10 +210,9 @@ fn record_accel_struct_builds(frame: &mut FrameContext, pool: &mut HashPool) { // Lease and bind a bunch of bottom-level acceleration structures and add to instance buffer let mut blas_nodes = Vec::with_capacity(BLAS_COUNT as _); for idx in 0..BLAS_COUNT { - let blas = pool.lease(blas_info).unwrap(); + let blas = pool.resource(blas_info).unwrap(); - Buffer::copy_from_slice( - &mut instance_buf, + instance_buf.copy_from_slice( idx * instance_len, AccelerationStructure::instance_slice(&[vk::AccelerationStructureInstanceKHR { transform: vk::TransformMatrixKHR { @@ -212,21 +233,21 @@ fn record_accel_struct_builds(frame: &mut FrameContext, pool: &mut HashPool) { }]), ); - let blas_node = frame.render_graph.bind_node(blas); - let scratch_buf = frame.render_graph.bind_node( - pool.lease( + let blas_node = frame.graph.bind_resource(blas); + let scratch_buf = frame.graph.bind_resource( + pool.resource( BufferInfo::device_mem( blas_size.build_size, vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS | vk::BufferUsageFlags::STORAGE_BUFFER, ) - .to_builder() + .into_builder() .alignment(accel_struct_scratch_offset_alignment), ) .unwrap(), ); - blas_nodes.push((scratch_buf, blas_node)); + blas_nodes.push((blas_node, scratch_buf, blas_geometry_info.clone())); } // Lease and bind a single top-level acceleration structure @@ -236,84 +257,96 @@ fn record_accel_struct_builds(frame: &mut FrameContext, pool: &mut HashPool) { flags: vk::GeometryFlagsKHR::OPAQUE, geometry: AccelerationStructureGeometryData::Instances { array_of_pointers: false, - addr: DeviceOrHostAddress::DeviceAddress(Buffer::device_address(&instance_buf)), + addr: DeviceOrHostAddress::DeviceAddress(instance_buf.device_address()), }, }, vk::AccelerationStructureBuildRangeInfoKHR::default().primitive_count(1), )]); - let instance_buf = frame.render_graph.bind_node(instance_buf); + let instance_buf = frame.graph.bind_resource(instance_buf); let tlas_size = AccelerationStructure::size_of(frame.device, &tlas_geometry_info); let tlas = pool - .lease(AccelerationStructureInfo::tlas(tlas_size.create_size)) + .resource(AccelerationStructureInfo::tlas(tlas_size.create_size)) .unwrap(); - let tlas_node = frame.render_graph.bind_node(tlas); - let tlas_scratch_buf = frame.render_graph.bind_node( - pool.lease( + let tlas_node = frame.graph.bind_resource(tlas); + let tlas_scratch_buf = frame.graph.bind_resource( + pool.resource( BufferInfo::device_mem( tlas_size.build_size, vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS | vk::BufferUsageFlags::STORAGE_BUFFER, ) - .to_builder() + .into_builder() .alignment(accel_struct_scratch_offset_alignment), ) .unwrap(), ); - let index_node = frame.render_graph.bind_node(index_buf); - let vertex_node = frame.render_graph.bind_node(vertex_buf); + let index_node = frame.graph.bind_resource(index_buf); + let vertex_node = frame.graph.bind_resource(vertex_buf); - let pass = frame - .render_graph - .begin_pass("build acceleration structures"); + let mut cmd = frame + .graph + .begin_cmd() + .debug_name("build acceleration structures"); - // TODO: AccessType for these is funky, should be access_node? - let mut pass = pass.read_node(index_node).read_node(vertex_node); + cmd.set_resource_access(index_node, AccessType::IndexBuffer); + cmd.set_resource_access(vertex_node, AccessType::VertexBuffer); - // TODO: Like this: - for (scratch_buf, blas_node) in &blas_nodes { - pass.access_node_mut(*scratch_buf, AccessType::AccelerationStructureBufferWrite); - pass.access_node_mut(*blas_node, AccessType::AccelerationStructureBuildWrite); + for (blas_node, scratch_buf, _) in &blas_nodes { + cmd.set_resource_access(*blas_node, AccessType::AccelerationStructureBuildWrite); + cmd.set_resource_access(*scratch_buf, AccessType::AccelerationStructureBufferWrite); } // Ugly copy of the nodes that I want to figure out a way around while not being confusing let blas_nodes_copy = blas_nodes .iter() - .map(|(_, blas_node)| *blas_node) + .map(|(blas_node, _, _)| *blas_node) .collect::>(); - let mut pass = pass.record_acceleration(move |accel, bindings| { - for (scratch_buf, blas_node) in blas_nodes { - let scratch_data = Buffer::device_address(&bindings[scratch_buf]); - accel.build_structure(&blas_geometry_info, blas_node, scratch_data); + let mut cmd = cmd.record_cmd(move |cmd| { + for (blas_node, scratch_buf, build_data) in blas_nodes { + let scratch_buf = cmd.resource(scratch_buf); + let scratch_addr = scratch_buf.device_address(); + + cmd.build_accel_struct(&[BuildAccelerationStructureInfo::new( + blas_node, + scratch_addr, + build_data, + )]); } }); for blas_node in blas_nodes_copy { - pass.access_node_mut(blas_node, AccessType::AccelerationStructureBuildRead); + cmd.set_resource_access(blas_node, AccessType::AccelerationStructureBuildRead); } - pass.access_node_mut(instance_buf, AccessType::AccelerationStructureBuildRead); - pass.access_node_mut( - tlas_scratch_buf, - AccessType::AccelerationStructureBufferWrite, - ); - pass.access_node_mut(tlas_node, AccessType::AccelerationStructureBuildWrite); - - pass.record_acceleration(move |accel, bindings| { - let scratch_data = Buffer::device_address(&bindings[tlas_scratch_buf]); - accel.build_structure(&tlas_geometry_info, tlas_node, scratch_data); - }); + cmd.resource_access(instance_buf, AccessType::AccelerationStructureBuildRead) + .resource_access( + tlas_scratch_buf, + AccessType::AccelerationStructureBufferWrite, + ) + .resource_access(tlas_node, AccessType::AccelerationStructureBuildWrite) + .record_cmd(move |cmd| { + let scratch_buf = cmd.resource(tlas_scratch_buf); + let scratch_addr = scratch_buf.device_address(); + + cmd.build_accel_struct(&[BuildAccelerationStructureInfo::new( + tlas_node, + scratch_addr, + tlas_geometry_info, + )]); + }); } -fn record_compute_array_bind(frame: &mut FrameContext, pool: &mut HashPool) { +fn record_pipeline_array_bind(frame: &mut FrameContext, pool: &mut HashPool) { let pipeline = compute_pipeline( "array_bind", frame.device, ComputePipelineInfo::default(), - Shader::new_compute( - inline_spirv!( + Shader::from_spirv( + glsl!( r#" #version 460 core + #pragma shader_stage(compute) layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; @@ -323,23 +356,16 @@ fn record_compute_array_bind(frame: &mut FrameContext, pool: &mut HashPool) { layout(offset = 0) float offset; } push_const; - layout(set = 0, binding = 0) uniform sampler2D layer_images_sampler_llr[LAYER_COUNT]; + layout(set = 0, binding = 0) uniform sampler2D + layer_images_sampler_llr[LAYER_COUNT]; void main() { } - "#, - comp + "# ) .as_slice(), ) - .specialization_info(SpecializationInfo::new( - vec![vk::SpecializationMapEntry { - constant_id: 0, - offset: 0, - size: 4, - }], - 5u32.to_ne_bytes(), - )), + .specialization(SpecializationMap::new(5u32.to_ne_bytes()).constant(0, 0, 4)), ); let image_info = ImageInfo::image_2d( @@ -350,53 +376,54 @@ fn record_compute_array_bind(frame: &mut FrameContext, pool: &mut HashPool) { ); let images = [ frame - .render_graph - .bind_node(pool.lease(image_info).unwrap()), + .graph + .bind_resource(pool.resource(image_info).unwrap()), frame - .render_graph - .bind_node(pool.lease(image_info).unwrap()), + .graph + .bind_resource(pool.resource(image_info).unwrap()), frame - .render_graph - .bind_node(pool.lease(image_info).unwrap()), + .graph + .bind_resource(pool.resource(image_info).unwrap()), frame - .render_graph - .bind_node(pool.lease(image_info).unwrap()), + .graph + .bind_resource(pool.resource(image_info).unwrap()), frame - .render_graph - .bind_node(pool.lease(image_info).unwrap()), + .graph + .bind_resource(pool.resource(image_info).unwrap()), ]; frame - .render_graph - .clear_color_image(images[0]) - .clear_color_image(images[1]) - .clear_color_image(images[2]) - .clear_color_image(images[3]) - .clear_color_image(images[4]) - .begin_pass("array-bind") + .graph + .clear_color_image(images[0], [0f32; 4]) + .clear_color_image(images[1], [0f32; 4]) + .clear_color_image(images[2], [0f32; 4]) + .clear_color_image(images[3], [0f32; 4]) + .clear_color_image(images[4], [0f32; 4]) + .begin_cmd() + .debug_name("array-bind") .bind_pipeline(&pipeline) - .read_descriptor((0, [0]), images[0]) - .read_descriptor((0, [1]), images[1]) - .read_descriptor((0, [2]), images[2]) - .read_descriptor((0, [3]), images[3]) - .read_descriptor((0, [4]), images[4]) - .record_compute(|compute, _| { - compute - .push_constants(&0f32.to_ne_bytes()) + .shader_resource_access((0, [0]), images[0], AccessType::ComputeShaderReadOther) + .shader_resource_access((0, [1]), images[1], AccessType::ComputeShaderReadOther) + .shader_resource_access((0, [2]), images[2], AccessType::ComputeShaderReadOther) + .shader_resource_access((0, [3]), images[3], AccessType::ComputeShaderReadOther) + .shader_resource_access((0, [4]), images[4], AccessType::ComputeShaderReadOther) + .record_cmd(|cmd| { + cmd.push_constants(0, &0f32.to_ne_bytes()) .dispatch(64, 64, 1); }); } -fn record_compute_bindless(frame: &mut FrameContext, pool: &mut HashPool) { +fn record_pipeline_bindless(frame: &mut FrameContext, pool: &mut HashPool) { let pipeline = compute_pipeline( "bindless", frame.device, ComputePipelineInfo::default(), Shader::new_compute( - inline_spirv!( + glsl!( r#" #version 460 core #extension GL_EXT_nonuniform_qualifier : require + #pragma shader_stage(compute) layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; @@ -415,8 +442,7 @@ fn record_compute_bindless(frame: &mut FrameContext, pool: &mut HashPool) { ); } } - "#, - comp + "# ) .as_slice(), ), @@ -430,62 +456,63 @@ fn record_compute_bindless(frame: &mut FrameContext, pool: &mut HashPool) { ); let images = [ frame - .render_graph - .bind_node(pool.lease(image_info).unwrap()), + .graph + .bind_resource(pool.resource(image_info).unwrap()), frame - .render_graph - .bind_node(pool.lease(image_info).unwrap()), + .graph + .bind_resource(pool.resource(image_info).unwrap()), frame - .render_graph - .bind_node(pool.lease(image_info).unwrap()), + .graph + .bind_resource(pool.resource(image_info).unwrap()), frame - .render_graph - .bind_node(pool.lease(image_info).unwrap()), + .graph + .bind_resource(pool.resource(image_info).unwrap()), frame - .render_graph - .bind_node(pool.lease(image_info).unwrap()), + .graph + .bind_resource(pool.resource(image_info).unwrap()), ]; frame - .render_graph - .begin_pass("compute-bindless") + .graph + .begin_cmd() + .debug_name("compute-bindless") .bind_pipeline(&pipeline) - .write_descriptor((0, [0]), images[0]) - .write_descriptor((0, [1]), images[1]) - .write_descriptor((0, [2]), images[2]) - .write_descriptor((0, [3]), images[3]) - .write_descriptor((0, [4]), images[4]) - .record_compute(|compute, _| { - compute - .push_constants(&5u32.to_ne_bytes()) + .shader_resource_access((0, [0]), images[0], AccessType::ComputeShaderWrite) + .shader_resource_access((0, [1]), images[1], AccessType::ComputeShaderWrite) + .shader_resource_access((0, [2]), images[2], AccessType::ComputeShaderWrite) + .shader_resource_access((0, [3]), images[3], AccessType::ComputeShaderWrite) + .shader_resource_access((0, [4]), images[4], AccessType::ComputeShaderWrite) + .record_cmd(|cmd| { + cmd.push_constants(0, &5u32.to_ne_bytes()) .dispatch(64, 64, 1); }); } -fn record_compute_no_op(frame: &mut FrameContext, _: &mut HashPool) { +fn record_pipeline_no_op(frame: &mut FrameContext, _: &mut HashPool) { let pipeline = compute_pipeline( "no_op", frame.device, ComputePipelineInfo::default(), Shader::new_compute( - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(compute) void main() { } - "#, - comp + "# ) .as_slice(), ), ); frame - .render_graph - .begin_pass("no-op") + .graph + .begin_cmd() + .debug_name("no-op") .bind_pipeline(&pipeline) - .record_compute(|compute, _| { - compute.dispatch(1, 1, 1); + .record_cmd(|cmd| { + cmd.dispatch(1, 1, 1); }); } @@ -493,20 +520,21 @@ fn record_graphic_bindless(frame: &mut FrameContext, pool: &mut HashPool) { let pipeline = graphic_vert_frag_pipeline( frame.device, GraphicPipelineInfo::default(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(vertex) void main() { } - "#, - vert + "# ) .as_slice(), - inline_spirv!( + glsl!( r#" #version 460 core #extension GL_EXT_nonuniform_qualifier : require + #pragma shader_stage(fragment) layout(push_constant) uniform PushConstants { layout(offset = 0) uint count; @@ -524,14 +552,13 @@ fn record_graphic_bindless(frame: &mut FrameContext, pool: &mut HashPool) { ); } } - "#, - frag + "# ) .as_slice(), ); - let image = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let image = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( 256, 256, vk::Format::R8G8B8A8_UNORM, @@ -549,40 +576,60 @@ fn record_graphic_bindless(frame: &mut FrameContext, pool: &mut HashPool) { ); let images = [ frame - .render_graph - .bind_node(pool.lease(image_info).unwrap()), + .graph + .bind_resource(pool.resource(image_info).unwrap()), frame - .render_graph - .bind_node(pool.lease(image_info).unwrap()), + .graph + .bind_resource(pool.resource(image_info).unwrap()), frame - .render_graph - .bind_node(pool.lease(image_info).unwrap()), + .graph + .bind_resource(pool.resource(image_info).unwrap()), frame - .render_graph - .bind_node(pool.lease(image_info).unwrap()), + .graph + .bind_resource(pool.resource(image_info).unwrap()), frame - .render_graph - .bind_node(pool.lease(image_info).unwrap()), + .graph + .bind_resource(pool.resource(image_info).unwrap()), ]; frame - .render_graph - .clear_color_image(images[0]) - .clear_color_image(images[1]) - .clear_color_image(images[2]) - .clear_color_image(images[3]) - .clear_color_image(images[4]) - .begin_pass("graphic-bindless") + .graph + .clear_color_image(images[0], [0f32; 4]) + .clear_color_image(images[1], [0f32; 4]) + .clear_color_image(images[2], [0f32; 4]) + .clear_color_image(images[3], [0f32; 4]) + .clear_color_image(images[4], [0f32; 4]) + .begin_cmd() + .debug_name("graphic-bindless") .bind_pipeline(&pipeline) - .read_descriptor((0, [0]), images[0]) - .read_descriptor((0, [1]), images[1]) - .read_descriptor((0, [2]), images[2]) - .read_descriptor((0, [3]), images[3]) - .read_descriptor((0, [4]), images[4]) - .clear_color(0, image) - .store_color(0, image) - .record_subpass(|subpass, _| { - subpass.push_constants(&5u32.to_ne_bytes()).draw(1, 1, 0, 0); + .shader_resource_access( + (0, [0]), + images[0], + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + .shader_resource_access( + (0, [1]), + images[1], + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + .shader_resource_access( + (0, [2]), + images[2], + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + .shader_resource_access( + (0, [3]), + images[3], + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + .shader_resource_access( + (0, [4]), + images[4], + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + .color_attachment_image(0, image, LoadOp::CLEAR_BLACK_ALPHA_ZERO, StoreOp::Store) + .record_cmd(|cmd| { + cmd.push_constants(0, &5u32.to_ne_bytes()).draw(1, 1, 0, 0); }); } @@ -590,39 +637,39 @@ fn record_graphic_load_store(frame: &mut FrameContext, _: &mut HashPool) { let pipeline = graphic_vert_frag_pipeline( frame.device, GraphicPipelineInfo::default(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(vertex) void main() { } - "#, - vert + "# ) .as_slice(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(fragment) layout(location = 0) out vec4 color_out; void main() { color_out = vec4(0); } - "#, - frag + "# ) .as_slice(), ); frame - .render_graph - .begin_pass("load-store") + .graph + .begin_cmd() + .debug_name("load-store") .bind_pipeline(&pipeline) - .load_color(0, frame.swapchain_image) - .store_color(0, frame.swapchain_image) - .record_subpass(|subpass, _| { - subpass.draw(1, 1, 0, 0); + .color_attachment_image(0, frame.swapchain_image, LoadOp::Load, StoreOp::Store) + .record_cmd(|cmd| { + cmd.draw(1, 1, 0, 0); }); } @@ -655,8 +702,7 @@ fn record_graphic_msaa_depth_stencil(frame: &mut FrameContext, pool: &mut HashPo vk::Format::D16_UNORM_S8_UINT, vk::Format::D32_SFLOAT_S8_UINT, ] { - let format_props = Device::image_format_properties( - frame.device, + let format_props = frame.device.physical_device.image_format_properties( format, vk::ImageType::TYPE_2D, vk::ImageTiling::OPTIMAL, @@ -696,10 +742,11 @@ fn record_graphic_msaa_depth_stencil(frame: &mut FrameContext, pool: &mut HashPo let pipeline = graphic_vert_frag_pipeline( frame.device, - GraphicPipelineInfoBuilder::default().samples(sample_count), - inline_spirv!( + GraphicPipelineInfo::builder().samples(sample_count), + glsl!( r#" #version 460 core + #pragma shader_stage(vertex) const vec2 UV[3] = { vec2(-1, -1), @@ -710,41 +757,40 @@ fn record_graphic_msaa_depth_stencil(frame: &mut FrameContext, pool: &mut HashPo void main() { gl_Position = vec4(UV[gl_VertexIndex], 0, 1); } - "#, - vert + "# ) .as_slice(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(fragment) layout(location = 0) out vec4 color_out; void main() { color_out = vec4(1); } - "#, - frag + "# ) .as_slice(), ); - let swapchain_format = frame.render_graph.node_info(frame.swapchain_image).fmt; - let msaa_color_image = frame.render_graph.bind_node( - pool.lease( + let swapchain_format = frame.graph.resource(frame.swapchain_image).info.fmt; + let msaa_color_image = frame.graph.bind_resource( + pool.resource( ImageInfo::image_2d( frame.width, frame.height, swapchain_format, vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::TRANSIENT_ATTACHMENT, ) - .to_builder() + .into_builder() .sample_count(sample_count), ) .unwrap(), ); - let msaa_depth_stencil_image = frame.render_graph.bind_node( - pool.lease( + let msaa_depth_stencil_image = frame.graph.bind_resource( + pool.resource( ImageInfo::image_2d( frame.width, frame.height, @@ -752,13 +798,13 @@ fn record_graphic_msaa_depth_stencil(frame: &mut FrameContext, pool: &mut HashPo vk::ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT | vk::ImageUsageFlags::TRANSIENT_ATTACHMENT, ) - .to_builder() + .into_builder() .sample_count(sample_count), ) .unwrap(), ); - let depth_stencil_image = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let depth_stencil_image = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( frame.width, frame.height, depth_stencil_format, @@ -767,7 +813,7 @@ fn record_graphic_msaa_depth_stencil(frame: &mut FrameContext, pool: &mut HashPo .unwrap(), ); - let depth_stencil_mode = DepthStencilMode { + let depth_stencil_info = DepthStencilInfo { back: StencilMode::IGNORE, bounds_test: true, compare_op: vk::CompareOp::LESS_OR_EQUAL, @@ -788,27 +834,37 @@ fn record_graphic_msaa_depth_stencil(frame: &mut FrameContext, pool: &mut HashPo }; frame - .render_graph - .begin_pass("msaa-depth-stencil") + .graph + .begin_cmd() + .debug_name("msaa-depth-stencil") .bind_pipeline(&pipeline) - .set_depth_stencil(depth_stencil_mode) - .clear_color(0, msaa_color_image) - .clear_depth_stencil(msaa_depth_stencil_image) - .resolve_color(0, 1, frame.swapchain_image) - .resolve_depth_stencil( + .depth_stencil(depth_stencil_info) + .color_attachment_image( + 0, + msaa_color_image, + LoadOp::CLEAR_BLACK_ALPHA_ZERO, + StoreOp::DontCare, + ) + .color_attachment_resolve_image(0, 1, frame.swapchain_image) + .depth_stencil_attachment_image( + msaa_depth_stencil_image, + LoadOp::CLEAR_ZERO_STENCIL_ZERO, + StoreOp::DontCare, + ) + .depth_stencil_attachment_resolve_image( 2, depth_stencil_image, Some(depth_resolve_mode), Some(ResolveMode::SampleZero), ) - .record_subpass(|subpass, _| { - subpass.draw(3, 1, 0, 0); + .record_cmd(|cmd| { + cmd.draw(3, 1, 0, 0); }); } fn record_graphic_will_merge_common_color1(frame: &mut FrameContext, pool: &mut HashPool) { - let image = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let image = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( 256, 256, vk::Format::R8G8B8A8_UNORM, @@ -819,71 +875,78 @@ fn record_graphic_will_merge_common_color1(frame: &mut FrameContext, pool: &mut // Pass "a" stores color0 which "b" compatibly loads; so these two will get merged frame - .render_graph - .begin_pass("a") + .graph + .begin_cmd() + .debug_name("a") .bind_pipeline(graphic_vert_frag_pipeline( frame.device, GraphicPipelineInfo::default(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(vertex) + void main() { } - "#, - vert + "# ) .as_slice(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(fragment) + layout(location = 0) out vec4 color0; + void main() { color0 = vec4(0); } - "#, - frag + "# ) .as_slice(), )) - .store_color(0, image) - .record_subpass(|subpass, _| { - subpass.draw(1, 1, 0, 0); + .color_attachment_image(0, image, LoadOp::DontCare, StoreOp::Store) + .record_cmd(|cmd| { + cmd.draw(1, 1, 0, 0); }); frame - .render_graph - .begin_pass("b") + .graph + .begin_cmd() + .debug_name("b") .bind_pipeline(&graphic_vert_frag_pipeline( frame.device, GraphicPipelineInfo::default(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(vertex) + void main() { } - "#, - vert + "# ) .as_slice(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(fragment) + layout(location = 0) out vec4 color0; + void main() { color0 = vec4(0); } - "#, - frag + "# ) .as_slice(), )) - .load_color(0, image) - .store_color(0, image) - .record_subpass(|subpass, _| { - subpass.draw(1, 1, 0, 0); + .color_attachment_image(0, image, LoadOp::Load, StoreOp::Store) + .record_cmd(|cmd| { + cmd.draw(1, 1, 0, 0); }); } fn record_graphic_will_merge_common_color2(frame: &mut FrameContext, pool: &mut HashPool) { - let image_0 = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let image_0 = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( 256, 256, vk::Format::R8G8B8A8_UNORM, @@ -891,8 +954,8 @@ fn record_graphic_will_merge_common_color2(frame: &mut FrameContext, pool: &mut )) .unwrap(), ); - let image_1 = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let image_1 = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( 256, 256, vk::Format::R8G8B8A8_UNORM, @@ -902,105 +965,115 @@ fn record_graphic_will_merge_common_color2(frame: &mut FrameContext, pool: &mut ); frame - .render_graph - .begin_pass("a") + .graph + .begin_cmd() + .debug_name("a") .bind_pipeline(graphic_vert_frag_pipeline( frame.device, GraphicPipelineInfo::default(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(vertex) + void main() { } - "#, - vert + "# ) .as_slice(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(fragment) + layout(location = 0) out vec4 color0; + void main() { color0 = vec4(0); } - "#, - frag + "# ) .as_slice(), )) - .store_color(0, image_0) - .record_subpass(|subpass, _| { - subpass.draw(1, 1, 0, 0); + .color_attachment_image(0, image_0, LoadOp::DontCare, StoreOp::Store) + .record_cmd(|cmd| { + cmd.draw(1, 1, 0, 0); }); frame - .render_graph - .begin_pass("b") + .graph + .begin_cmd() + .debug_name("b") .bind_pipeline(&graphic_vert_frag_pipeline( frame.device, GraphicPipelineInfo::default(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(vertex) + void main() { } - "#, - vert + "# ) .as_slice(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(fragment) + layout(location = 0) out vec4 color0; layout(location = 1) out vec4 color1; + void main() { color0 = vec4(0); color1 = vec4(0); } - "#, - frag + "# ) .as_slice(), )) - .load_color(0, image_0) - .store_color(0, image_0) - .store_color(1, image_1) - .record_subpass(|subpass, _| { - subpass.draw(1, 1, 0, 0); + .color_attachment_image(0, image_0, LoadOp::Load, StoreOp::Store) + .color_attachment_image(1, image_1, LoadOp::DontCare, StoreOp::Store) + .record_cmd(|cmd| { + cmd.draw(1, 1, 0, 0); }); frame - .render_graph - .begin_pass("c") + .graph + .begin_cmd() + .debug_name("c") .bind_pipeline(&graphic_vert_frag_pipeline( frame.device, GraphicPipelineInfo::default(), - inline_spirv!( + glsl!( r#" - #version 460 core - void main() { } - "#, - vert + #version 460 core + #pragma shader_stage(vertex) + + void main() { } + "# ) .as_slice(), - inline_spirv!( + glsl!( r#" - #version 460 core - layout(location = 0) out vec4 color0; - void main() { - color0 = vec4(0); - } - "#, - frag + #version 460 core + #pragma shader_stage(fragment) + + layout(location = 0) out vec4 color0; + + void main() { + color0 = vec4(0); + } + "# ) .as_slice(), )) - .clear_color(0, image_0) - .store_color(0, image_0) - .record_subpass(|subpass, _| { - subpass.draw(1, 1, 0, 0); + .color_attachment_image(0, image_0, LoadOp::CLEAR_BLACK_ALPHA_ZERO, StoreOp::Store) + .record_cmd(|cmd| { + cmd.draw(1, 1, 0, 0); }); } fn record_graphic_will_merge_common_depth1(frame: &mut FrameContext, pool: &mut HashPool) { - let color_image = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let color_image = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( 256, 256, vk::Format::R8G8B8A8_UNORM, @@ -1008,8 +1081,8 @@ fn record_graphic_will_merge_common_depth1(frame: &mut FrameContext, pool: &mut )) .unwrap(), ); - let depth_image = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let depth_image = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( 256, 256, vk::Format::D32_SFLOAT, @@ -1020,71 +1093,77 @@ fn record_graphic_will_merge_common_depth1(frame: &mut FrameContext, pool: &mut // Pass "a" stores color0+depth which "b" compatibly loads; so these two will get merged frame - .render_graph - .begin_pass("a") + .graph + .begin_cmd() + .debug_name("a") .bind_pipeline(graphic_vert_frag_pipeline( frame.device, GraphicPipelineInfo::default(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(vertex) + void main() { } - "#, - vert + "# ) .as_slice(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(fragment) + layout(location = 0) out vec4 color_out; + void main() { color_out = vec4(0); } - "#, - frag + "# ) .as_slice(), )) - .store_color(0, color_image) - .store_depth_stencil(depth_image) - .record_subpass(|subpass, _| { - subpass.draw(1, 1, 0, 0); + .color_attachment_image(0, color_image, LoadOp::DontCare, StoreOp::Store) + .depth_stencil_attachment_image(depth_image, LoadOp::DontCare, StoreOp::Store) + .record_cmd(|cmd| { + cmd.draw(1, 1, 0, 0); }); frame - .render_graph - .begin_pass("b") + .graph + .begin_cmd() + .debug_name("b") .bind_pipeline(graphic_vert_frag_pipeline( frame.device, GraphicPipelineInfo::default(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(vertex) + void main() { } - "#, - vert + "# ) .as_slice(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(fragment) + void main() { gl_FragDepth = 0.0; } - "#, - frag + "# ) .as_slice(), )) - .load_depth_stencil(depth_image) - .store_depth_stencil(depth_image) - .record_subpass(|subpass, _| { - subpass.draw(1, 1, 0, 0); + .depth_stencil_attachment_image(depth_image, LoadOp::Load, StoreOp::Store) + .record_cmd(|cmd| { + cmd.draw(1, 1, 0, 0); }); } fn record_graphic_will_merge_common_depth2(frame: &mut FrameContext, pool: &mut HashPool) { - let color_image = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let color_image = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( 256, 256, vk::Format::R8G8B8A8_UNORM, @@ -1092,8 +1171,8 @@ fn record_graphic_will_merge_common_depth2(frame: &mut FrameContext, pool: &mut )) .unwrap(), ); - let depth_image = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let depth_image = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( 256, 256, vk::Format::D32_SFLOAT, @@ -1104,71 +1183,77 @@ fn record_graphic_will_merge_common_depth2(frame: &mut FrameContext, pool: &mut // Pass "a" stores color0+depth which "b" compatibly loads; so these two will get merged frame - .render_graph - .begin_pass("a") + .graph + .begin_cmd() + .debug_name("a") .bind_pipeline(graphic_vert_frag_pipeline( frame.device, GraphicPipelineInfo::default(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(vertex) + void main() { } - "#, - vert + "# ) .as_slice(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(fragment) + void main() { gl_FragDepth = 0.0; } - "#, - frag + "# ) .as_slice(), )) - .store_depth_stencil(depth_image) - .record_subpass(|subpass, _| { - subpass.draw(1, 1, 0, 0); + .depth_stencil_attachment_image(depth_image, LoadOp::DontCare, StoreOp::Store) + .record_cmd(|cmd| { + cmd.draw(1, 1, 0, 0); }); frame - .render_graph - .begin_pass("b") + .graph + .begin_cmd() + .debug_name("b") .bind_pipeline(graphic_vert_frag_pipeline( frame.device, GraphicPipelineInfo::default(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(vertex) + void main() { } - "#, - vert + "# ) .as_slice(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(fragment) + layout(location = 0) out vec4 color_out; + void main() { color_out = vec4(0); } - "#, - frag + "# ) .as_slice(), )) - .store_color(0, color_image) - .load_depth_stencil(depth_image) - .store_depth_stencil(depth_image) - .record_subpass(|subpass, _| { - subpass.draw(1, 1, 0, 0); + .color_attachment_image(0, color_image, LoadOp::DontCare, StoreOp::Store) + .depth_stencil_attachment_image(depth_image, LoadOp::Load, StoreOp::Store) + .record_cmd(|cmd| { + cmd.draw(1, 1, 0, 0); }); } fn record_graphic_will_merge_common_depth3(frame: &mut FrameContext, pool: &mut HashPool) { - let depth_image = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let depth_image = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( 256, 256, vk::Format::D32_SFLOAT, @@ -1178,92 +1263,97 @@ fn record_graphic_will_merge_common_depth3(frame: &mut FrameContext, pool: &mut ); frame - .render_graph - .begin_pass("a") + .graph + .begin_cmd() + .debug_name("a") .bind_pipeline(graphic_vert_frag_pipeline( frame.device, GraphicPipelineInfo::default(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(vertex) + void main() { } - "#, - vert + "# ) .as_slice(), - inline_spirv!( + glsl!( kind: frag, r#" #version 460 core + #pragma shader_stage(fragment) + void main() { gl_FragDepth = 0.0; } - "#, - frag + "# ) .as_slice(), )) - .store_depth_stencil(depth_image) - .record_subpass(|subpass, _| { - subpass.draw(1, 1, 0, 0); + .depth_stencil_attachment_image(depth_image, LoadOp::DontCare, StoreOp::Store) + .record_cmd(|cmd| { + cmd.draw(1, 1, 0, 0); }); frame - .render_graph - .begin_pass("b") + .graph + .begin_cmd() + .debug_name("b") .bind_pipeline(graphic_vert_frag_pipeline( frame.device, GraphicPipelineInfo::default(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(vertex) + void main() { } - "#, - vert + "# ) .as_slice(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(fragment) + void main() { gl_FragDepth = 0.0; } - "#, - frag + "# ) .as_slice(), )) - .load_depth_stencil(depth_image) - .store_depth_stencil(depth_image) - .record_subpass(|subpass, _| { - subpass.draw(1, 1, 0, 0); + .depth_stencil_attachment_image(depth_image, LoadOp::Load, StoreOp::Store) + .record_cmd(|cmd| { + cmd.draw(1, 1, 0, 0); }); } fn record_graphic_will_merge_subpass_input(frame: &mut FrameContext, pool: &mut HashPool) { - let vertex = inline_spirv!( + let vertex = glsl!( r#" #version 460 core + #pragma shader_stage(vertex) void main() { } - "#, - vert + "# ) .as_slice(); let pipeline_a = graphic_vert_frag_pipeline( frame.device, GraphicPipelineInfo::default(), vertex, - inline_spirv!( + glsl!( kind: frag, r#" #version 460 core + #pragma shader_stage(fragment) layout(location = 0) out vec4 color_out; void main() { color_out = vec4(0); } - "#, - frag + "# ) .as_slice(), ); @@ -1271,9 +1361,11 @@ fn record_graphic_will_merge_subpass_input(frame: &mut FrameContext, pool: &mut frame.device, GraphicPipelineInfo::default(), vertex, - inline_spirv!( + glsl!( + kind: frag, r#" #version 460 core + #pragma shader_stage(fragment) layout(input_attachment_index = 0, binding = 0) uniform subpassInput color_in; layout(location = 0) out vec4 color_out; @@ -1281,13 +1373,12 @@ fn record_graphic_will_merge_subpass_input(frame: &mut FrameContext, pool: &mut void main() { color_out = subpassLoad(color_in); } - "#, - frag + "# ) .as_slice(), ); - let image = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let image = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( 256, 256, vk::Format::R8G8B8A8_UNORM, @@ -1300,21 +1391,22 @@ fn record_graphic_will_merge_subpass_input(frame: &mut FrameContext, pool: &mut // Pass "a" stores color 0 which "b" compatibly inputs; so these two will get merged frame - .render_graph - .begin_pass("a") + .graph + .begin_cmd() + .debug_name("a") .bind_pipeline(&pipeline_a) - .clear_color(0, image) - .store_color(0, image) - .record_subpass(|subpass, _| { - subpass.draw(1, 1, 0, 0); + .color_attachment_image(0, image, LoadOp::CLEAR_BLACK_ALPHA_ZERO, StoreOp::Store) + .record_cmd(|cmd| { + cmd.draw(1, 1, 0, 0); }); frame - .render_graph - .begin_pass("b") + .graph + .begin_cmd() + .debug_name("b") .bind_pipeline(&pipeline_b) - .store_color(0, image) - .record_subpass(|subpass, _| { - subpass.draw(1, 1, 0, 0); + .color_attachment_image(0, image, LoadOp::DontCare, StoreOp::Store) + .record_cmd(|cmd| { + cmd.draw(1, 1, 0, 0); }); } @@ -1322,32 +1414,32 @@ fn record_graphic_wont_merge(frame: &mut FrameContext, pool: &mut HashPool) { let pipeline = graphic_vert_frag_pipeline( frame.device, GraphicPipelineInfo::default(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(vertex) void main() { } - "#, - vert + "# ) .as_slice(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(fragment) layout(location = 0) out vec4 color; void main() { } - "#, - frag + "# ) .as_slice(), ); - let image = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let image = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( 256, 256, vk::Format::R8G8B8A8_UNORM, @@ -1358,20 +1450,22 @@ fn record_graphic_wont_merge(frame: &mut FrameContext, pool: &mut HashPool) { // These two passes have common writes but are otherwise regular - they won't get merged frame - .render_graph - .begin_pass("c") + .graph + .begin_cmd() + .debug_name("c") .bind_pipeline(&pipeline) - .store_color(0, image) - .record_subpass(|subpass, _| { - subpass.draw(1, 1, 0, 0); + .color_attachment_image(0, image, LoadOp::DontCare, StoreOp::Store) + .record_cmd(|cmd| { + cmd.draw(1, 1, 0, 0); }); frame - .render_graph - .begin_pass("d") + .graph + .begin_cmd() + .debug_name("d") .bind_pipeline(&pipeline) - .store_color(0, image) - .record_subpass(|subpass, _| { - subpass.draw(1, 1, 0, 0); + .color_attachment_image(0, image, LoadOp::DontCare, StoreOp::Store) + .record_cmd(|cmd| { + cmd.draw(1, 1, 0, 0); }); } @@ -1379,19 +1473,20 @@ fn record_transfer_graphic_multipass(frame: &mut FrameContext, pool: &mut HashPo let pipeline = graphic_vert_frag_pipeline( frame.device, GraphicPipelineInfo::default(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(vertex) void main() { } - "#, - vert + "# ) .as_slice(), - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(fragment) layout(binding = 0) uniform sampler2D my_sampler_lle; @@ -1400,14 +1495,13 @@ fn record_transfer_graphic_multipass(frame: &mut FrameContext, pool: &mut HashPo void main() { color_out = texture(my_sampler_lle, vec2(0)); } - "#, - frag + "# ) .as_slice(), ); let images = [ - frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( 256, 256, vk::Format::R8G8B8A8_UNORM, @@ -1415,8 +1509,8 @@ fn record_transfer_graphic_multipass(frame: &mut FrameContext, pool: &mut HashPo )) .unwrap(), ), - frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( 256, 256, vk::Format::R8G8B8A8_UNORM, @@ -1426,30 +1520,43 @@ fn record_transfer_graphic_multipass(frame: &mut FrameContext, pool: &mut HashPo ), ]; - frame.render_graph.clear_color_image(images[0]); - frame.render_graph.clear_color_image(images[1]); + frame.graph.clear_color_image(images[0], [0f32; 4]); + frame.graph.clear_color_image(images[1], [0f32; 4]); // a and b should merge into one renderpass with two subpasses; however the use of images[1] in // b should have a pipeline barrier (on the clear we just did) before the pass starts. frame - .render_graph - .begin_pass("a") + .graph + .begin_cmd() + .debug_name("a") .bind_pipeline(&pipeline) - .clear_color(0, frame.swapchain_image) - .store_color(0, frame.swapchain_image) - .read_descriptor(0, images[0]) - .record_subpass(|subpass, _| { - subpass.draw(1, 1, 0, 0); + .color_attachment_image( + 0, + frame.swapchain_image, + LoadOp::CLEAR_BLACK_ALPHA_ZERO, + StoreOp::Store, + ) + .shader_resource_access( + 0, + images[0], + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + .record_cmd(|cmd| { + cmd.draw(1, 1, 0, 0); }); frame - .render_graph - .begin_pass("b") + .graph + .begin_cmd() + .debug_name("b") .bind_pipeline(&pipeline) - .load_color(0, frame.swapchain_image) - .store_color(0, frame.swapchain_image) - .read_descriptor(0, images[1]) - .record_subpass(|subpass, _| { - subpass.draw(1, 1, 0, 0); + .color_attachment_image(0, frame.swapchain_image, LoadOp::Load, StoreOp::Store) + .shader_resource_access( + 0, + images[1], + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + .record_cmd(|cmd| { + cmd.draw(1, 1, 0, 0); }); } @@ -1457,31 +1564,30 @@ fn record_transfer_graphic_multipass(frame: &mut FrameContext, pool: &mut HashPo fn compute_pipeline( key: &'static str, - device: &Arc, + device: &Device, info: impl Into, shader: impl Into, -) -> Arc { +) -> ComputePipeline { use std::{cell::RefCell, collections::HashMap}; thread_local! { - static TLS: RefCell>> = Default::default(); + static TLS: RefCell> = Default::default(); } TLS.with(|tls| { - Arc::clone( - tls.borrow_mut().entry(key).or_insert_with(|| { - Arc::new(ComputePipeline::create(device, info, shader).unwrap()) - }), - ) + tls.borrow_mut() + .entry(key) + .or_insert_with(|| ComputePipeline::create(device, info, shader).unwrap()) + .clone() }) } fn graphic_vert_frag_pipeline( - device: &Arc, + device: &Device, info: impl Into, vert_source: &'static [u32], frag_source: &'static [u32], -) -> Arc { +) -> GraphicPipeline { use std::{cell::RefCell, collections::HashMap}; #[derive(Eq, Hash, PartialEq)] @@ -1492,33 +1598,30 @@ fn graphic_vert_frag_pipeline( } thread_local! { - static TLS: RefCell>> = Default::default(); + static TLS: RefCell> = Default::default(); } let info = info.into(); TLS.with(|tls| { - Arc::clone( - tls.borrow_mut() - .entry(Key { + tls.borrow_mut() + .entry(Key { + info, + vert_source, + frag_source, + }) + .or_insert_with(move || { + GraphicPipeline::create( + device, info, - vert_source, - frag_source, - }) - .or_insert_with(move || { - Arc::new( - GraphicPipeline::create( - device, - info, - [ - Shader::new_vertex(vert_source), - Shader::new_fragment(frag_source), - ], - ) - .unwrap(), - ) - }), - ) + [ + Shader::new_vertex(vert_source), + Shader::new_fragment(frag_source), + ], + ) + .unwrap() + }) + .clone() }) } diff --git a/examples/getting-started.md b/examples/getting-started.md deleted file mode 100644 index be20a759..00000000 --- a/examples/getting-started.md +++ /dev/null @@ -1,54 +0,0 @@ -# Getting Started with _Screen 13_ - -This guide is intended for developers who are new to _Screen 13_ and want a step-by-step introduction. For further details on these -topics refer to the online [documentation](https://docs.rs/screen-13/latest/screen_13/). - -## Required Packages - -_Linux (Debian-like)_: -- `sudo apt install cmake uuid-dev libfontconfig-dev libssl-dev` - -_Mac OS (10.15 or later)_: -- Xcode 12 -- Python 2.7 -- `brew install cmake ossp-uuid` - -_Windows_: -- TODO (works but I haven't gathered the requirements) - -## Documentation - -Read the generated [documentation](https://docs.rs/screen-13/latest/screen_13/) online, or run the -following command locally: - -``` -cargo doc --open -``` - -## Changes - -Stay informed of recent changes to _Screen 13_ using the -[change log](https://github.com/attackgoat/screen-13/blob/master/CHANGELOG.md) file. - -## Performance Profiling - -Most of the example code (_the ones which use an `EventLoop`_) support profiling using `puffin`. To -use it, first install and run `puffin_viewer` and then run the example with the -`--features profile-with-puffin` and `--release` flags. - -You may need to disable CPU thermal throttling in order to get consistent results on some platforms. -The inconsistent results are certainly valid, but they do not help in accurately measuring potential -changes. This may be done on Intel Linux machines by modifying the Intel P-State driver: - -```bash -echo 100 | sudo tee /sys/devices/system/cpu/intel_pstate/min_perf_pct -``` - -(_[Source](https://www.kernel.org/doc/Documentation/cpu-freq/intel-pstate.txt)_) - -## Helpful tools - -- [VulkanSDK](https://vulkan.lunarg.com/sdk/home) _(Required when calling `EventLoop::debug(true)`)_ -- NVIDIA: [nvidia-smi](https://developer.nvidia.com/nvidia-system-management-interface) -- AMD: [RadeonTop](https://github.com/clbr/radeontop) -- [RenderDoc](https://renderdoc.org/) diff --git a/examples/image_sampler.rs b/examples/image_sampler.rs index e64d16b8..364b0185 100644 --- a/examples/image_sampler.rs +++ b/examples/image_sampler.rs @@ -1,15 +1,30 @@ mod profile_with_puffin; use { + ash::vk, clap::Parser, hassle_rs::compile_hlsl, - inline_spirv::inline_spirv, - screen_13::prelude::*, - screen_13_window::WindowBuilder, + log::info, std::{ path::{Path, PathBuf}, sync::Arc, + time::Instant, }, + vk_graph::{ + Graph, + cmd::{LoadOp, StoreOp}, + driver::{ + buffer::Buffer, + device::Device, + graphic::{GraphicPipeline, GraphicPipelineInfo}, + image::{Image, ImageInfo}, + shader::{SamplerInfo, Shader}, + }, + pool::hash::HashPool, + }, + vk_graph_window::Window, + vk_shader_macros::glsl, + vk_sync::AccessType, }; /// Displays a sequence of image samplers. @@ -31,49 +46,69 @@ fn main() -> anyhow::Result<()> { profile_with_puffin::init(); let args = Args::parse(); - let window = WindowBuilder::default().debug(args.debug).build()?; + let window = Window::builder().debug(args.debug).build()?; let gulf_image = read_image(&window.device, "examples/res/image/gulf.jpg")?; // Sampler info contains the full definition of Vulkan sampler settings using a builder struct - let edge_edge = SamplerInfoBuilder::default() + let edge_edge = SamplerInfo::builder() .address_mode_u(vk::SamplerAddressMode::CLAMP_TO_EDGE) .address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE); - let border_edge_black = SamplerInfoBuilder::default() + let border_edge_black = SamplerInfo::builder() .address_mode_u(vk::SamplerAddressMode::CLAMP_TO_BORDER) .address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE) .border_color(vk::BorderColor::FLOAT_OPAQUE_BLACK); - let edge_border_white = SamplerInfoBuilder::default() + let edge_border_white = SamplerInfo::builder() .address_mode_u(vk::SamplerAddressMode::CLAMP_TO_EDGE) .address_mode_v(vk::SamplerAddressMode::CLAMP_TO_BORDER) .border_color(vk::BorderColor::FLOAT_OPAQUE_WHITE); // Image samplers are part of the shader pipeline and so we will create three pipelines total - let pipelines = [edge_edge, border_edge_black, edge_border_white] + let pipeline_modes = [ + ("edge_edge", edge_edge), + ("border_edge_black", border_edge_black), + ("edge_border_white", edge_border_white), + ]; + let pipelines = pipeline_modes .into_iter() - .map(|sampler_info| create_pipeline(&window.device, sampler_info)) + .map(|(_, sampler_info)| create_pipeline(&window.device, sampler_info)) .collect::, _>>()?; + let pipeline_names = pipeline_modes.map(|(name, _)| name); let mut pipeline_index = 0; let mut pipeline_time = 0.0; + let mut prev_frame_at = Instant::now(); + + info!("active sampler mode: {}", pipeline_names[pipeline_index]); window.run(|frame| { + let now = Instant::now(); + + let dt = now - prev_frame_at; + prev_frame_at = now; + // Periodically change the active pipeline index - pipeline_time += 0.016; - if pipeline_time > 2.0 { + pipeline_time += dt.as_secs_f32(); + if pipeline_time > 5.0 { pipeline_time = 0.0; pipeline_index += 1; pipeline_index %= pipelines.len(); + info!("active sampler mode: {}", pipeline_names[pipeline_index]); } // Draw gulf.jpg using the active pipeline - let gulf_image = frame.render_graph.bind_node(&gulf_image); + let gulf_image = frame.graph.bind_resource(&gulf_image); frame - .render_graph - .begin_pass("Draw gulf image to swapchain") + .graph + .begin_cmd() + .debug_name("Draw gulf image to swapchain") .bind_pipeline(&pipelines[pipeline_index]) - .read_descriptor(0, gulf_image) - .store_color(0, frame.swapchain_image) - .record_subpass(|subpass, _| { - subpass.draw(3, 1, 0, 0); + .shader_resource_access( + 0, + gulf_image, + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + .color_attachment_image(0, frame.swapchain_image, LoadOp::DontCare, StoreOp::Store) + .record_cmd(|cmd| { + cmd.draw(3, 1, 0, 0); }); })?; @@ -81,86 +116,94 @@ fn main() -> anyhow::Result<()> { } fn create_pipeline( - device: &Arc, + device: &Device, sampler_info: impl Into, -) -> anyhow::Result> { +) -> anyhow::Result { let args = Args::parse(); let mut frag_shader = match (args.hlsl, args.separate) { (true, true) => { // HLSL separate image sampler Shader::new_fragment( - inline_spirv!( + compile_hlsl( + "fragment.hlsl", r#" - struct FullscreenVertexOutput - { - float4 position : SV_Position; - [[vk::location(0)]] float2 uv : TEXCOORD0; - }; - - [[vk::binding(0, 0)]] Texture2D screenTexture : register(t0); - [[vk::binding(1, 0)]] SamplerState textureSampler : register(s0); - - float4 main(FullscreenVertexOutput input) - : SV_Target - { - return screenTexture.Sample(textureSampler, input.uv); - } - "#, - frag, - hlsl - ) + struct FullscreenVertexOutput + { + float4 position : SV_Position; + [[vk::location(0)]] float2 uv : TEXCOORD0; + }; + + [[vk::binding(0, 0)]] Texture2D screenTexture : register(t0); + [[vk::binding(1, 0)]] SamplerState textureSampler : register(s1); + + float4 main(FullscreenVertexOutput input) + : SV_Target + { + return screenTexture.Sample(textureSampler, input.uv); + } + "#, + "main", + "ps_5_0", + &["-spirv"], + &[], + )? .as_slice(), ) } (true, false) => { - // HLSL combined image sampler: inline_spirv uses shaderc which does not support this, so - // we are using hassle_rs which uses dxc. You must follow the instructions listed here to - // use hassle_rs: + // HLSL combined image sampler: include_glsl uses shaderc which does not support this, + // so we are using hassle_rs which uses dxc. You must follow the + // instructions listed here to use hassle_rs: // See: https://github.com/Traverse-Research/hassle-rs // See: https://github.com/microsoft/DirectXShaderCompiler/wiki/Vulkan-combined-image-sampler-type // See: https://github.com/google/shaderc/issues/1310 Shader::new_fragment( - compile_hlsl( - "fragment.hlsl", - r#" - struct FullscreenVertexOutput - { - float4 position : SV_Position; - [[vk::location(0)]] float2 uv : TEXCOORD0; - }; - - [[vk::combinedImageSampler]][[vk::binding(0, 0)]] Texture2D screenTexture : register(t0); - [[vk::combinedImageSampler]][[vk::binding(0, 0)]] SamplerState textureSampler : register(s0); - - float4 main(FullscreenVertexOutput input) - : SV_Target - { - return screenTexture.Sample(textureSampler, input.uv); - } - "#, - "main", "ps_5_0", &["-spirv"], &[], - )? - .as_slice(), - ) + compile_hlsl( + "fragment.hlsl", + r#" + struct FullscreenVertexOutput + { + float4 position : SV_Position; + [[vk::location(0)]] float2 uv : TEXCOORD0; + }; + + [[vk::combinedImageSampler]][[vk::binding(0, 0)]] + Texture2D screenTexture : register(t0); + [[vk::combinedImageSampler]][[vk::binding(0, 0)]] + SamplerState textureSampler : register(s0); + + float4 main(FullscreenVertexOutput input) + : SV_Target + { + return screenTexture.Sample(textureSampler, input.uv); + } + "#, + "main", + "ps_5_0", + &["-spirv"], + &[], + )? + .as_slice(), + ) } (false, true) => { // GLSL separate image sampler Shader::new_fragment( - inline_spirv!( + glsl!( r#" - #version 460 core - - layout(binding = 0) uniform texture2D image; - layout(binding = 1) uniform sampler image_sampler; - layout(location = 0) in vec2 vk_TexCoord; - layout(location = 0) out vec4 vk_Color; - - void main() { - vk_Color = texture(sampler2D(image, image_sampler), vk_TexCoord); - } - "#, - frag + #version 460 core + #pragma shader_stage(fragment) + + layout(binding = 0) uniform texture2D image; + layout(binding = 1) uniform sampler image_sampler; + layout(location = 0) in vec2 vk_TexCoord; + layout(location = 0) out vec4 vk_Color; + + void main() { + vk_Color = texture(sampler2D(image, image_sampler), vk_TexCoord); + } + "# ) .as_slice(), ) @@ -168,9 +211,10 @@ fn create_pipeline( (false, false) => { // GLSL combined image sampler Shader::new_fragment( - inline_spirv!( + glsl!( r#" - #version 460 core + #version 460 core + #pragma shader_stage(fragment) layout(binding = 0) uniform sampler2D image; layout(location = 0) in vec2 vk_TexCoord; @@ -179,8 +223,7 @@ fn create_pipeline( void main() { vk_Color = texture(image, vk_TexCoord); } - "#, - frag + "# ) .as_slice(), ) @@ -192,14 +235,15 @@ fn create_pipeline( let sampler_binding = args.separate as u32; frag_shader = frag_shader.image_sampler(sampler_binding, sampler_info); - Ok(Arc::new(GraphicPipeline::create( + Ok(GraphicPipeline::create( device, GraphicPipelineInfo::default(), [ Shader::new_vertex( - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(vertex) const vec2[3] VERTICES = { vec2(-1, -1), @@ -213,18 +257,17 @@ fn create_pipeline( gl_Position = vec4(VERTICES[gl_VertexIndex], 0, 1); vk_TexCoord = 0.75 * gl_Position.xy + vec2(0.5); } - "#, - vert + "# ) .as_slice(), ), frag_shader, ], - )?)) + )?) } -fn read_image(device: &Arc, path: impl AsRef) -> anyhow::Result> { - // For another way to loading images, see screen_13_fx::ImageLoader +fn read_image(device: &Device, path: impl AsRef) -> anyhow::Result> { + // For another way to loading images, see vk_graph_fx::ImageLoader let gulf_jpg = image::open(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path))?; let image = Arc::new(Image::create( device, @@ -237,17 +280,17 @@ fn read_image(device: &Arc, path: impl AsRef) -> anyhow::Result Result<(), WindowError> { pretty_env_logger::init(); profile_with_puffin::init(); - // Screen 13 things we need for this demo + // vk-graph things we need for this demo let args = Args::parse(); - let window = WindowBuilder::default() + let window = Window::builder() .debug(args.debug) .v_sync(false) .window(|window| window.with_inner_size(LogicalSize::new(1024, 768))) @@ -27,11 +32,16 @@ fn main() -> Result<(), WindowError> { // Some example state to make the demo more interesting let mut value = 0; let choices = ["test test this is 1", "test test this is 2"]; + let mut prev_frame_at = Instant::now(); window.run(|frame| { + let now = Instant::now(); + let dt = now - prev_frame_at; + prev_frame_at = now; + // Lease and clear an image as a stand-in for some real game or program output - let app_image = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let app_image = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( frame.width, frame.height, vk::Format::R8G8B8A8_UNORM, @@ -42,16 +52,16 @@ fn main() -> Result<(), WindowError> { .unwrap(), ); frame - .render_graph - .clear_color_image_value(app_image, [0.2, 0.22, 0.2, 1.0]); + .graph + .clear_color_image(app_image, [0.2, 0.22, 0.2, 1.0]); // Use the draw function callback to do some fun meant-for-debug-mode GUI stuff let gui_image = imgui.draw( - 0.016, + dt.as_secs_f32(), frame.events, frame.window, &mut pool, - frame.render_graph, + frame.graph, |ui, _, _| { ui.window("Hello world") .position([10.0, 10.0], Condition::FirstUseEver) @@ -76,12 +86,7 @@ fn main() -> Result<(), WindowError> { ); // Present "gui_image" on top of "app_image" onto "frame.swapchain" - display.present_images( - frame.render_graph, - gui_image, - app_image, - frame.swapchain_image, - ); + display.present_images(frame.graph, gui_image, app_image, frame.swapchain_image); }) } diff --git a/examples/min_max.rs b/examples/min_max.rs index cb26019f..1a886853 100644 --- a/examples/min_max.rs +++ b/examples/min_max.rs @@ -1,10 +1,24 @@ use { + ash::vk, bytemuck::cast_slice, clap::Parser, - inline_spirv::inline_spirv, log::warn, - screen_13::prelude::*, std::{mem::size_of, sync::Arc}, + vk_graph::{ + Graph, + driver::{ + DriverError, + buffer::{Buffer, BufferInfo}, + compute::{ComputePipeline, ComputePipelineInfo}, + device::{Device, DeviceInfo}, + image::{Image, ImageInfo}, + shader::{SamplerInfo, Shader}, + }, + node::ImageNode, + pool::hash::HashPool, + }, + vk_shader_macros::glsl, + vk_sync::AccessType, }; // Min/max sampler reduction is commonly used to create depth buffer mip-maps for use with gpu-based @@ -19,10 +33,10 @@ use { fn main() -> Result<(), DriverError> { pretty_env_logger::init(); - let mut render_graph = RenderGraph::new(); + let mut graph = Graph::default(); let args = Args::parse(); - let device_info = DeviceInfoBuilder::default().debug(args.debug); - let device = Arc::new(Device::create_headless(device_info)?); + let device_info = DeviceInfo::builder().debug(args.debug); + let device = Device::create(device_info)?; let size = 4; // The 4x4 depth image will have pixels that look like this: @@ -30,29 +44,29 @@ fn main() -> Result<(), DriverError> { // 4.0 5.0 6.0 7.0 // 8.0 9.0 10.0 11.0 // 12.0 13.0 14.0 15.0 - let depth_image = fill_depth_image(&device, &mut render_graph, size)?; + let depth_image = fill_depth_image(&device, &mut graph, size)?; // These 2x2 reduced images have undefined data until we wait on the results later let min_reduced_image = reduce_depth_image( &device, - &mut render_graph, + &mut graph, depth_image, vk::SamplerReductionMode::MIN, )?; let max_reduced_image = reduce_depth_image( &device, - &mut render_graph, + &mut graph, depth_image, vk::SamplerReductionMode::MAX, )?; // Create result buffers so we can read back the results - let min_result_buf = copy_image_to_buffer(&device, &mut render_graph, min_reduced_image)?; - let max_result_buf = copy_image_to_buffer(&device, &mut render_graph, max_reduced_image)?; + let min_result_buf = copy_image_to_buffer(&device, &mut graph, min_reduced_image)?; + let max_result_buf = copy_image_to_buffer(&device, &mut graph, max_reduced_image)?; - render_graph - .resolve() - .submit(&mut HashPool::new(&device), 0, 0)? + graph + .into_submission() + .queue_submit(&mut HashPool::new(&device), 0, 0)? .wait_until_executed()?; // For each image we have reduced each 2x2 pixel group into the min/max values of each group @@ -86,8 +100,8 @@ fn main() -> Result<(), DriverError> { } fn fill_depth_image( - device: &Arc, - render_graph: &mut RenderGraph, + device: &Device, + graph: &mut Graph, size: u32, ) -> Result { let info = ImageInfo::image_2d( @@ -107,7 +121,7 @@ fn fill_depth_image( // Sometimes required because support is not 100% common: Check min/max reduction support // https://vulkan.gpuinfo.org/listdevicescoverage.php?extension=VK_EXT_sampler_filter_minmax&platform=all - let fmt_props = Device::format_properties(device, fmt); + let fmt_props = device.physical_device.format_properties(fmt); if !fmt_props.optimal_tiling_features.contains( vk::FormatFeatureFlags::SAMPLED_IMAGE | vk::FormatFeatureFlags::SAMPLED_IMAGE_FILTER_LINEAR @@ -129,7 +143,9 @@ fn fill_depth_image( ); // Not required, but good practice: Check image format support - let image_fmt_props = Device::image_format_properties(device, fmt, ty, tiling, usage, flags)? + let image_fmt_props = device + .physical_device + .image_format_properties(fmt, ty, tiling, usage, flags)? .ok_or(DriverError::Unsupported)?; if size > image_fmt_props.max_extent.width || size > image_fmt_props.max_extent.height { // In this case you might use a smaller image @@ -142,24 +158,24 @@ fn fill_depth_image( // device.physical_device.sampler_filter_minmax_properties.image_component_mapping let depth_data = (0..size.pow(2)).map(|x| x as f32).collect::>(); - let depth_data = render_graph.bind_node(Buffer::create_from_slice( + let depth_data = graph.bind_resource(Buffer::create_from_slice( device, vk::BufferUsageFlags::TRANSFER_SRC, cast_slice(&depth_data), )?); - let depth_image = render_graph.bind_node(Image::create(device, info)?); - render_graph.copy_buffer_to_image(depth_data, depth_image); + let depth_image = graph.bind_resource(Image::create(device, info)?); + graph.copy_buffer_to_image(depth_data, depth_image); Ok(depth_image) } fn reduce_depth_image( - device: &Arc, - render_graph: &mut RenderGraph, + device: &Device, + graph: &mut Graph, depth_image: ImageNode, reduction_mode: vk::SamplerReductionMode, ) -> Result { - let depth_info = render_graph.node_info(depth_image); + let depth_info = graph.resource(depth_image).info; assert_eq!(depth_info.width, depth_info.height); @@ -171,16 +187,19 @@ fn reduce_depth_image( vk::Format::R32_SFLOAT, vk::ImageUsageFlags::STORAGE | vk::ImageUsageFlags::TRANSFER_SRC, ); - let reduced_image = render_graph.bind_node(Image::create(device, reduced_info)?); + let reduced_image = graph.bind_resource(Image::create(device, reduced_info)?); - render_graph - .begin_pass("Reduce depth image") - .bind_pipeline(&Arc::new(ComputePipeline::create( + graph + .begin_cmd() + .debug_name("Reduce depth image") + .bind_pipeline(ComputePipeline::create( device, ComputePipelineInfo::default(), Shader::new_compute( - inline_spirv!( - r#"#version 460 core + glsl!( + r#" + #version 460 core + #pragma shader_stage(compute) layout(binding = 0) uniform sampler2D depth_image; layout(binding = 1) writeonly uniform image2D reduced_image; @@ -192,38 +211,38 @@ fn reduce_depth_image( ivec2 store_xy = ivec2(gl_GlobalInvocationID.xy); imageStore(reduced_image, store_xy, sample_val); - }"#, - comp + } + "# ) .as_slice(), ) .image_sampler(0, SamplerInfo::LINEAR.reduction_mode(reduction_mode)), - )?)) - .read_descriptor(0, depth_image) - .write_descriptor(1, reduced_image) - .record_compute(move |compute, _| { - compute.dispatch(reduced_info.width, reduced_info.height, 1); + )?) + .shader_resource_access(0, depth_image, AccessType::ComputeShaderReadOther) + .shader_resource_access(1, reduced_image, AccessType::ComputeShaderWrite) + .record_cmd(move |cmd| { + cmd.dispatch(reduced_info.width, reduced_info.height, 1); }); Ok(reduced_image) } fn copy_image_to_buffer( - device: &Arc, - render_graph: &mut RenderGraph, + device: &Device, + graph: &mut Graph, reduced_image: ImageNode, ) -> Result, DriverError> { - let reduced_info = render_graph.node_info(reduced_image); + let reduced_info = graph.resource(reduced_image).info; let result_len = (reduced_info.width * reduced_info.height) as vk::DeviceSize * size_of::() as vk::DeviceSize; - let result_buf = render_graph.bind_node(Buffer::create( + let result_buf = graph.bind_resource(Buffer::create( device, BufferInfo::host_mem(result_len, vk::BufferUsageFlags::TRANSFER_DST), )?); - render_graph.copy_image_to_buffer(reduced_image, result_buf); + graph.copy_image_to_buffer(reduced_image, result_buf); - Ok(render_graph.unbind_node(result_buf)) + Ok(graph.resource(result_buf).clone()) } #[derive(Parser)] diff --git a/examples/mip_compute.rs b/examples/mip_compute.rs index a75c3a9b..8894f066 100644 --- a/examples/mip_compute.rs +++ b/examples/mip_compute.rs @@ -1,8 +1,23 @@ mod profile_with_puffin; use { - bytemuck::cast_slice, clap::Parser, inline_spirv::inline_spirv, screen_13::prelude::*, - std::sync::Arc, + ash::vk, + bytemuck::cast_slice, + clap::Parser, + vk_graph::{ + Graph, + driver::{ + DriverError, + buffer::{Buffer, BufferInfo}, + compute::{ComputePipeline, ComputePipelineInfo}, + device::{Device, DeviceInfo}, + image::{Image, ImageInfo}, + shader::{SamplerInfo, Shader}, + }, + pool::hash::HashPool, + }, + vk_shader_macros::glsl, + vk_sync::AccessType, }; /// This program demonstrates a single render pass which uses multiple executions to record a chain @@ -16,12 +31,12 @@ fn main() -> Result<(), DriverError> { profile_with_puffin::init(); let args = Args::parse(); - let device_info = DeviceInfoBuilder::default().debug(args.debug); - let device = Arc::new(Device::create_headless(device_info)?); + let device_info = DeviceInfo::builder().debug(args.debug); + let device = Device::create(device_info)?; - let mut render_graph = RenderGraph::new(); + let mut graph = Graph::default(); - let depth_pyramid = render_graph.bind_node(Image::create( + let depth_pyramid = graph.bind_resource(Image::create( &device, ImageInfo::image_2d( 4, @@ -32,14 +47,14 @@ fn main() -> Result<(), DriverError> { | vk::ImageUsageFlags::TRANSFER_SRC | vk::ImageUsageFlags::TRANSFER_DST, ) - .to_builder() + .into_builder() .mip_level_count(3), )?); - let depth_info = render_graph.node_info(depth_pyramid); + let depth_info = graph.resource(depth_pyramid).info; // You would normally create this buffer by copying the depth attachment image #[allow(clippy::inconsistent_digit_grouping)] - let depth_buf = render_graph.bind_node(Buffer::create_from_slice( + let depth_buf = graph.bind_resource(Buffer::create_from_slice( &device, vk::BufferUsageFlags::TRANSFER_SRC, cast_slice(&[ @@ -49,17 +64,19 @@ fn main() -> Result<(), DriverError> { [13.0__, 14.0, 15.0, 16.0], ]), )?); - render_graph.copy_buffer_to_image(depth_buf, depth_pyramid); + graph.copy_buffer_to_image(depth_buf, depth_pyramid); - let mut pass = render_graph - .begin_pass("update depth pyramid") + let mut pass = graph + .begin_cmd() + .debug_name("update depth pyramid") .bind_pipeline(ComputePipeline::create( &device, ComputePipelineInfo::default(), Shader::new_compute( - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(compute) layout(binding = 0) uniform sampler2D src_mip; layout(binding = 1, r32f) writeonly uniform image2D dst_mip; @@ -68,15 +85,13 @@ fn main() -> Result<(), DriverError> { vec4 depth = texture(src_mip, vec2(gl_GlobalInvocationID.xy << 1) + 1.0); imageStore(dst_mip, ivec2(gl_GlobalInvocationID.xy), depth); } - "#, - comp, - vulkan1_2 + "# ) .as_slice(), ) .image_sampler( 0, - SamplerInfoBuilder::default() + SamplerInfo::builder() .address_mode_u(vk::SamplerAddressMode::CLAMP_TO_EDGE) .address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE) .mag_filter(vk::Filter::LINEAR) @@ -88,26 +103,28 @@ fn main() -> Result<(), DriverError> { for mip_level in 1..depth_info.mip_level_count { pass = pass - .read_descriptor_as( + .shader_subresource_access( 0, depth_pyramid, depth_info - .default_view_info() - .to_builder() + .into_image_view() + .into_builder() .base_mip_level(mip_level - 1) .mip_level_count(1), + AccessType::ComputeShaderReadOther, ) - .write_descriptor_as( + .shader_subresource_access( 1, depth_pyramid, depth_info - .default_view_info() - .to_builder() + .into_image_view() + .into_builder() .base_mip_level(mip_level) .mip_level_count(1), + AccessType::ComputeShaderWrite, ) - .record_compute(move |compute, _| { - compute.dispatch( + .record_cmd(move |cmd| { + cmd.dispatch( depth_info.width >> mip_level, depth_info.height >> mip_level, 1, @@ -115,14 +132,14 @@ fn main() -> Result<(), DriverError> { }); } - let depth_pixel = render_graph.bind_node(Buffer::create( + let depth_pixel = graph.bind_resource(Buffer::create( &device, BufferInfo::host_mem(size_of::() as _, vk::BufferUsageFlags::TRANSFER_DST), )?); - render_graph.copy_image_to_buffer_region( + graph.copy_image_to_buffer_region( depth_pyramid, depth_pixel, - vk::BufferImageCopy { + [vk::BufferImageCopy { buffer_offset: 0, buffer_row_length: 1, buffer_image_height: 1, @@ -138,14 +155,14 @@ fn main() -> Result<(), DriverError> { height: 1, depth: 1, }, - }, + }], ); - let depth_pixel = render_graph.unbind_node(depth_pixel); + let depth_pixel = graph.resource(depth_pixel).clone(); - render_graph - .resolve() - .submit(&mut HashPool::new(&device), 0, 0)? + graph + .into_submission() + .queue_submit(&mut HashPool::new(&device), 0, 0)? .wait_until_executed()?; let depth_pixel = f32::from_ne_bytes(Buffer::mapped_slice(&depth_pixel).try_into().unwrap()); diff --git a/examples/mip_graphic.rs b/examples/mip_graphic.rs index 9698d0df..788a20eb 100644 --- a/examples/mip_graphic.rs +++ b/examples/mip_graphic.rs @@ -1,14 +1,27 @@ mod profile_with_puffin; use { + ash::vk, bytemuck::{Pod, Zeroable, bytes_of}, clap::Parser, core::f32, glam::{Vec4, vec3}, - inline_spirv::inline_spirv, - screen_13::prelude::*, - screen_13_window::{WindowBuilder, WindowError}, std::sync::Arc, + vk_graph::{ + Graph, + cmd::{LoadOp, StoreOp}, + driver::{ + DriverError, + device::Device, + graphic::{GraphicPipeline, GraphicPipelineInfo}, + image::{Image, ImageInfo}, + shader::{SamplerInfoBuilder, Shader}, + }, + pool::lazy::LazyPool, + }, + vk_graph_window::{Window, WindowError}, + vk_shader_macros::glsl, + vk_sync::AccessType, }; // TODO: Add texelFetch option @@ -18,7 +31,7 @@ fn main() -> Result<(), WindowError> { profile_with_puffin::init(); let args = Args::parse(); - let window = WindowBuilder::default().debug(args.debug).build()?; + let window = Window::builder().debug(args.debug).build()?; let size = 237u32; let mip_level_count = size.ilog2(); @@ -31,7 +44,7 @@ fn main() -> Result<(), WindowError> { vk::Format::R8G8B8A8_UNORM, vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::SAMPLED, ) - .to_builder() + .into_builder() .mip_level_count(mip_level_count) .build(); let image = Arc::new(Image::create(&window.device, image_info)?); @@ -46,59 +59,76 @@ fn main() -> Result<(), WindowError> { // https://vulkan.gpuinfo.org/listsurfaceusageflags.php assert!( frame - .render_graph - .node_info(frame.swapchain_image) + .graph + .resource(frame.swapchain_image) + .info .usage .contains(vk::ImageUsageFlags::COLOR_ATTACHMENT) ); - let image = frame.render_graph.bind_node(&image); - let swapchain_info = frame.render_graph.node_info(frame.swapchain_image); + let image = frame.graph.bind_resource(&image); + let swapchain_info = frame.graph.resource(frame.swapchain_image).info; let stripe_width = swapchain_info.width / mip_level_count; - let mut pass = frame - .render_graph - .begin_pass("splat mips") + let mut cmd = frame + .graph + .begin_cmd() + .debug_name("splat mips") .bind_pipeline(&splat); for mip_level in 0..mip_level_count { let stripe_x = mip_level * stripe_width; - pass = pass - .read_descriptor_as( + let load_op = if mip_level == 0 { + LoadOp::CLEAR_BLACK_ALPHA_ZERO + } else { + LoadOp::Load + }; + cmd = cmd + .shader_subresource_access( 0, image, image_info - .default_view_info() - .to_builder() + .into_image_view() + .into_builder() .base_mip_level(mip_level) .mip_level_count(1), + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, ) - .load_color(0, frame.swapchain_image) - .store_color(0, frame.swapchain_image) - .set_render_area(stripe_x as _, 0, stripe_width, swapchain_info.height) - .record_subpass(|subpass, _| { - subpass.draw(6, 1, 0, 0); + .color_attachment_image(0, frame.swapchain_image, load_op, StoreOp::Store) + .render_area(vk::Rect2D { + offset: vk::Offset2D { + x: stripe_x as _, + y: 0, + }, + extent: vk::Extent2D { + width: stripe_width, + height: swapchain_info.height, + }, + }) + .record_cmd(|cmd| { + cmd.draw(6, 1, 0, 0); }); } }) } -fn fill_mip_levels(device: &Arc, image: &Arc) -> Result<(), DriverError> { - #[derive(Clone, Copy, Pod, Zeroable)] +fn fill_mip_levels(device: &Device, image: &Arc) -> Result<(), DriverError> { #[repr(C)] + #[derive(Clone, Copy, Pod, Zeroable)] struct PushConstants { a: Vec4, b: Vec4, } - let vertical_gradient = Arc::new(GraphicPipeline::create( + let vertical_gradient = GraphicPipeline::create( device, GraphicPipelineInfo::default(), [ Shader::new_vertex( - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(vertex) const vec2 POSITION[] = { vec2(-1, -1), @@ -116,15 +146,15 @@ fn fill_mip_levels(device: &Arc, image: &Arc) -> Result<(), Drive ab = max(position.y, 0); gl_Position = vec4(position, 0, 1); } - "#, - vert + "# ) .as_slice(), ), Shader::new_fragment( - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(fragment) layout(push_constant) uniform PushConstants { layout(offset = 0) vec3 a; @@ -137,41 +167,45 @@ fn fill_mip_levels(device: &Arc, image: &Arc) -> Result<(), Drive void main() { color = vec4(mix(a, b, ab), 1); } - "#, - frag + "# ) .as_slice(), ), ], - )?); + )?; - let mut render_graph = RenderGraph::new(); + let mut graph = Graph::default(); let image_info = image.info; - let image = render_graph.bind_node(image); + let image = graph.bind_resource(image); // NOTE: Each pass writes to a different mip level, and so although it's the same image they are // unable to be used as a single pass so we must call begin_pass for each. Without starting a // new pass for each level the Vulkan framebuffer would be set to the size of the first image. for mip_level in 0..image_info.mip_level_count { - render_graph - .begin_pass("fill mip levels") + graph + .begin_cmd() + .debug_name("fill mip levels") .bind_pipeline(&vertical_gradient) - .store_color_as( + .color_attachment_image_view( 0, image, image_info - .default_view_info() - .to_builder() + .into_image_view() + .into_builder() .base_mip_level(mip_level) .mip_level_count(1), + LoadOp::DontCare, + StoreOp::Store, ) - .record_subpass(|subpass, _| { - subpass - .push_constants(bytes_of(&PushConstants { + .record_cmd(|cmd| { + cmd.push_constants( + 0, + bytes_of(&PushConstants { a: vec3(0.0, 1.0, 1.0).extend(f32::NAN), b: vec3(1.0, 0.0, 1.0).extend(f32::NAN), - })) - .draw(6, 1, 0, 0); + }), + ) + .draw(6, 1, 0, 0); }); } @@ -185,26 +219,27 @@ fn fill_mip_levels(device: &Arc, image: &Arc) -> Result<(), Drive family .queue_flags .contains(vk::QueueFlags::GRAPHICS) - .then_some(idx) + .then_some(idx as u32) }) .ok_or(DriverError::Unsupported)?; // Submits to the GPU but does not wait for anything to be finished - render_graph - .resolve() - .submit(&mut LazyPool::new(device), queue_family_index, 0) + graph + .into_submission() + .queue_submit(&mut LazyPool::new(device), queue_family_index, 0) .map(|_| ()) } -fn splat(device: &Arc) -> Result, DriverError> { - Ok(Arc::new(GraphicPipeline::create( +fn splat(device: &Device) -> Result { + GraphicPipeline::create( device, GraphicPipelineInfo::default(), [ Shader::new_vertex( - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(vertex) const vec2 POSITION[] = { vec2(-1, -1), @@ -229,15 +264,15 @@ fn splat(device: &Arc) -> Result, DriverError> { texcoord = TEXCOORD[gl_VertexIndex]; gl_Position = vec4(POSITION[gl_VertexIndex], 0, 1); } - "#, - vert + "# ) .as_slice(), ), Shader::new_fragment( - inline_spirv!( + glsl!( r#" #version 460 core + #pragma shader_stage(fragment) layout(binding = 0) uniform sampler2D image; @@ -247,8 +282,7 @@ fn splat(device: &Arc) -> Result, DriverError> { void main() { color = texture(image, texcoord); } - "#, - frag + "# ) .as_slice(), ) @@ -257,7 +291,7 @@ fn splat(device: &Arc) -> Result, DriverError> { SamplerInfoBuilder::default().mipmap_mode(vk::SamplerMipmapMode::LINEAR), ), ], - )?)) + ) } #[derive(Parser)] diff --git a/examples/msaa.rs b/examples/msaa.rs index 6c4a0f27..45eb8408 100644 --- a/examples/msaa.rs +++ b/examples/msaa.rs @@ -1,22 +1,33 @@ mod profile_with_puffin; use { - bytemuck::{NoUninit, bytes_of, cast_slice}, + ash::vk, + bytemuck::{Pod, Zeroable, bytes_of, cast_slice}, clap::Parser, glam::{Mat4, Vec3}, - inline_spirv::inline_spirv, log::warn, - screen_13::prelude::*, - screen_13_window::WindowBuilder, - std::{mem::size_of, sync::Arc}, + std::{mem::size_of, sync::Arc, time::Instant}, + vk_graph::{ + cmd::{LoadOp, StoreOp}, + driver::{ + DriverError, + buffer::{Buffer, BufferInfo}, + device::Device, + graphic::{DepthStencilInfo, GraphicPipeline, GraphicPipelineInfo}, + image::{ImageInfo, SampleCount}, + physical_device::Vulkan10Limits, + }, + pool::{Pool as _, fifo::FifoPool}, + }, + vk_graph_window::Window, + vk_shader_macros::glsl, + vk_sync::AccessType, winit::{event::Event, keyboard::KeyCode}, winit_input_helper::WinitInputHelper, }; type CubeVertex = [[f32; 3]; 3]; -const WHITE: ClearColorValue = ClearColorValue([1.0, 1.0, 1.0, 1.0]); - /// Draws a spinning cube with high-contrast edges; hold any key to display the cube in non-MSAA /// mode. /// @@ -27,7 +38,7 @@ fn main() -> anyhow::Result<()> { let mut input = WinitInputHelper::default(); let args = Args::parse(); - let window = WindowBuilder::default().debug(args.debug).build()?; + let window = Window::builder().debug(args.debug).build()?; let depth_format = best_depth_format(&window.device); let sample_count = max_supported_sample_count(&window.device); let mesh_msaa_pipeline = create_mesh_pipeline(&window.device, sample_count)?; @@ -36,42 +47,42 @@ fn main() -> anyhow::Result<()> { let mut pool = FifoPool::new(&window.device); let mut angle = 0f32; + let mut prev_frame_at = Instant::now(); window.run(|frame| { - input.step_with_window_events( - &frame - .events - .iter() - .filter_map(|event| { - if let Event::WindowEvent { event, .. } = event { - Some(event.clone()) - } else { - None - } - }) - .collect::>(), - ); + let now = Instant::now(); + let dt = now - prev_frame_at; + prev_frame_at = now; + + input.step(); + for event in frame.events { + match event { + Event::WindowEvent { event, .. } => { + let _ = input.process_window_event(event); + } + Event::DeviceEvent { event, .. } => { + input.process_device_event(event); + } + _ => {} + } + } + input.end_step(); // Hold the tab key to render in non-multisample mode let will_render_msaa = !input.key_held(KeyCode::Tab) && sample_count != SampleCount::Type1; - angle += input - .delta_time() - .map(|dt| dt.as_secs_f32()) - .unwrap_or(0.016) - * 0.1; + angle += dt.as_secs_f32() * 0.1; let world_transform = Mat4::from_rotation_x(angle) * Mat4::from_rotation_y(angle * 0.61) * Mat4::from_rotation_z(angle * 0.22); let mut scene_uniform_buf = pool - .lease(BufferInfo::host_mem( + .resource(BufferInfo::host_mem( size_of::() as _, vk::BufferUsageFlags::UNIFORM_BUFFER, )) .unwrap(); - Buffer::copy_from_slice( - &mut scene_uniform_buf, + scene_uniform_buf.copy_from_slice( 0, bytes_of(&SceneUniformBuffer { view: Mat4::look_at_lh(Vec3::Z * 4.0, Vec3::ZERO, Vec3::NEG_Y), @@ -86,38 +97,39 @@ fn main() -> anyhow::Result<()> { }), ); - let cube_vertex_buf = frame.render_graph.bind_node(&cube_mesh.vertex_buf); - let scene_uniform_buf = frame.render_graph.bind_node(scene_uniform_buf); + let cube_vertex_buf = frame.graph.bind_resource(&cube_mesh.vertex_buf); + let scene_uniform_buf = frame.graph.bind_resource(scene_uniform_buf); - let mut pass = frame - .render_graph - .begin_pass("cube") + let mut cmd = frame + .graph + .begin_cmd() + .debug_name("cube") .bind_pipeline(if will_render_msaa { &mesh_msaa_pipeline } else { &mesh_noaa_pipeline }) - .set_depth_stencil(DepthStencilMode::DEPTH_WRITE) - .access_node(cube_vertex_buf, AccessType::VertexBuffer) - .access_descriptor(0, scene_uniform_buf, AccessType::AnyShaderReadUniformBuffer); + .depth_stencil(DepthStencilInfo::DEPTH_WRITE_LESS_IGNORE_STENCIL) + .resource_access(cube_vertex_buf, AccessType::VertexBuffer) + .shader_resource_access(0, scene_uniform_buf, AccessType::AnyShaderReadUniformBuffer); if will_render_msaa { - let msaa_color_image = pass.bind_node( - pool.lease( + let msaa_color_image = cmd.bind_resource( + pool.resource( ImageInfo::image_2d( frame.width, frame.height, - pass.node_info(frame.swapchain_image).fmt, + cmd.resource(frame.swapchain_image).info.fmt, vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::TRANSIENT_ATTACHMENT, ) - .to_builder() + .into_builder() .sample_count(sample_count), ) .unwrap(), ); - let msaa_depth_image = pass.bind_node( - pool.lease( + let msaa_depth_image = cmd.bind_resource( + pool.resource( ImageInfo::image_2d( frame.width, frame.height, @@ -125,20 +137,28 @@ fn main() -> anyhow::Result<()> { vk::ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT | vk::ImageUsageFlags::TRANSIENT_ATTACHMENT, ) - .to_builder() + .into_builder() .sample_count(sample_count), ) .unwrap(), ); // Attachments for multisample mode - pass = pass - .clear_color_value(0, msaa_color_image, WHITE) - .clear_depth_stencil(msaa_depth_image) - .resolve_color(0, 1, frame.swapchain_image); + cmd.set_color_attachment_image( + 0, + msaa_color_image, + LoadOp::CLEAR_WHITE_ALPHA_ONE, + StoreOp::DontCare, + ) + .set_color_attachment_resolve_image(0, 1, frame.swapchain_image) + .set_depth_stencil_attachment_image( + msaa_depth_image, + LoadOp::CLEAR_ONE_STENCIL_ZERO, + StoreOp::DontCare, + ); } else { - let noaa_depth_image = pass.bind_node( - pool.lease(ImageInfo::image_2d( + let noaa_depth_image = cmd.bind_resource( + pool.resource(ImageInfo::image_2d( frame.width, frame.height, depth_format, @@ -149,16 +169,22 @@ fn main() -> anyhow::Result<()> { ); // Attachments for non-multisample mode - pass = pass - .clear_color_value(0, frame.swapchain_image, WHITE) - .clear_depth_stencil(noaa_depth_image) - .store_color(0, frame.swapchain_image); + cmd.set_color_attachment_image( + 0, + frame.swapchain_image, + LoadOp::CLEAR_WHITE_ALPHA_ONE, + StoreOp::Store, + ) + .set_depth_stencil_attachment_image( + noaa_depth_image, + LoadOp::CLEAR_ONE_STENCIL_ZERO, + StoreOp::DontCare, + ); } - pass.record_subpass(move |subpass, _| { - subpass - .bind_vertex_buffer(cube_vertex_buf) - .push_constants(bytes_of(&world_transform)) + cmd.record_cmd(move |cmd| { + cmd.bind_vertex_buffer(0, cube_vertex_buf, 0) + .push_constants(0, bytes_of(&world_transform)) .draw(cube_mesh.vertex_count, 1, 0, 0); }); })?; @@ -168,8 +194,7 @@ fn main() -> anyhow::Result<()> { fn best_depth_format(device: &Device) -> vk::Format { for format in [vk::Format::D32_SFLOAT, vk::Format::D16_UNORM] { - let format_props = Device::image_format_properties( - device, + let format_props = device.physical_device.image_format_properties( format, vk::ImageType::TYPE_2D, vk::ImageTiling::OPTIMAL, @@ -298,7 +323,7 @@ fn load_cube_data() -> [CubeVertex; 36] { } /// Loads a cube as unindexed position, normal and color vertices -fn load_cube_mesh(device: &Arc) -> Result { +fn load_cube_mesh(device: &Device) -> Result { let vertices = load_cube_data(); let vertex_buf = Arc::new(Buffer::create_from_slice( @@ -314,12 +339,13 @@ fn load_cube_mesh(device: &Arc) -> Result { } fn create_mesh_pipeline( - device: &Arc, + device: &Device, sample_count: SampleCount, -) -> Result, DriverError> { - let vert = inline_spirv!( +) -> Result { + let vert = glsl!( r#" #version 460 core + #pragma shader_stage(vertex) layout(push_constant) uniform PushConstants { mat4 world; @@ -345,12 +371,12 @@ fn create_mesh_pipeline( normal_out = (push_const.world * vec4(normal, 1.0)).xyz; color_out = color; } - "#, - vert + "# ); - let frag = inline_spirv!( + let frag = glsl!( r#" #version 460 core + #pragma shader_stage(fragment) layout(set = 0, binding = 0) uniform Scene { mat4 view; @@ -368,20 +394,12 @@ fn create_mesh_pipeline( color_out = vec4(color * lambertian, 1.0); } - "#, - frag + "# ); - let info = GraphicPipelineInfoBuilder::default().samples(sample_count); + let info = GraphicPipelineInfo::builder().samples(sample_count); - Ok(Arc::new(GraphicPipeline::create( - device, - info, - [ - Shader::new_vertex(vert.as_slice()), - Shader::new_fragment(frag.as_slice()), - ], - )?)) + GraphicPipeline::create(device, info, [vert.as_slice(), frag.as_slice()]) } #[derive(Parser)] @@ -397,7 +415,7 @@ struct Model { } #[repr(C)] -#[derive(Clone, Copy, NoUninit)] +#[derive(Clone, Copy, Pod, Zeroable)] struct SceneUniformBuffer { view: Mat4, projection: Mat4, diff --git a/examples/multipass.rs b/examples/multipass.rs index 6224e245..da43a56c 100644 --- a/examples/multipass.rs +++ b/examples/multipass.rs @@ -1,13 +1,28 @@ mod profile_with_puffin; use { + ash::vk, bytemuck::{bytes_of, cast_slice}, clap::Parser, glam::{Mat4, Vec3, Vec4, vec3}, - inline_spirv::inline_spirv, - screen_13::prelude::*, - screen_13_window::WindowBuilder, - std::sync::Arc, + std::{sync::Arc, time::Instant}, + vk_graph::{ + Graph, + cmd::{LoadOp, StoreOp}, + driver::{ + DriverError, + buffer::{Buffer, BufferInfo}, + device::Device, + graphic::{DepthStencilInfo, GraphicPipeline, GraphicPipelineInfo}, + image::ImageInfo, + shader::Shader, + }, + node::BufferLeaseNode, + pool::{Lease, Pool as _, lazy::LazyPool}, + }, + vk_graph_window::Window, + vk_shader_macros::glsl, + vk_sync::AccessType, }; #[derive(Clone, Copy)] @@ -36,8 +51,8 @@ const GOLD: Material = Material { roughness: 0.3, }; -/// The example demonstrates leasing resources (images and buffers) and composing rendering -/// operations with just a few lines of RenderGraph builder-pattern code. +/// The example demonstrates requesting pooled resources (images and buffers) and composing +/// rendering operations with just a few lines of Graph builder-pattern code. /// /// Also shown: /// - Basic PBR rendering (from Sascha Willems) @@ -48,7 +63,7 @@ fn main() -> anyhow::Result<()> { profile_with_puffin::init(); let args = Args::parse(); - let window = WindowBuilder::default().debug(args.debug).build()?; + let window = Window::builder().debug(args.debug).build()?; let depth_stencil_format = best_depth_stencil_format(&window.device); let mut pool = LazyPool::new(&window.device); let fill_background = create_fill_background_pipeline(&window.device); @@ -56,15 +71,22 @@ fn main() -> anyhow::Result<()> { let pbr = create_pbr_pipeline(&window.device); let funky_shape = create_funky_shape(&window.device, &mut pool)?; - let mut t = 0.0; + let mut angle = 0.0; + let mut prev_frame_at = Instant::now(); + window.run(|frame| { - t += 0.016; + let now = Instant::now(); + + let dt = now - prev_frame_at; + prev_frame_at = now; + + angle += dt.as_secs_f32(); - let index_buf = frame.render_graph.bind_node(&funky_shape.index_buf); - let vertex_buf = frame.render_graph.bind_node(&funky_shape.vertex_buf); + let index_buf = frame.graph.bind_resource(&funky_shape.index_buf); + let vertex_buf = frame.graph.bind_resource(&funky_shape.vertex_buf); - let depth_stencil = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let depth_stencil = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( frame.width, frame.height, depth_stencil_format, @@ -75,32 +97,39 @@ fn main() -> anyhow::Result<()> { ); let camera = camera(frame.width, frame.height); - let model = Mat4::from_rotation_y(t * 0.4); + let model = Mat4::from_rotation_y(angle); let obj_pos = Vec3::ZERO; let material = GOLD; - let camera_buf = bind_camera_buf(frame.render_graph, &mut pool, camera, model); - let light_buf = bind_light_buf(frame.render_graph, &mut pool); + let camera_buf = bind_camera_buf(frame.graph, &mut pool, camera, model); + let light_buf = bind_light_buf(frame.graph, &mut pool); let push_const_data = write_push_consts(obj_pos, material); - let mut write = DepthStencilMode::DEPTH_WRITE; + let mut write = DepthStencilInfo::DEPTH_WRITE_LESS_IGNORE_STENCIL; // Depth Prepass frame - .render_graph - .begin_pass("Depth Prepass") + .graph + .begin_cmd() + .debug_name("Depth Prepass") .bind_pipeline(&prepass) - .set_depth_stencil(write) - .read_descriptor(0, camera_buf) - .access_node(index_buf, AccessType::IndexBuffer) - .access_node(vertex_buf, AccessType::VertexBuffer) - .clear_depth_stencil(depth_stencil) - .store_depth_stencil(depth_stencil) - .record_subpass(move |subpass, _| { - subpass - .bind_index_buffer(index_buf, vk::IndexType::UINT16) - .bind_vertex_buffer(vertex_buf) - .push_constants(bytes_of(&obj_pos)) + .depth_stencil(write) + .shader_resource_access( + 0, + camera_buf, + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + .resource_access(index_buf, AccessType::IndexBuffer) + .resource_access(vertex_buf, AccessType::VertexBuffer) + .depth_stencil_attachment_image( + depth_stencil, + LoadOp::CLEAR_ZERO_STENCIL_ZERO, + StoreOp::Store, + ) + .record_cmd(move |cmd| { + cmd.bind_index_buffer(index_buf, 0, vk::IndexType::UINT16) + .bind_vertex_buffer(0, vertex_buf, 0) + .push_constants(0, bytes_of(&obj_pos)) .draw_indexed(funky_shape.index_count, 1, 0, 0, 0); }); @@ -117,22 +146,29 @@ fn main() -> anyhow::Result<()> { // Renders a golden orb on an un-cleared swapchain image frame - .render_graph - .begin_pass("funky shape PBR") + .graph + .begin_cmd() + .debug_name("funky shape PBR") .bind_pipeline(&pbr) - .set_depth_stencil(write) - .read_descriptor(0, camera_buf) - .read_descriptor(1, light_buf) - .access_node(index_buf, AccessType::IndexBuffer) - .access_node(vertex_buf, AccessType::VertexBuffer) - .load_depth_stencil(depth_stencil) - .store_depth_stencil(depth_stencil) - .store_color(0, frame.swapchain_image) - .record_subpass(move |subpass, _| { - subpass - .bind_index_buffer(index_buf, vk::IndexType::UINT16) - .bind_vertex_buffer(vertex_buf) - .push_constants(&push_const_data) + .depth_stencil(write) + .shader_resource_access( + 0, + camera_buf, + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + .shader_resource_access( + 1, + light_buf, + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + .resource_access(index_buf, AccessType::IndexBuffer) + .resource_access(vertex_buf, AccessType::VertexBuffer) + .depth_stencil_attachment_image(depth_stencil, LoadOp::Load, StoreOp::Store) + .color_attachment_image(0, frame.swapchain_image, LoadOp::DontCare, StoreOp::Store) + .record_cmd(move |cmd| { + cmd.bind_index_buffer(index_buf, 0, vk::IndexType::UINT16) + .bind_vertex_buffer(0, vertex_buf, 0) + .push_constants(0, &push_const_data) .draw_indexed(funky_shape.index_count, 1, 0, 0, 0); }); @@ -145,15 +181,15 @@ fn main() -> anyhow::Result<()> { // Renders a solid color wherever the golden orb did not draw frame - .render_graph - .begin_pass("fill background") + .graph + .begin_cmd() + .debug_name("fill background") .bind_pipeline(&fill_background) - .set_depth_stencil(read) - .load_depth_stencil(depth_stencil) - .load_color(0, frame.swapchain_image) - .store_color(0, frame.swapchain_image) - .record_subpass(move |subpass, _| { - subpass.draw(6, 1, 0, 0); + .depth_stencil(read) + .depth_stencil_attachment_image(depth_stencil, LoadOp::Load, StoreOp::DontCare) + .color_attachment_image(0, frame.swapchain_image, LoadOp::Load, StoreOp::Store) + .record_cmd(move |cmd| { + cmd.draw(6, 1, 0, 0); }); })?; @@ -166,8 +202,7 @@ fn best_depth_stencil_format(device: &Device) -> vk::Format { vk::Format::D16_UNORM_S8_UINT, vk::Format::D32_SFLOAT_S8_UINT, ] { - let format_props = Device::image_format_properties( - device, + let format_props = device.physical_device.image_format_properties( format, vk::ImageType::TYPE_2D, vk::ImageTiling::OPTIMAL, @@ -185,32 +220,32 @@ fn best_depth_stencil_format(device: &Device) -> vk::Format { } fn bind_camera_buf( - render_graph: &mut RenderGraph, + graph: &mut Graph, pool: &mut LazyPool, camera: Camera, model: Mat4, ) -> BufferLeaseNode { let mut buf = pool - .lease(BufferInfo::host_mem( + .resource(BufferInfo::host_mem( 204, vk::BufferUsageFlags::UNIFORM_BUFFER, )) .unwrap(); write_camera_buf(&mut buf, camera, model); - render_graph.bind_node(buf) + graph.bind_resource(buf) } -fn bind_light_buf(render_graph: &mut RenderGraph, pool: &mut LazyPool) -> BufferLeaseNode { +fn bind_light_buf(graph: &mut Graph, pool: &mut LazyPool) -> BufferLeaseNode { let mut buf = pool - .lease(BufferInfo::host_mem( + .resource(BufferInfo::host_mem( 64, vk::BufferUsageFlags::UNIFORM_BUFFER, )) .unwrap(); write_light_buf(&mut buf); - render_graph.bind_node(buf) + graph.bind_resource(buf) } fn write_push_consts(obj_pos: Vec3, material: Material) -> [u8; 32] { @@ -243,7 +278,7 @@ fn camera(width: u32, height: u32) -> Camera { /// Returns ready-to-use index and vertex buffers. Index count is also returned. The shape data uses /// temporary staging buffers which are not required but are fun. -fn create_funky_shape(device: &Arc, pool: &mut LazyPool) -> Result { +fn create_funky_shape(device: &Device, pool: &mut LazyPool) -> Result { // Static index/vertex data courtesy of the polyhedron-ops library let (indices, vertices) = funky_shape_data(); let index_count = indices.len() as u32; @@ -277,13 +312,13 @@ fn create_funky_shape(device: &Arc, pool: &mut LazyPool) -> Result, pool: &mut LazyPool) -> Result, pool: &mut LazyPool) -> Result) -> Arc { +fn create_fill_background_pipeline(device: &Device) -> GraphicPipeline { let vertex_shader = Shader::new_vertex( - inline_spirv!( + glsl!( r#" #version 450 core + #pragma shader_stage(vertex) const float X[6] = {-1, -1, 1, 1, 1, -1}; const float Y[6] = {-1, 1, -1, 1, -1, 1}; - vec2 vertex_pos() - { + vec2 vertex_pos() { float x = X[gl_VertexIndex]; float y = Y[gl_VertexIndex]; return vec2(x, y); } - void main() - { + void main() { gl_Position = vec4(vertex_pos(), 0, 1); } - "#, - vert + "# ) .as_slice(), ); let fragment_shader = Shader::new_fragment( - inline_spirv!( + glsl!( r#" #version 450 + #pragma shader_stage(fragment) layout(location = 0) out vec4 color; - void main() - { + void main() { color = vec4(vec3(0.75), 1.0); } - "#, - frag + "# ) .as_slice(), ); - Arc::new( - GraphicPipeline::create( - device, - GraphicPipelineInfo::default(), - [vertex_shader, fragment_shader], - ) - .unwrap(), + GraphicPipeline::create( + device, + GraphicPipelineInfo::default(), + [vertex_shader, fragment_shader], ) + .unwrap() } -fn create_prepass_pipeline(device: &Arc) -> Arc { +fn create_prepass_pipeline(device: &Device) -> GraphicPipeline { let vertex_shader = Shader::new_vertex( - inline_spirv!( + glsl!( r#" #version 450 + #pragma shader_stage(vertex) layout (location = 0) in vec3 inPos; layout (location = 1) in vec3 inNormal; @@ -380,65 +411,59 @@ fn create_prepass_pipeline(device: &Arc) -> Arc { vec3 objPos; } pushConsts; - out gl_PerVertex - { + out gl_PerVertex { vec4 gl_Position; }; - void main() - { + void main() { vec3 locPos = vec3(ubo.model * vec4(inPos, 1.0)); outWorldPos = locPos + pushConsts.objPos; outNormal = mat3(ubo.model) * inNormal; gl_Position = ubo.projection * ubo.view * vec4(outWorldPos, 1.0); } - "#, - vert + "# ) .as_slice(), ); let fragment_shader = Shader::new_fragment( - inline_spirv!( + glsl!( r#" #version 450 + #pragma shader_stage(fragment) layout (location = 0) in vec3 inWorldPos; layout (location = 1) in vec3 inNormal; - layout (binding = 0) uniform UBO - { + layout (binding = 0) uniform UBO { mat4 projection; mat4 model; mat4 view; vec3 camPos; } ubo; - void main() - { + void main() { } - "#, - frag + "# ) .as_slice(), ); - Arc::new( - GraphicPipeline::create( - device, - GraphicPipelineInfo::default(), - [vertex_shader, fragment_shader], - ) - .unwrap(), + GraphicPipeline::create( + device, + GraphicPipelineInfo::default(), + [vertex_shader, fragment_shader], ) + .unwrap() } -fn create_pbr_pipeline(device: &Arc) -> Arc { +fn create_pbr_pipeline(device: &Device) -> GraphicPipeline { // See: https://github.com/SaschaWillems/Vulkan/blob/master/data/shaders/glsl/pbrbasic/pbr.vert let vertex_shader = Shader::new_vertex( - inline_spirv!( + glsl!( r#" #version 450 + #pragma shader_stage(vertex) layout (location = 0) in vec3 inPos; layout (location = 1) in vec3 inNormal; @@ -458,29 +483,27 @@ fn create_pbr_pipeline(device: &Arc) -> Arc { vec3 objPos; } pushConsts; - out gl_PerVertex - { + out gl_PerVertex { vec4 gl_Position; }; - void main() - { + void main() { vec3 locPos = vec3(ubo.model * vec4(inPos, 1.0)); outWorldPos = locPos + pushConsts.objPos; outNormal = mat3(ubo.model) * inNormal; gl_Position = ubo.projection * ubo.view * vec4(outWorldPos, 1.0); } - "#, - vert + "# ) .as_slice(), ); // See: https://github.com/SaschaWillems/Vulkan/blob/master/data/shaders/glsl/pbrbasic/pbr.frag let fragment_shader = Shader::new_fragment( - inline_spirv!( + glsl!( r#" #version 450 + #pragma shader_stage(fragment) layout (location = 0) in vec3 inWorldPos; layout (location = 1) in vec3 inNormal; @@ -606,20 +629,17 @@ fn create_pbr_pipeline(device: &Arc) -> Arc { outColor = vec4(color, 1.0); } - "#, - frag + "# ) .as_slice(), ); - Arc::new( - GraphicPipeline::create( - device, - GraphicPipelineInfo::default(), - [vertex_shader, fragment_shader], - ) - .unwrap(), + GraphicPipeline::create( + device, + GraphicPipelineInfo::default(), + [vertex_shader, fragment_shader], ) + .unwrap() } /// Returns index and position/normal data (polyhedron_ops you are 🥇🏆🥂💯) diff --git a/examples/multithread.rs b/examples/multithread.rs index dd66ba33..8b803045 100644 --- a/examples/multithread.rs +++ b/examples/multithread.rs @@ -1,13 +1,11 @@ mod profile_with_puffin; use { + ash::vk, bmfont::{BMFont, OrdinateOrientation}, clap::Parser, image::ImageReader, log::info, - screen_13::prelude::*, - screen_13_fx::BitmapFont, - screen_13_window::WindowBuilder, std::{ collections::VecDeque, io::Cursor, @@ -19,6 +17,17 @@ use { thread::{available_parallelism, sleep, spawn}, time::{Duration, Instant}, }, + vk_graph::{ + Graph, + driver::{ + buffer::Buffer, + device::Device, + image::{Image, ImageInfo}, + }, + pool::{Pool as _, hash::HashPool}, + }, + vk_graph_fx::BitmapFont, + vk_graph_window::Window, }; const COLOR_SUBRESOURCE_LAYER: vk::ImageSubresourceLayers = vk::ImageSubresourceLayers { @@ -38,10 +47,7 @@ fn main() -> anyhow::Result<()> { // For this example we don't use V-Sync so that we are able to submit work as often as possible let args = Args::parse(); - let window = WindowBuilder::default() - .debug(args.debug) - .v_sync(false) - .build()?; + let window = Window::builder().debug(args.debug).v_sync(false).build()?; // We want to create one hardware queue for each CPU, or at least two let desired_queue_count = available_parallelism() @@ -63,7 +69,8 @@ fn main() -> anyhow::Result<()> { || queue_family_properties .queue_flags .contains(vk::QueueFlags::GRAPHICS) - }); + }) + .map(|(idx, queue_family_properties)| (idx as u32, queue_family_properties)); assert!( secondary_queue_family.is_some(), @@ -72,8 +79,7 @@ fn main() -> anyhow::Result<()> { let (secondary_queue_family_index, secondary_queue_family_properties) = secondary_queue_family.unwrap(); - let queue_count = - desired_queue_count.min(secondary_queue_family_properties.queue_count) as usize; + let queue_count = desired_queue_count.min(secondary_queue_family_properties.queue_count); assert!(queue_count > 0, "GPU does not support secondary queues"); @@ -81,14 +87,14 @@ fn main() -> anyhow::Result<()> { let running = Arc::new(AtomicBool::new(true)); let thread_count = queue_count; - let mut threads = Vec::with_capacity(thread_count); + let mut threads = Vec::with_capacity(thread_count as _); let (tx, rx) = channel(); info!("Launching {thread_count} threads"); for thread_index in 0..thread_count { let running = Arc::clone(&running); - let device = Arc::clone(&window.device); + let device = window.device.clone(); let tx = tx.clone(); threads.push(spawn(move || { let queue_index = thread_index; @@ -101,9 +107,9 @@ fn main() -> anyhow::Result<()> { let t = 12.0 * ((Instant::now() - started_at).as_millis() % 32) as f32; // Clear a new image to a cycling color - let mut render_graph = RenderGraph::new(); - let image = render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let mut graph = Graph::default(); + let image = graph.bind_resource( + pool.resource(ImageInfo::image_2d( 10, 10, vk::Format::R8G8B8A8_UNORM, @@ -111,7 +117,7 @@ fn main() -> anyhow::Result<()> { )) .unwrap(), ); - render_graph.clear_color_image_value( + graph.clear_color_image( image, [ (t.sin() * 127.0 + 128.0) as u8, @@ -121,12 +127,12 @@ fn main() -> anyhow::Result<()> { ], ); - let image = render_graph.unbind_node(image); + let image = graph.resource(image).clone(); // Submit on a queue we are reserving for only this thread to use - render_graph - .resolve() - .submit(&mut pool, secondary_queue_family_index, queue_index) + graph + .into_submission() + .queue_submit(&mut pool, secondary_queue_family_index, queue_index) .unwrap(); // After submit() is called we can safely use this image on another thread! @@ -152,10 +158,12 @@ fn main() -> anyhow::Result<()> { } } - frame.render_graph.clear_color_image(frame.swapchain_image); + frame + .graph + .clear_color_image(frame.swapchain_image, [0f32; 4]); for (image_idx, image) in images.iter().enumerate() { - let image = frame.render_graph.bind_node(image); + let image = frame.graph.bind_resource(image); let x = (image_idx % 8) as f32; let y = (image_idx / 8) as f32; @@ -163,11 +171,11 @@ fn main() -> anyhow::Result<()> { let j = frame.width as f32 / 10.0; let k = frame.height as f32 / 10.0; - frame.render_graph.blit_image_region( + frame.graph.blit_image_region( image, frame.swapchain_image, vk::Filter::NEAREST, - vk::ImageBlit { + [vk::ImageBlit { src_subresource: COLOR_SUBRESOURCE_LAYER, src_offsets: [ vk::Offset3D { x: 0, y: 0, z: 0 }, @@ -186,14 +194,14 @@ fn main() -> anyhow::Result<()> { z: 1, }, ], - }, + }], ); } let fps = (1.0 / elapsed.as_secs_f32()).round(); let message = format!("FPS: {fps}"); font.print_scale( - frame.render_graph, + frame.graph, frame.swapchain_image, 0.0, 0.0, @@ -213,15 +221,17 @@ fn main() -> anyhow::Result<()> { Ok(()) } -fn load_font(device: &Arc) -> anyhow::Result { +fn load_font(device: &Device) -> anyhow::Result { // Load the font definition file using the bmfont crate let font = BMFont::new( Cursor::new(include_bytes!("res/font/small/small_10px.fnt")), OrdinateOrientation::TopToBottom, )?; + let mut graph = Graph::default(); + // We happen to know this font only requires a single image, this uses the image crate - let temp_buf = Buffer::create_from_slice( + let temp_buf = graph.bind_resource(Buffer::create_from_slice( device, vk::BufferUsageFlags::TRANSFER_SRC, ImageReader::new(Cursor::new( @@ -232,33 +242,30 @@ fn load_font(device: &Arc) -> anyhow::Result { .into_rgba8() .to_vec() .as_slice(), - )?; + )?); // This image will hold the font glyphs - let page_0 = Image::create( - device, - ImageInfo::image_2d( - 64, - 64, - vk::Format::R8G8B8A8_UNORM, - vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST, - ), - ) - .unwrap(); - - let mut render_graph = RenderGraph::new(); - let page_0 = render_graph.bind_node(page_0); - let temp_buf = render_graph.bind_node(temp_buf); - render_graph.copy_buffer_to_image(temp_buf, page_0); - - // Unbind page_0 to get the Arc but we could have just bound a reference (with no unbind) - let page_0 = render_graph.unbind_node(page_0); - - // This copy happens in queue index 0! Notice the unbind above is OK because we already asked - // for the copy to happen first! - render_graph - .resolve() - .submit(&mut HashPool::new(device), 0, 0)?; + let page_0 = graph.bind_resource( + Image::create( + device, + ImageInfo::image_2d( + 64, + 64, + vk::Format::R8G8B8A8_UNORM, + vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST, + ), + ) + .unwrap(), + ); + + graph.copy_buffer_to_image(temp_buf, page_0); + + let page_0 = graph.resource(page_0).clone(); + + // This copy happens in queue index 0! + graph + .into_submission() + .queue_submit(&mut HashPool::new(device), 0, 0)?; BitmapFont::new(device, font, [page_0]) } diff --git a/examples/profile_with_puffin/mod.rs b/examples/profile_with_puffin/mod.rs index bae98e06..0ad58989 100644 --- a/examples/profile_with_puffin/mod.rs +++ b/examples/profile_with_puffin/mod.rs @@ -8,7 +8,7 @@ //! ``` //! //! For more information see: -//! https://github.com/attackgoat/screen-13/blob/master/examples/getting-started.md +//! https://github.com/attackgoat/vk-graph/blob/master/examples/getting-started.md #[cfg(feature = "profile-with-puffin")] use { diff --git a/examples/ray_omni.rs b/examples/ray_omni.rs index 70340d7e..065a4196 100644 --- a/examples/ray_omni.rs +++ b/examples/ray_omni.rs @@ -1,22 +1,43 @@ mod profile_with_puffin; use { + ash::vk, bytemuck::{Pod, Zeroable, bytes_of, cast_slice}, clap::Parser, glam::{Mat4, Vec3, Vec4, vec3, vec4}, - inline_spirv::inline_spirv, log::info, meshopt::remap::{generate_vertex_remap, remap_index_buffer, remap_vertex_buffer}, - screen_13::prelude::*, - screen_13_window::WindowBuilder, std::{ env::current_exe, fs::{metadata, write}, mem::size_of, path::{Path, PathBuf}, sync::Arc, + time::Instant, }, tobj::{GPU_LOAD_OPTIONS, load_obj}, + vk_graph::{ + Graph, + cmd::{BuildAccelerationStructureInfo, LoadOp, StoreOp}, + driver::{ + DriverError, + accel_struct::{ + AccelerationStructure, AccelerationStructureGeometry, + AccelerationStructureGeometryData, AccelerationStructureGeometryInfo, + AccelerationStructureInfo, + }, + buffer::{Buffer, BufferInfo}, + device::Device, + graphic::{DepthStencilInfo, GraphicPipeline, GraphicPipelineInfo}, + image::ImageInfo, + shader::Shader, + }, + node::AccelerationStructureLeaseNode, + pool::{Pool as _, lazy::LazyPool}, + }, + vk_graph_window::Window, + vk_shader_macros::glsl, + vk_sync::AccessType, }; fn main() -> anyhow::Result<()> { @@ -24,7 +45,7 @@ fn main() -> anyhow::Result<()> { profile_with_puffin::init(); let args = Args::parse(); - let window = WindowBuilder::default().debug(args.debug).build()?; + let window = Window::builder().debug(args.debug).build()?; let mut pool = LazyPool::new(&window.device); let depth_fmt = best_2d_optimal_format( @@ -41,20 +62,24 @@ fn main() -> anyhow::Result<()> { let gfx_pipeline = create_pipeline(&window.device)?; let mut angle = 0f32; + let mut prev_frame_at = Instant::now(); window.run(|frame| { - angle += 0.016; + let now = Instant::now(); + let dt = now - prev_frame_at; + prev_frame_at = now; + + angle += dt.as_secs_f32(); - let scene_tlas = - create_tlas(frame.device, &mut pool, frame.render_graph, &scene_blas).unwrap(); + let scene_tlas = create_tlas(frame.device, &mut pool, frame.graph, &scene_blas).unwrap(); - let ground_mesh_index_buf = frame.render_graph.bind_node(&ground_mesh.index_buf); - let ground_mesh_vertex_buf = frame.render_graph.bind_node(&ground_mesh.vertex_buf); - let model_mesh_index_buf = frame.render_graph.bind_node(&model_mesh.index_buf); - let model_mesh_vertex_buf = frame.render_graph.bind_node(&model_mesh.vertex_buf); + let ground_mesh_index_buf = frame.graph.bind_resource(&ground_mesh.index_buf); + let ground_mesh_vertex_buf = frame.graph.bind_resource(&ground_mesh.vertex_buf); + let model_mesh_index_buf = frame.graph.bind_resource(&model_mesh.index_buf); + let model_mesh_vertex_buf = frame.graph.bind_resource(&model_mesh.vertex_buf); - let depth_image = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let depth_image = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( frame.width, frame.height, depth_fmt, @@ -62,15 +87,14 @@ fn main() -> anyhow::Result<()> { )) .unwrap(), ); - let camera_buf = frame.render_graph.bind_node({ + let camera_buf = frame.graph.bind_resource({ let mut buf = pool - .lease(BufferInfo::host_mem( + .resource(BufferInfo::host_mem( size_of::() as _, vk::BufferUsageFlags::UNIFORM_BUFFER, )) .unwrap(); - Buffer::copy_from_slice( - &mut buf, + buf.copy_from_slice( 0, bytes_of(&Camera { projection: Mat4::perspective_rh( @@ -89,32 +113,39 @@ fn main() -> anyhow::Result<()> { }); frame - .render_graph - .begin_pass("Mesh with ray-query shadows") + .graph + .begin_cmd() + .debug_name("Mesh with ray-query shadows") .bind_pipeline(&gfx_pipeline) - .access_node(ground_mesh_index_buf, AccessType::IndexBuffer) - .access_node(ground_mesh_vertex_buf, AccessType::VertexBuffer) - .access_node(model_mesh_index_buf, AccessType::IndexBuffer) - .access_node(model_mesh_vertex_buf, AccessType::VertexBuffer) - .access_descriptor(0, camera_buf, AccessType::AnyShaderReadUniformBuffer) - .access_descriptor( + .resource_access(ground_mesh_index_buf, AccessType::IndexBuffer) + .resource_access(ground_mesh_vertex_buf, AccessType::VertexBuffer) + .resource_access(model_mesh_index_buf, AccessType::IndexBuffer) + .resource_access(model_mesh_vertex_buf, AccessType::VertexBuffer) + .shader_resource_access(0, camera_buf, AccessType::AnyShaderReadUniformBuffer) + .shader_resource_access( 1, scene_tlas, AccessType::RayTracingShaderReadAccelerationStructure, ) - .set_depth_stencil(DepthStencilMode::DEPTH_WRITE) - .clear_depth_stencil(depth_image) - .clear_color_value(0, frame.swapchain_image, [0xff, 0xff, 0xff, 0xff]) - .store_color(0, frame.swapchain_image) - .record_subpass(move |subpass, _| { - subpass - .bind_index_buffer(model_mesh_index_buf, vk::IndexType::UINT32) - .bind_vertex_buffer(model_mesh_vertex_buf) + .depth_stencil(DepthStencilInfo::DEPTH_WRITE_LESS_IGNORE_STENCIL) + .depth_stencil_attachment_image( + depth_image, + LoadOp::CLEAR_ONE_STENCIL_ZERO, + StoreOp::DontCare, + ) + .color_attachment_image( + 0, + frame.swapchain_image, + LoadOp::CLEAR_WHITE_ALPHA_ONE, + StoreOp::Store, + ) + .record_cmd(move |cmd| { + cmd.bind_index_buffer(model_mesh_index_buf, 0, vk::IndexType::UINT32) + .bind_vertex_buffer(0, model_mesh_vertex_buf, 0) .draw_indexed(model_mesh.index_count, 1, 0, 0, 0); - subpass - .bind_index_buffer(ground_mesh_index_buf, vk::IndexType::UINT32) - .bind_vertex_buffer(ground_mesh_vertex_buf) + cmd.bind_index_buffer(ground_mesh_index_buf, 0, vk::IndexType::UINT32) + .bind_vertex_buffer(0, ground_mesh_vertex_buf, 0) .draw_indexed(ground_mesh.index_count, 1, 0, 0, 0); }); })?; @@ -129,8 +160,7 @@ fn best_2d_optimal_format( flags: vk::ImageCreateFlags, ) -> vk::Format { for format in formats { - let format_props = Device::image_format_properties( - device, + let format_props = device.physical_device.image_format_properties( *format, vk::ImageType::TYPE_2D, vk::ImageTiling::OPTIMAL, @@ -147,7 +177,7 @@ fn best_2d_optimal_format( } fn create_blas( - device: &Arc, + device: &Device, models: &[&Model], ) -> Result, DriverError> { let info = AccelerationStructureGeometryInfo::blas( @@ -159,11 +189,11 @@ fn create_blas( max_primitive_count: model.index_count / 3, flags: vk::GeometryFlagsKHR::OPAQUE, geometry: AccelerationStructureGeometryData::triangles( - Buffer::device_address(&model.index_buf), + model.index_buf.device_address(), vk::IndexType::UINT32, model.vertex_count, None, - Buffer::device_address(&model.vertex_buf), + model.vertex_buf.device_address(), vk::Format::R32G32B32_SFLOAT, 24, ), @@ -177,8 +207,8 @@ fn create_blas( .flags(vk::BuildAccelerationStructureFlagsKHR::PREFER_FAST_TRACE); let size = AccelerationStructure::size_of(device, &info); - let mut render_graph = RenderGraph::new(); - let blas = render_graph.bind_node(AccelerationStructure::create( + let mut graph = Graph::default(); + let blas = graph.bind_resource(AccelerationStructure::create( device, AccelerationStructureInfo::blas(size.create_size), )?); @@ -190,46 +220,51 @@ fn create_blas( .unwrap() .min_accel_struct_scratch_offset_alignment as vk::DeviceSize; - let scratch_buf = render_graph.bind_node(Buffer::create( + let scratch_buf = graph.bind_resource(Buffer::create( device, BufferInfo::device_mem( size.build_size, vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS | vk::BufferUsageFlags::STORAGE_BUFFER, ) - .to_builder() + .into_builder() .alignment(accel_struct_scratch_offset_alignment), )?); - let scratch_data = render_graph.node_device_address(scratch_buf); + let scratch_addr = graph.resource(scratch_buf).device_address(); - let mut pass = render_graph.begin_pass("Build BLAS"); + let mut cmd = graph.begin_cmd().debug_name("Build BLAS"); for model in models.iter().copied() { - let index_buf = pass.bind_node(&model.index_buf); - let vertex_buf = pass.bind_node(&model.vertex_buf); + let index_buf = cmd.bind_resource(&model.index_buf); + let vertex_buf = cmd.bind_resource(&model.vertex_buf); - pass.access_node_mut(index_buf, AccessType::AccelerationStructureBuildRead); - pass.access_node_mut(vertex_buf, AccessType::AccelerationStructureBuildRead); + cmd.set_resource_access(index_buf, AccessType::AccelerationStructureBuildRead); + cmd.set_resource_access(vertex_buf, AccessType::AccelerationStructureBuildRead); } - pass.access_node(blas, AccessType::AccelerationStructureBuildWrite) - .access_node(scratch_buf, AccessType::AccelerationStructureBufferWrite) - .record_acceleration(move |accel, _| { - accel.build_structure(&info, blas, scratch_data); + cmd.resource_access(blas, AccessType::AccelerationStructureBuildWrite) + .resource_access(scratch_buf, AccessType::AccelerationStructureBufferWrite) + .record_cmd(move |cmd| { + cmd.build_accel_struct(&[BuildAccelerationStructureInfo::new( + blas, + scratch_addr, + info, + )]); }); - let blas = render_graph.unbind_node(blas); + let blas = graph.resource(blas).clone(); - render_graph - .resolve() - .submit(&mut LazyPool::new(device), 0, 0)?; + graph + .into_submission() + .queue_submit(&mut LazyPool::new(device), 0, 0)?; Ok(blas) } -fn create_pipeline(device: &Arc) -> Result, DriverError> { - let vert = inline_spirv!( +fn create_pipeline(device: &Device) -> Result { + let vert = glsl!( r#" #version 460 core + #pragma shader_stage(vertex) layout (location = 0) in vec3 inPos; layout (location = 1) in vec3 inNormal; @@ -257,15 +292,15 @@ fn create_pipeline(device: &Arc) -> Result, DriverE outLightVec = normalize(ubo.lightPos - inPos); outViewVec = -pos.xyz; } - "#, - vert, - vulkan1_2 + "# ); - let frag = inline_spirv!( + let frag = glsl!( + target: vulkan1_2, r#" #version 460 core #extension GL_EXT_ray_tracing : enable #extension GL_EXT_ray_query : enable + #pragma shader_stage(fragment) layout (binding = 1) uniform accelerationStructureEXT topLevelAS; @@ -289,35 +324,45 @@ fn create_pipeline(device: &Arc) -> Result, DriverE outFragColor = vec4(diffuse, 1.0); rayQueryEXT rayQuery; - rayQueryInitializeEXT(rayQuery, topLevelAS, gl_RayFlagsTerminateOnFirstHitEXT, 0xFF, inWorldPos, 0.01, L, 1000.0); + rayQueryInitializeEXT( + rayQuery, + topLevelAS, + gl_RayFlagsTerminateOnFirstHitEXT, + 0xFF, + inWorldPos, + 0.01, + L, + 1000.0 + ); - // Traverse the acceleration structure and store information about the first intersection (if any) + // Traverse the acceleration structure and store the first intersection, if any. rayQueryProceedEXT(rayQuery); // If the intersection has hit a triangle, the fragment is shadowed - if (rayQueryGetIntersectionTypeEXT(rayQuery, true) == gl_RayQueryCommittedIntersectionTriangleEXT ) { + if ( + rayQueryGetIntersectionTypeEXT(rayQuery, true) + == gl_RayQueryCommittedIntersectionTriangleEXT + ) { outFragColor *= 0.1; } } - "#, - frag, - vulkan1_2 + "# ); - Ok(Arc::new(GraphicPipeline::create( + GraphicPipeline::create( device, GraphicPipelineInfo::default(), [ Shader::new_vertex(vert.as_slice()), Shader::new_fragment(frag.as_slice()), ], - )?)) + ) } fn create_tlas( - device: &Arc, + device: &Device, pool: &mut LazyPool, - render_graph: &mut RenderGraph, + graph: &mut Graph, blas: &Arc, ) -> Result { let instances = [vk::AccelerationStructureInstanceKHR { @@ -348,7 +393,7 @@ fn create_tlas( | vk::BufferUsageFlags::STORAGE_BUFFER, ), )?; - Buffer::copy_from_slice(&mut buffer, 0, instance_data); + buffer.copy_from_slice(0, instance_data); buffer }); @@ -356,14 +401,14 @@ fn create_tlas( let info = AccelerationStructureGeometryInfo::tlas([( AccelerationStructureGeometry::opaque( 2, - AccelerationStructureGeometryData::instances(Buffer::device_address(&instance_buf)), + AccelerationStructureGeometryData::instances(instance_buf.device_address()), ), vk::AccelerationStructureBuildRangeInfoKHR::default().primitive_count(1), )]) .flags(vk::BuildAccelerationStructureFlagsKHR::PREFER_FAST_TRACE); let size = AccelerationStructure::size_of(device, &info); let tlas = - render_graph.bind_node(pool.lease(AccelerationStructureInfo::tlas(size.create_size))?); + graph.bind_resource(pool.resource(AccelerationStructureInfo::tlas(size.create_size))?); let accel_struct_scratch_offset_alignment = device .physical_device @@ -372,28 +417,33 @@ fn create_tlas( .unwrap() .min_accel_struct_scratch_offset_alignment as vk::DeviceSize; - let scratch_buf = render_graph.bind_node( - pool.lease( + let scratch_buf = graph.bind_resource( + pool.resource( BufferInfo::device_mem( size.build_size, vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS | vk::BufferUsageFlags::STORAGE_BUFFER, ) - .to_builder() + .into_builder() .alignment(accel_struct_scratch_offset_alignment), )?, ); - let scratch_data = render_graph.node_device_address(scratch_buf); - let blas = render_graph.bind_node(blas); - let instance_buf = render_graph.bind_node(instance_buf); - - render_graph - .begin_pass("Build TLAS") - .access_node(blas, AccessType::AccelerationStructureBuildRead) - .access_node(instance_buf, AccessType::AccelerationStructureBuildRead) - .access_node(scratch_buf, AccessType::AccelerationStructureBufferWrite) - .access_node(tlas, AccessType::AccelerationStructureBuildWrite) - .record_acceleration(move |accel, _| { - accel.build_structure(&info, tlas, scratch_data); + let scratch_addr = graph.resource(scratch_buf).device_address(); + let blas = graph.bind_resource(blas); + let instance_buf = graph.bind_resource(instance_buf); + + graph + .begin_cmd() + .debug_name("Build TLAS") + .resource_access(blas, AccessType::AccelerationStructureBuildRead) + .resource_access(instance_buf, AccessType::AccelerationStructureBuildRead) + .resource_access(scratch_buf, AccessType::AccelerationStructureBufferWrite) + .resource_access(tlas, AccessType::AccelerationStructureBuildWrite) + .record_cmd(move |cmd| { + cmd.build_accel_struct(&[BuildAccelerationStructureInfo::new( + tlas, + scratch_addr, + info, + )]); }); Ok(tlas) @@ -418,7 +468,7 @@ fn download_model_from_github(model_name: &str) -> anyhow::Result { Ok(model_path) } -fn load_ground_mesh(device: &Arc) -> Result { +fn load_ground_mesh(device: &Device) -> Result { let extent = 100f32; let v0 = [-extent, 0.0, -extent]; let v1 = [extent, 0.0, -extent]; @@ -450,7 +500,7 @@ fn load_ground_mesh(device: &Arc) -> Result { } fn load_model( - device: &Arc, + device: &Device, path: impl AsRef, face_fn: fn(a: Vec3, b: Vec3, c: Vec3) -> [T; 3], ) -> anyhow::Result @@ -525,7 +575,7 @@ where } /// Loads an .obj model as indexed position and normal vertices -fn load_model_mesh(device: &Arc, path: impl AsRef) -> anyhow::Result { +fn load_model_mesh(device: &Device, path: impl AsRef) -> anyhow::Result { #[repr(C)] #[derive(Clone, Copy, Default, Pod, Zeroable)] struct Vertex { diff --git a/examples/ray_trace.rs b/examples/ray_trace.rs index 53dff06b..9ba2cb02 100644 --- a/examples/ray_trace.rs +++ b/examples/ray_trace.rs @@ -1,22 +1,44 @@ mod profile_with_puffin; use { - bytemuck::cast_slice, + ash::vk, + bytemuck::{Pod, Zeroable, bytes_of, cast_slice}, clap::Parser, - inline_spirv::inline_spirv, log::warn, - screen_13::prelude::*, - screen_13_window::WindowBuilder, std::{io::BufReader, mem::size_of, sync::Arc}, tobj::{GPU_LOAD_OPTIONS, load_mtl_buf, load_obj_buf}, + vk_graph::{ + Graph, + cmd::BuildAccelerationStructureInfo, + driver::{ + DriverError, + accel_struct::{ + AccelerationStructure, AccelerationStructureGeometry, + AccelerationStructureGeometryData, AccelerationStructureGeometryInfo, + AccelerationStructureInfo, + }, + buffer::{Buffer, BufferInfo}, + device::Device, + image::ImageInfo, + physical_device::RayTraceProperties, + ray_trace::{RayTracePipeline, RayTracePipelineInfo, RayTraceShaderGroup}, + shader::Shader, + }, + pool::{Pool as _, hash::HashPool}, + }, + vk_graph_window::Window, + vk_shader_macros::glsl, + vk_sync::AccessType, winit::{event::Event, keyboard::KeyCode}, winit_input_helper::WinitInputHelper, }; -static SHADER_RAY_GEN: &[u32] = inline_spirv!( +static SHADER_RAY_GEN: &[u32] = glsl!( + target: vulkan1_2, r#" #version 460 #extension GL_EXT_ray_tracing : require + #pragma shader_stage(raygen) #define M_PI 3.1415926535897932384626433832795 @@ -83,17 +105,17 @@ static SHADER_RAY_GEN: &[u32] = inline_spirv!( imageStore(image, ivec2(gl_LaunchIDEXT.xy), color); } - "#, - rgen, - vulkan1_2 + "# ) .as_slice(); -static SHADER_CLOSEST_HIT: &[u32] = inline_spirv!( +static SHADER_CLOSEST_HIT: &[u32] = glsl!( + target: vulkan1_2, r#" #version 460 #extension GL_EXT_ray_tracing : require #extension GL_EXT_nonuniform_qualifier : enable + #pragma shader_stage(closest) #define M_PI 3.1415926535897932384626433832795 @@ -281,16 +303,16 @@ static SHADER_CLOSEST_HIT: &[u32] = inline_spirv!( payload.rayDepth += 1; } - "#, - rchit, - vulkan1_2 + "# ) .as_slice(); -static SHADER_MISS: &[u32] = inline_spirv!( +static SHADER_MISS: &[u32] = glsl!( + target: vulkan1_2, r#" #version 460 #extension GL_EXT_ray_tracing : require + #pragma shader_stage(miss) layout(location = 0) rayPayloadInEXT Payload { vec3 rayOrigin; @@ -307,32 +329,30 @@ static SHADER_MISS: &[u32] = inline_spirv!( void main() { payload.rayActive = 0; } - "#, - rmiss, - vulkan1_2 + "# ) .as_slice(); -static SHADER_SHADOW_MISS: &[u32] = inline_spirv!( +static SHADER_SHADOW_MISS: &[u32] = glsl!( + target: vulkan1_2, r#" #version 460 #extension GL_EXT_ray_tracing : require + #pragma shader_stage(miss) layout(location = 1) rayPayloadInEXT bool isShadow; void main() { isShadow = false; } - "#, - rmiss, - vulkan1_2 + "# ) .as_slice(); -fn create_ray_trace_pipeline(device: &Arc) -> Result, DriverError> { - Ok(Arc::new(RayTracePipeline::create( +fn create_ray_trace_pipeline(device: &Device) -> Result { + RayTracePipeline::create( device, - RayTracePipelineInfoBuilder::default().max_ray_recursion_depth(1), + RayTracePipelineInfo::builder().max_ray_recursion_depth(1), [ Shader::new_ray_gen(SHADER_RAY_GEN), Shader::new_closest_hit(SHADER_CLOSEST_HIT), @@ -345,12 +365,12 @@ fn create_ray_trace_pipeline(device: &Arc) -> Result, + device: &Device, ) -> Result<(Arc, Arc, u32, u32, Arc, Arc), DriverError> { use std::slice::from_raw_parts; @@ -398,7 +418,7 @@ fn load_scene_buffers( | vk::BufferUsageFlags::STORAGE_BUFFER, ), )?; - Buffer::copy_from_slice(&mut buf, 0, data); + buf.copy_from_slice(0, data); buf }; @@ -413,7 +433,7 @@ fn load_scene_buffers( | vk::BufferUsageFlags::STORAGE_BUFFER, ), )?; - Buffer::copy_from_slice(&mut buf, 0, data); + buf.copy_from_slice(0, data); buf }; @@ -429,7 +449,7 @@ fn load_scene_buffers( device, BufferInfo::host_mem(data.len() as _, vk::BufferUsageFlags::STORAGE_BUFFER), )?; - Buffer::copy_from_slice(&mut buf, 0, data); + buf.copy_from_slice(0, data); buf }; @@ -460,13 +480,13 @@ fn load_scene_buffers( 0.0, ] }) - .collect::>(); + .collect::>(); let buf_len = materials.len() * 64; let mut buf = Buffer::create( device, BufferInfo::host_mem(buf_len as _, vk::BufferUsageFlags::STORAGE_BUFFER), )?; - Buffer::copy_from_slice(&mut buf, 0, unsafe { + buf.copy_from_slice(0, unsafe { from_raw_parts(materials.as_ptr() as *const _, buf_len) }); buf @@ -488,7 +508,7 @@ fn main() -> anyhow::Result<()> { profile_with_puffin::init(); let args = Args::parse(); - let window = WindowBuilder::default().debug(args.debug).build()?; + let window = Window::builder().debug(args.debug).build()?; let mut cache = HashPool::new(&window.device); // ------------------------------------------------------------------------------------------ // @@ -527,23 +547,23 @@ fn main() -> anyhow::Result<()> { vk::BufferUsageFlags::SHADER_BINDING_TABLE_KHR | vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS, ) - .to_builder() + .into_builder() .alignment(shader_group_base_alignment as _), ) .unwrap(); let data = Buffer::mapped_slice_mut(&mut buf); - let rgen_handle = RayTracePipeline::group_handle(&ray_trace_pipeline, 0)?; + let rgen_handle = RayTracePipeline::group_handle(&ray_trace_pipeline, 0); data[0..rgen_handle.len()].copy_from_slice(rgen_handle); - let hit_handle = RayTracePipeline::group_handle(&ray_trace_pipeline, 1)?; + let hit_handle = RayTracePipeline::group_handle(&ray_trace_pipeline, 1); data[sbt_hit_start as usize..sbt_hit_start as usize + hit_handle.len()] .copy_from_slice(hit_handle); - let miss_handle = RayTracePipeline::group_handle(&ray_trace_pipeline, 2)?; + let miss_handle = RayTracePipeline::group_handle(&ray_trace_pipeline, 2); data[sbt_miss_start as usize..sbt_miss_start as usize + miss_handle.len()] .copy_from_slice(miss_handle); - let miss_shadow_handle = RayTracePipeline::group_handle(&ray_trace_pipeline, 3)?; + let miss_shadow_handle = RayTracePipeline::group_handle(&ray_trace_pipeline, 3); let sbt_miss_shadow_start = sbt_miss_start + shader_group_handle_alignment; data[sbt_miss_shadow_start as usize ..sbt_miss_shadow_start as usize + miss_shadow_handle.len()] @@ -551,7 +571,7 @@ fn main() -> anyhow::Result<()> { buf }); - let sbt_address = Buffer::device_address(&sbt_buf); + let sbt_address = sbt_buf.device_address(); let sbt_rgen = vk::StridedDeviceAddressRegionKHR { device_address: sbt_address, stride: shader_group_handle_size as _, @@ -584,11 +604,11 @@ fn main() -> anyhow::Result<()> { AccelerationStructureGeometry::opaque( triangle_count, AccelerationStructureGeometryData::triangles( - Buffer::device_address(&index_buf), + index_buf.device_address(), vk::IndexType::UINT32, vertex_count, None, - Buffer::device_address(&vertex_buf), + vertex_buf.device_address(), vk::Format::R32G32B32_SFLOAT, 12, ), @@ -633,7 +653,7 @@ fn main() -> anyhow::Result<()> { | vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS, ), )?; - Buffer::copy_from_slice(&mut buffer, 0, instance_data); + buffer.copy_from_slice(0, instance_data); buffer }); @@ -645,7 +665,7 @@ fn main() -> anyhow::Result<()> { let tlas_geometry_info = AccelerationStructureGeometryInfo::tlas([( AccelerationStructureGeometry::opaque( 1, - AccelerationStructureGeometryData::instances(Buffer::device_address(&instance_buf)), + AccelerationStructureGeometryData::instances(instance_buf.device_address()), ), vk::AccelerationStructureBuildRangeInfoKHR::default().primitive_count(1), )]); @@ -668,62 +688,72 @@ fn main() -> anyhow::Result<()> { .unwrap() .min_accel_struct_scratch_offset_alignment as vk::DeviceSize; - let mut render_graph = RenderGraph::new(); - let index_node = render_graph.bind_node(&index_buf); - let vertex_node = render_graph.bind_node(&vertex_buf); - let blas_node = render_graph.bind_node(&blas); + let mut graph = Graph::default(); + let index_node = graph.bind_resource(&index_buf); + let vertex_node = graph.bind_resource(&vertex_buf); + let blas_node = graph.bind_resource(&blas); { - let scratch_buf = render_graph.bind_node(Buffer::create( + let scratch_buf = graph.bind_resource(Buffer::create( &window.device, BufferInfo::device_mem( blas_size.build_size, vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS | vk::BufferUsageFlags::STORAGE_BUFFER, ) - .to_builder() + .into_builder() .alignment(accel_struct_scratch_offset_alignment), )?); - let scratch_data = render_graph.node_device_address(scratch_buf); - - render_graph - .begin_pass("Build BLAS") - .access_node(index_node, AccessType::AccelerationStructureBuildRead) - .access_node(vertex_node, AccessType::AccelerationStructureBuildRead) - .access_node(scratch_buf, AccessType::AccelerationStructureBufferWrite) - .access_node(blas_node, AccessType::AccelerationStructureBuildWrite) - .record_acceleration(move |accel, _| { - accel.build_structure(&blas_geometry_info, blas_node, scratch_data); + let scratch_data = graph.resource(scratch_buf).device_address(); + + graph + .begin_cmd() + .debug_name("Build BLAS") + .resource_access(index_node, AccessType::AccelerationStructureBuildRead) + .resource_access(vertex_node, AccessType::AccelerationStructureBuildRead) + .resource_access(scratch_buf, AccessType::AccelerationStructureBufferWrite) + .resource_access(blas_node, AccessType::AccelerationStructureBuildWrite) + .record_cmd(move |cmd| { + cmd.build_accel_struct(&[BuildAccelerationStructureInfo::new( + blas_node, + scratch_data, + blas_geometry_info, + )]); }); } { - let scratch_buf = render_graph.bind_node(Buffer::create( + let scratch_buf = graph.bind_resource(Buffer::create( &window.device, BufferInfo::device_mem( tlas_size.build_size, vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS | vk::BufferUsageFlags::STORAGE_BUFFER, ) - .to_builder() + .into_builder() .alignment(accel_struct_scratch_offset_alignment), )?); - let scratch_data = render_graph.node_device_address(scratch_buf); - let instance_node = render_graph.bind_node(&instance_buf); - let tlas_node = render_graph.bind_node(&tlas); - - render_graph - .begin_pass("Build TLAS") - .access_node(blas_node, AccessType::AccelerationStructureBuildRead) - .access_node(instance_node, AccessType::AccelerationStructureBuildRead) - .access_node(scratch_buf, AccessType::AccelerationStructureBufferWrite) - .access_node(tlas_node, AccessType::AccelerationStructureBuildWrite) - .record_acceleration(move |accel, _| { - accel.build_structure(&tlas_geometry_info, tlas_node, scratch_data); + let scratch_addr = graph.resource(scratch_buf).device_address(); + let instance_node = graph.bind_resource(&instance_buf); + let tlas_node = graph.bind_resource(&tlas); + + graph + .begin_cmd() + .debug_name("Build TLAS") + .resource_access(blas_node, AccessType::AccelerationStructureBuildRead) + .resource_access(instance_node, AccessType::AccelerationStructureBuildRead) + .resource_access(scratch_buf, AccessType::AccelerationStructureBufferWrite) + .resource_access(tlas_node, AccessType::AccelerationStructureBuildWrite) + .record_cmd(move |cmd| { + cmd.build_accel_struct(&[BuildAccelerationStructureInfo::new( + tlas_node, + scratch_addr, + tlas_geometry_info, + )]); }); } - render_graph.resolve().submit(&mut cache, 0, 0)?; + graph.into_submission().queue_submit(&mut cache, 0, 0)?; } // ------------------------------------------------------------------------------------------ // @@ -748,10 +778,10 @@ fn main() -> anyhow::Result<()> { if image.is_none() { image = Some(Arc::new( cache - .lease(ImageInfo::image_2d( + .resource(ImageInfo::image_2d( frame.width, frame.height, - frame.render_graph.node_info(frame.swapchain_image).fmt, + frame.graph.resource(frame.swapchain_image).info.fmt, vk::ImageUsageFlags::STORAGE | vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::TRANSFER_SRC, @@ -760,22 +790,22 @@ fn main() -> anyhow::Result<()> { )); } - let image_node = frame.render_graph.bind_node(image.as_ref().unwrap()); + let image_node = frame.graph.bind_resource(image.as_ref().unwrap()); { - input.step_with_window_events( - &frame - .events - .iter() - .filter_map(|event| { - if let Event::WindowEvent { event, .. } = event { - Some(event.clone()) - } else { - None - } - }) - .collect::>(), - ); + input.step(); + for event in frame.events { + match event { + Event::WindowEvent { event, .. } => { + let _ = input.process_window_event(event); + } + Event::DeviceEvent { event, .. } => { + input.process_device_event(event); + } + _ => {} + } + } + input.end_step(); const SPEED: f32 = 0.1f32; @@ -801,14 +831,15 @@ fn main() -> anyhow::Result<()> { if input.key_pressed(KeyCode::Escape) { frame_count = 0; - frame.render_graph.clear_color_image(image_node); + frame.graph.clear_color_image(image_node, [0f32; 4]); } else { frame_count += 1; } } - let camera_buf = frame.render_graph.bind_node({ + let camera_buf = frame.graph.bind_resource({ #[repr(C)] + #[derive(Clone, Copy, Pod, Zeroable)] struct Camera { position: [f32; 4], right: [f32; 4], @@ -818,60 +849,59 @@ fn main() -> anyhow::Result<()> { } let mut buf = cache - .lease(BufferInfo::host_mem( + .resource(BufferInfo::host_mem( size_of::() as _, vk::BufferUsageFlags::UNIFORM_BUFFER, )) .unwrap(); - Buffer::copy_from_slice(&mut buf, 0, unsafe { - std::slice::from_raw_parts( - &Camera { - position, - right, - up, - forward, - frame_count, - } as *const _ as *const _, - size_of::(), - ) - }); + buf.copy_from_slice( + 0, + bytes_of(&Camera { + position, + right, + up, + forward, + frame_count, + }), + ); buf }); - let blas_node = frame.render_graph.bind_node(&blas); - let tlas_node = frame.render_graph.bind_node(&tlas); - let index_buf_node = frame.render_graph.bind_node(&index_buf); - let vertex_buf_node = frame.render_graph.bind_node(&vertex_buf); - let material_id_buf_node = frame.render_graph.bind_node(&material_id_buf); - let material_buf_node = frame.render_graph.bind_node(&material_buf); - let sbt_node = frame.render_graph.bind_node(&sbt_buf); + let blas_node = frame.graph.bind_resource(&blas); + let tlas_node = frame.graph.bind_resource(&tlas); + let index_buf_node = frame.graph.bind_resource(&index_buf); + let vertex_buf_node = frame.graph.bind_resource(&vertex_buf); + let material_id_buf_node = frame.graph.bind_resource(&material_id_buf); + let material_buf_node = frame.graph.bind_resource(&material_buf); + let sbt_node = frame.graph.bind_resource(&sbt_buf); frame - .render_graph - .begin_pass("basic ray tracer") + .graph + .begin_cmd() + .debug_name("basic ray tracer") .bind_pipeline(&ray_trace_pipeline) - .access_node( + .resource_access( blas_node, AccessType::RayTracingShaderReadAccelerationStructure, ) - .access_node(sbt_node, AccessType::RayTracingShaderReadOther) - .access_descriptor( + .resource_access(sbt_node, AccessType::RayTracingShaderReadOther) + .shader_resource_access( 0, tlas_node, AccessType::RayTracingShaderReadAccelerationStructure, ) - .access_descriptor(1, camera_buf, AccessType::RayTracingShaderReadOther) - .access_descriptor(2, index_buf_node, AccessType::RayTracingShaderReadOther) - .access_descriptor(3, vertex_buf_node, AccessType::RayTracingShaderReadOther) - .write_descriptor(4, image_node) - .access_descriptor( + .shader_resource_access(1, camera_buf, AccessType::RayTracingShaderReadOther) + .shader_resource_access(2, index_buf_node, AccessType::RayTracingShaderReadOther) + .shader_resource_access(3, vertex_buf_node, AccessType::RayTracingShaderReadOther) + .shader_resource_access(4, image_node, AccessType::AnyShaderWrite) + .shader_resource_access( 5, material_id_buf_node, AccessType::RayTracingShaderReadOther, ) - .access_descriptor(6, material_buf_node, AccessType::RayTracingShaderReadOther) - .record_ray_trace(move |ray_trace, _| { - ray_trace.trace_rays( + .shader_resource_access(6, material_buf_node, AccessType::RayTracingShaderReadOther) + .record_cmd(move |cmd| { + cmd.trace_rays( &sbt_rgen, &sbt_miss, &sbt_hit, @@ -881,7 +911,7 @@ fn main() -> anyhow::Result<()> { 1, ); }) - .submit_pass() + .end_cmd() .copy_image(image_node, frame.swapchain_image); })?; diff --git a/examples/rt_triangle.rs b/examples/rt_triangle.rs index 6024f7d2..2c22743e 100644 --- a/examples/rt_triangle.rs +++ b/examples/rt_triangle.rs @@ -1,18 +1,39 @@ mod profile_with_puffin; use { - bytemuck::{NoUninit, cast_slice}, + ash::vk, + bytemuck::{Pod, Zeroable, cast_slice}, clap::Parser, - inline_spirv::inline_spirv, - screen_13::prelude::*, - screen_13_window::WindowBuilder, std::sync::Arc, + vk_graph::{ + Graph, + cmd::BuildAccelerationStructureInfo, + driver::{ + DriverError, + accel_struct::{ + AccelerationStructure, AccelerationStructureGeometry, + AccelerationStructureGeometryData, AccelerationStructureGeometryInfo, + AccelerationStructureInfo, + }, + buffer::{Buffer, BufferInfo}, + device::Device, + physical_device::RayTraceProperties, + ray_trace::{RayTracePipeline, RayTracePipelineInfo, RayTraceShaderGroup}, + shader::Shader, + }, + pool::hash::HashPool, + }, + vk_graph_window::Window, + vk_shader_macros::glsl, + vk_sync::AccessType, }; -static SHADER_RAY_GEN: &[u32] = inline_spirv!( +static SHADER_RAY_GEN: &[u32] = glsl!( + target: vulkan1_2, r#" #version 460 #extension GL_EXT_ray_tracing : enable + #pragma shader_stage(raygen) layout(binding = 0, set = 0) uniform accelerationStructureEXT topLevelAS; layout(binding = 1, set = 0, rgba32f) uniform image2D image; @@ -31,21 +52,33 @@ static SHADER_RAY_GEN: &[u32] = inline_spirv!( float tmin = 0.001; float tmax = 10000.0; - traceRayEXT(topLevelAS, gl_RayFlagsOpaqueEXT, 0xff, 0, 0, 0, origin.xyz, tmin, direction.xyz, tmax, 0); + traceRayEXT( + topLevelAS, + gl_RayFlagsOpaqueEXT, + 0xff, + 0, + 0, + 0, + origin.xyz, + tmin, + direction.xyz, + tmax, + 0 + ); imageStore(image, ivec2(gl_LaunchIDEXT.xy), vec4(hitValue, 0.0)); } - "#, - rgen, - vulkan1_2 + "# ) .as_slice(); -static SHADER_CLOSEST_HIT: &[u32] = inline_spirv!( +static SHADER_CLOSEST_HIT: &[u32] = glsl!( + target: vulkan1_2, r#" #version 460 #extension GL_EXT_ray_tracing : enable #extension GL_EXT_nonuniform_qualifier : enable + #pragma shader_stage(closest) layout(location = 0) rayPayloadInEXT vec3 resultColor; hitAttributeEXT vec2 attribs; @@ -54,32 +87,30 @@ static SHADER_CLOSEST_HIT: &[u32] = inline_spirv!( const vec3 barycentricCoords = vec3(1.0f - attribs.x - attribs.y, attribs.x, attribs.y); resultColor = barycentricCoords; } - "#, - rchit, - vulkan1_2 + "# ) .as_slice(); -static SHADER_MISS: &[u32] = inline_spirv!( +static SHADER_MISS: &[u32] = glsl!( + target: vulkan1_2, r#" #version 460 #extension GL_EXT_ray_tracing : enable + #pragma shader_stage(miss) layout(location = 0) rayPayloadInEXT vec3 hitValue; void main() { hitValue = vec3(0.0, 0.0, 0.2); } - "#, - rmiss, - vulkan1_2 + "# ) .as_slice(); -fn create_ray_trace_pipeline(device: &Arc) -> Result, DriverError> { - Ok(Arc::new(RayTracePipeline::create( +fn create_ray_trace_pipeline(device: &Device) -> Result { + RayTracePipeline::create( device, - RayTracePipelineInfoBuilder::default().max_ray_recursion_depth(1), + RayTracePipelineInfo::builder().max_ray_recursion_depth(1), [ Shader::new_ray_gen(SHADER_RAY_GEN), Shader::new_closest_hit(SHADER_CLOSEST_HIT), @@ -90,7 +121,7 @@ fn create_ray_trace_pipeline(device: &Arc) -> Result anyhow::Result<()> { profile_with_puffin::init(); let args = Args::parse(); - let window = WindowBuilder::default().debug(args.debug).build()?; + let window = Window::builder().debug(args.debug).build()?; let mut pool = HashPool::new(&window.device); // ------------------------------------------------------------------------------------------ // @@ -136,27 +167,27 @@ fn main() -> anyhow::Result<()> { vk::BufferUsageFlags::SHADER_BINDING_TABLE_KHR | vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS, ) - .to_builder() + .into_builder() .alignment(shader_group_base_alignment as _), ) .unwrap(); let data = Buffer::mapped_slice_mut(&mut buf); - let rgen_handle = RayTracePipeline::group_handle(&ray_trace_pipeline, 0)?; + let rgen_handle = RayTracePipeline::group_handle(&ray_trace_pipeline, 0); data[0..rgen_handle.len()].copy_from_slice(rgen_handle); - let hit_handle = RayTracePipeline::group_handle(&ray_trace_pipeline, 1)?; + let hit_handle = RayTracePipeline::group_handle(&ray_trace_pipeline, 1); data[sbt_hit_start as usize..sbt_hit_start as usize + hit_handle.len()] .copy_from_slice(hit_handle); - let miss_handle = RayTracePipeline::group_handle(&ray_trace_pipeline, 2)?; + let miss_handle = RayTracePipeline::group_handle(&ray_trace_pipeline, 2); data[sbt_miss_start as usize..sbt_miss_start as usize + miss_handle.len()] .copy_from_slice(miss_handle); buf }); - let sbt_address = Buffer::device_address(&sbt_buf); + let sbt_address = sbt_buf.device_address(); let sbt_rgen = vk::StridedDeviceAddressRegionKHR { device_address: sbt_address, stride: shader_group_handle_size as _, @@ -182,7 +213,7 @@ fn main() -> anyhow::Result<()> { let vertex_count = triangle_count * 3; #[repr(C)] - #[derive(Debug, Clone, Copy, NoUninit)] + #[derive(Debug, Clone, Copy, Pod, Zeroable)] #[allow(dead_code)] struct Vertex { pos: [f32; 3], @@ -212,7 +243,7 @@ fn main() -> anyhow::Result<()> { | vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS, ), )?; - Buffer::copy_from_slice(&mut buf, 0, data); + buf.copy_from_slice(0, data); Arc::new(buf) }; @@ -226,7 +257,7 @@ fn main() -> anyhow::Result<()> { | vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS, ), )?; - Buffer::copy_from_slice(&mut buf, 0, data); + buf.copy_from_slice(0, data); Arc::new(buf) }; @@ -238,11 +269,11 @@ fn main() -> anyhow::Result<()> { AccelerationStructureGeometry::opaque( triangle_count, AccelerationStructureGeometryData::triangles( - Buffer::device_address(&index_buf), + index_buf.device_address(), vk::IndexType::UINT32, vertex_count, None, - Buffer::device_address(&vertex_buf), + vertex_buf.device_address(), vk::Format::R32G32B32_SFLOAT, 12, ), @@ -287,7 +318,7 @@ fn main() -> anyhow::Result<()> { | vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS, ), )?; - Buffer::copy_from_slice(&mut buffer, 0, instance_data); + buffer.copy_from_slice(0, instance_data); buffer }); @@ -299,7 +330,7 @@ fn main() -> anyhow::Result<()> { let tlas_geometry_info = AccelerationStructureGeometryInfo::tlas([( AccelerationStructureGeometry::opaque( 1, - AccelerationStructureGeometryData::instances(Buffer::device_address(&instance_buf)), + AccelerationStructureGeometryData::instances(instance_buf.device_address()), ), vk::AccelerationStructureBuildRangeInfoKHR::default().primitive_count(1), )]); @@ -322,62 +353,72 @@ fn main() -> anyhow::Result<()> { .unwrap() .min_accel_struct_scratch_offset_alignment as vk::DeviceSize; - let mut render_graph = RenderGraph::new(); - let index_node = render_graph.bind_node(&index_buf); - let vertex_node = render_graph.bind_node(&vertex_buf); - let blas_node = render_graph.bind_node(&blas); + let mut graph = Graph::default(); + let index_node = graph.bind_resource(&index_buf); + let vertex_node = graph.bind_resource(&vertex_buf); + let blas_node = graph.bind_resource(&blas); { - let scratch_buf = render_graph.bind_node(Buffer::create( + let scratch_buf = graph.bind_resource(Buffer::create( &window.device, BufferInfo::device_mem( blas_size.build_size, vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS | vk::BufferUsageFlags::STORAGE_BUFFER, ) - .to_builder() + .into_builder() .alignment(accel_struct_scratch_offset_alignment), )?); - let scratch_data = render_graph.node_device_address(scratch_buf); - - render_graph - .begin_pass("Build BLAS") - .access_node(index_node, AccessType::AccelerationStructureBuildRead) - .access_node(vertex_node, AccessType::AccelerationStructureBuildRead) - .access_node(scratch_buf, AccessType::AccelerationStructureBufferWrite) - .access_node(blas_node, AccessType::AccelerationStructureBuildWrite) - .record_acceleration(move |accel, _| { - accel.build_structure(&blas_geometry_info, blas_node, scratch_data); + let scratch_addr = graph.resource(scratch_buf).device_address(); + + graph + .begin_cmd() + .debug_name("Build BLAS") + .resource_access(index_node, AccessType::AccelerationStructureBuildRead) + .resource_access(vertex_node, AccessType::AccelerationStructureBuildRead) + .resource_access(scratch_buf, AccessType::AccelerationStructureBufferWrite) + .resource_access(blas_node, AccessType::AccelerationStructureBuildWrite) + .record_cmd(move |cmd| { + cmd.build_accel_struct(&[BuildAccelerationStructureInfo::new( + blas_node, + scratch_addr, + blas_geometry_info, + )]); }); } { - let instance_node = render_graph.bind_node(instance_buf); - let scratch_buf = render_graph.bind_node(Buffer::create( + let instance_node = graph.bind_resource(instance_buf); + let scratch_buf = graph.bind_resource(Buffer::create( &window.device, BufferInfo::device_mem( tlas_size.build_size, vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS | vk::BufferUsageFlags::STORAGE_BUFFER, ) - .to_builder() + .into_builder() .alignment(accel_struct_scratch_offset_alignment), )?); - let scratch_data = render_graph.node_device_address(scratch_buf); - let tlas_node = render_graph.bind_node(&tlas); - - render_graph - .begin_pass("Build TLAS") - .access_node(blas_node, AccessType::AccelerationStructureBuildRead) - .access_node(instance_node, AccessType::AccelerationStructureBuildRead) - .access_node(scratch_buf, AccessType::AccelerationStructureBufferWrite) - .access_node(tlas_node, AccessType::AccelerationStructureBuildWrite) - .record_acceleration(move |accel, _| { - accel.build_structure(&tlas_geometry_info, tlas_node, scratch_data); + let scratch_addr = graph.resource(scratch_buf).device_address(); + let tlas_node = graph.bind_resource(&tlas); + + graph + .begin_cmd() + .debug_name("Build TLAS") + .resource_access(blas_node, AccessType::AccelerationStructureBuildRead) + .resource_access(instance_node, AccessType::AccelerationStructureBuildRead) + .resource_access(scratch_buf, AccessType::AccelerationStructureBufferWrite) + .resource_access(tlas_node, AccessType::AccelerationStructureBuildWrite) + .record_cmd(move |cmd| { + cmd.build_accel_struct(&[BuildAccelerationStructureInfo::new( + tlas_node, + scratch_addr, + tlas_geometry_info, + )]); }); } - render_graph.resolve().submit(&mut pool, 0, 0)?; + graph.into_submission().queue_submit(&mut pool, 0, 0)?; } // ------------------------------------------------------------------------------------------ // @@ -388,27 +429,28 @@ fn main() -> anyhow::Result<()> { // - Trace the image // - Copy image to the swapchain window.run(|frame| { - let blas_node = frame.render_graph.bind_node(&blas); - let tlas_node = frame.render_graph.bind_node(&tlas); - let sbt_node = frame.render_graph.bind_node(&sbt_buf); + let blas_node = frame.graph.bind_resource(&blas); + let tlas_node = frame.graph.bind_resource(&tlas); + let sbt_node = frame.graph.bind_resource(&sbt_buf); frame - .render_graph - .begin_pass("ray-traced triangle") + .graph + .begin_cmd() + .debug_name("ray-traced triangle") .bind_pipeline(&ray_trace_pipeline) - .access_node( + .resource_access( blas_node, AccessType::RayTracingShaderReadAccelerationStructure, ) - .access_node(sbt_node, AccessType::RayTracingShaderReadOther) - .access_descriptor( + .resource_access(sbt_node, AccessType::RayTracingShaderReadOther) + .shader_resource_access( 0, tlas_node, AccessType::RayTracingShaderReadAccelerationStructure, ) - .write_descriptor(1, frame.swapchain_image) - .record_ray_trace(move |ray_trace, _| { - ray_trace.trace_rays( + .shader_resource_access(1, frame.swapchain_image, AccessType::AnyShaderWrite) + .record_cmd(move |cmd| { + cmd.trace_rays( &sbt_rgen, &sbt_miss, &sbt_hit, @@ -418,7 +460,7 @@ fn main() -> anyhow::Result<()> { 1, ); }) - .submit_pass(); + .end_cmd(); })?; Ok(()) diff --git a/examples/shader-toy/Cargo.toml b/examples/shader-toy/Cargo.toml index 44bab96b..0e6a7941 100644 --- a/examples/shader-toy/Cargo.toml +++ b/examples/shader-toy/Cargo.toml @@ -1,11 +1,12 @@ [package] name = "shader-toy" -version = "0.1.0" authors = ["John Wells "] -edition = "2021" -license = "MIT OR Apache-2.0" -readme = "README.md" description = "Example api usage" +edition.workspace = true +license.workspace = true +publish = false +readme.workspace = true +repository.workspace = true [features] default = ["include-pak"] @@ -13,18 +14,18 @@ include-pak = [] [dependencies] anyhow = "1.0" -bytemuck = "1.14" -clap = { version = "4.5", features = ["derive"] } -pak = "0.5" +bytemuck = { version = "1.25", features = ["derive"] } +clap = { version = "4.6", features = ["derive"] } +pak = "0.7" pretty_env_logger = "0.5" -screen-13 = { path = "../.." } -screen-13-fx = { path = "../../contrib/screen-13-fx" } -screen-13-window = { path = "../../contrib/screen-13-window" } -winit = "0.30" +vk-graph.workspace = true +vk-graph-fx.workspace = true +vk-graph-window.workspace = true +winit.workspace = true [build-dependencies] anyhow = "1.0" -pak = { version = "0.5", features = ["bake"] } +pak = { version = "0.7", features = ["bake"] } paste = "1.0" shader-prepper = "0.3.0-pre.3" -shaderc = "0.8" +shaderc = "0.10" diff --git a/examples/shader-toy/build.rs b/examples/shader-toy/build.rs index 91813984..80a1c620 100644 --- a/examples/shader-toy/build.rs +++ b/examples/shader-toy/build.rs @@ -169,8 +169,8 @@ fn remove_common_path( fn read_shader_source(path: impl AsRef) -> String { use { shader_prepper::{ - process_file, BoxedIncludeProviderError, IncludeProvider, ResolvedInclude, - ResolvedIncludePath, + BoxedIncludeProviderError, IncludeProvider, ResolvedInclude, ResolvedIncludePath, + process_file, }, std::fs::read_to_string, }; diff --git a/examples/shader-toy/src/main.rs b/examples/shader-toy/src/main.rs index ea1de611..f51bcdda 100644 --- a/examples/shader-toy/src/main.rs +++ b/examples/shader-toy/src/main.rs @@ -34,13 +34,24 @@ mod res { use { anyhow::Context, - bytemuck::{bytes_of, Pod, Zeroable}, + bytemuck::{Pod, Zeroable, bytes_of}, clap::Parser, pak::{Pak, PakBuf}, - screen_13::prelude::*, - screen_13_fx::*, - screen_13_window::WindowBuilder, - std::{sync::Arc, time::Instant}, + std::time::Instant, + vk_graph::{ + Graph, + cmd::{LoadOp, StoreOp}, + driver::{ + ash::vk, + graphic::{GraphicPipeline, GraphicPipelineInfo}, + image::ImageInfo, + shader::Shader, + sync::AccessType, + }, + pool::{Pool as _, lazy::LazyPool}, + }, + vk_graph_fx::*, + vk_graph_window::Window, winit::dpi::PhysicalSize, }; @@ -48,9 +59,9 @@ fn main() -> anyhow::Result<()> { pretty_env_logger::init(); let args = Args::parse(); - let window = WindowBuilder::default() + let window = Window::builder() .debug(args.debug) - .desired_image_count(3) + .min_image_count(3) .window(|builder| builder.with_inner_size(PhysicalSize::new(1280.0f64, 720.0f64))) .build()?; let display = GraphicPresenter::new(&window.device).context("Presenter")?; @@ -94,33 +105,29 @@ fn main() -> anyhow::Result<()> { // no depth/stencil // 1x sample count // one-sided - let buffer_pipeline = Arc::new( - GraphicPipeline::create( - &window.device, - GraphicPipelineInfo::default(), - [ - Shader::new_vertex(res::shader::QUAD_VERT), - Shader::new_fragment(res::shader::FLOCKAROO_BUF_FRAG), - ], - ) - .context("FLOCKAROO_BUF_FRAG")?, - ); - let image_pipeline = Arc::new( - GraphicPipeline::create( - &window.device, - GraphicPipelineInfo::default(), - [ - Shader::new_vertex(res::shader::QUAD_VERT), - Shader::new_fragment(res::shader::FLOCKAROO_IMG_FRAG), - ], - ) - .context("FLOCKAROO_IMG_FRAG")?, - ); + let buffer_pipeline = GraphicPipeline::create( + &window.device, + GraphicPipelineInfo::default(), + [ + Shader::new_vertex(res::shader::QUAD_VERT), + Shader::new_fragment(res::shader::FLOCKAROO_BUF_FRAG), + ], + ) + .context("FLOCKAROO_BUF_FRAG")?; + let image_pipeline = GraphicPipeline::create( + &window.device, + GraphicPipelineInfo::default(), + [ + Shader::new_vertex(res::shader::QUAD_VERT), + Shader::new_fragment(res::shader::FLOCKAROO_IMG_FRAG), + ], + ) + .context("FLOCKAROO_IMG_FRAG")?; - let mut render_graph = RenderGraph::new(); - let blank_image = render_graph.bind_node( + let mut graph = Graph::default(); + let blank_image = graph.bind_resource( cache - .lease(ImageInfo::image_2d( + .resource(ImageInfo::image_2d( 8, 8, vk::Format::R8G8B8A8_SRGB, @@ -130,9 +137,9 @@ fn main() -> anyhow::Result<()> { ); let (width, height) = (1280, 720); - let framebuffer_image = render_graph.bind_node( + let framebuffer_image = graph.bind_resource( cache - .lease(ImageInfo::image_2d( + .resource(ImageInfo::image_2d( width, height, vk::Format::R8G8B8A8_SRGB, @@ -143,9 +150,9 @@ fn main() -> anyhow::Result<()> { )) .context("Framebuffer image")?, ); - let temp_image = render_graph.bind_node( + let temp_image = graph.bind_resource( cache - .lease(ImageInfo::image_2d( + .resource(ImageInfo::image_2d( width, height, vk::Format::R8G8B8A8_SRGB, @@ -157,18 +164,19 @@ fn main() -> anyhow::Result<()> { .context("Temp image")?, ); - render_graph - .clear_color_image_value(framebuffer_image, [1.0, 1.0, 0.0, 1.0]) - .clear_color_image_value(blank_image, [0.0, 0.0, 0.0, 1.0]) - .clear_color_image_value(temp_image, [0.0, 1.0, 0.0, 1.0]); + graph + .clear_color_image(framebuffer_image, [1.0, 1.0, 0.0, 1.0]) + .clear_color_image(blank_image, [0.0, 0.0, 0.0, 1.0]) + .clear_color_image(temp_image, [0.0, 1.0, 0.0, 1.0]); - let mut framebuffer_image_binding = Some(render_graph.unbind_node(framebuffer_image)); - let mut blank_image_binding = Some(render_graph.unbind_node(blank_image)); - let mut temp_image_binding = Some(render_graph.unbind_node(temp_image)); + let mut framebuffer_image_binding = Some(graph.resource(framebuffer_image).clone()); + let mut blank_image_binding = Some(graph.resource(blank_image).clone()); + let mut temp_image_binding = Some(graph.resource(temp_image).clone()); - render_graph.resolve().submit(&mut cache, 0, 0)?; + graph.into_submission().queue_submit(&mut cache, 0, 0)?; let started_at = Instant::now(); + let mut prev_frame_at = started_at; let mut count = 0i32; let framebuffer_info = framebuffer_image_binding.as_ref().unwrap().info; let flowers_image_info = flowers_image_binding.as_ref().unwrap().info; @@ -177,33 +185,38 @@ fn main() -> anyhow::Result<()> { window .run(|frame| { + let now = Instant::now(); + // Update the stuff any shader toy shader would want to know each frame - let elapsed = Instant::now() - started_at; + let dt = now - prev_frame_at; + prev_frame_at = now; + + let elapsed = now - started_at; count += 1; // Bind things to this graph (the graph will own our things until we unbind them) let flowers_image = frame - .render_graph - .bind_node(flowers_image_binding.take().unwrap()); + .graph + .bind_resource(flowers_image_binding.take().unwrap()); let noise_image = frame - .render_graph - .bind_node(noise_image_binding.take().unwrap()); + .graph + .bind_resource(noise_image_binding.take().unwrap()); let framebuffer_image = frame - .render_graph - .bind_node(framebuffer_image_binding.take().unwrap()); + .graph + .bind_resource(framebuffer_image_binding.take().unwrap()); let blank_image = frame - .render_graph - .bind_node(blank_image_binding.take().unwrap()); + .graph + .bind_resource(blank_image_binding.take().unwrap()); let temp_image = frame - .render_graph - .bind_node(temp_image_binding.take().unwrap()); + .graph + .bind_resource(temp_image_binding.take().unwrap()); // We need to push a shader-toy defined set of constants to each pipeline - any copy // type will do but we are getting fancy here by defining a struct to be super precise // about what we're doing - but you may want to just send a bunch of f32's #[repr(C)] - #[derive(Clone, Copy)] + #[derive(Clone, Copy, Pod, Zeroable)] struct PushConstants { resolution: [f32; 3], _pad_1: u32, @@ -217,25 +230,6 @@ fn main() -> anyhow::Result<()> { channel_resolution: [f32; 16], } - unsafe impl Pod for PushConstants {} - - unsafe impl Zeroable for PushConstants { - fn zeroed() -> Self { - Self { - resolution: [0f32; 3], - _pad_1: 0u32, - date: [0f32; 4], - mouse: [0f32; 4], - time: 0f32, - time_delta: 0f32, - frame: 0i32, - sample_rate: 0f32, - channel_time: [0f32; 4], - channel_resolution: [0f32; 16], - } - } - } - // Each pipeline gets the same constant data let push_consts = PushConstants { resolution: [frame.width as f32, frame.height as _, 1.0], @@ -243,7 +237,7 @@ fn main() -> anyhow::Result<()> { date: [1970.0, 1.0, 1.0, elapsed.as_secs_f32()], mouse: [0.0, 0.0, 0.0, 0.0], time: elapsed.as_secs_f32(), - time_delta: 0.016, + time_delta: dt.as_secs_f32(), frame: count, sample_rate: 44100.0, channel_time: [ @@ -280,40 +274,62 @@ fn main() -> anyhow::Result<()> { // Fill a buffer using a single-pass CFD pipeline where previous output feeds next input frame - .render_graph - .begin_pass("Buffer A") + .graph + .begin_cmd() + .debug_name("Buffer A") .bind_pipeline(&buffer_pipeline) - .read_descriptor(0, input) - .read_descriptor(1, noise_image) - .read_descriptor(2, flowers_image) - .read_descriptor(3, blank_image) - .store_color(0, output) - .record_subpass(move |subpass, _| { - subpass.push_constants(bytes_of(&push_consts)); - subpass.draw(6, 1, 0, 0); + .shader_resource_access( + 0, + input, + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + .shader_resource_access( + 1, + noise_image, + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + .shader_resource_access( + 2, + flowers_image, + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + .shader_resource_access( + 3, + blank_image, + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + .color_attachment_image(0, output, LoadOp::DontCare, StoreOp::Store) + .record_cmd(move |cmd| { + cmd.push_constants(0, bytes_of(&push_consts)) + .draw(6, 1, 0, 0); }); // Make the CFD look more like paint with a second pass frame - .render_graph - .begin_pass("Image") + .graph + .begin_cmd() + .debug_name("Image") .bind_pipeline(&image_pipeline) - .read_descriptor(0, output) - .store_color(0, input) - .record_subpass(move |subpass, _| { - subpass.push_constants(bytes_of(&push_consts)); - subpass.draw(6, 1, 0, 0); + .shader_resource_access( + 0, + output, + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + .color_attachment_image(0, input, LoadOp::DontCare, StoreOp::Store) + .record_cmd(move |cmd| { + cmd.push_constants(0, bytes_of(&push_consts)) + .draw(6, 1, 0, 0); }); // Done! - display.present_image(frame.render_graph, input, frame.swapchain_image); + display.present_image(frame.graph, input, frame.swapchain_image); // Unbind things from this graph (we want them back for the next frame!) - flowers_image_binding = Some(frame.render_graph.unbind_node(flowers_image)); - noise_image_binding = Some(frame.render_graph.unbind_node(noise_image)); - framebuffer_image_binding = Some(frame.render_graph.unbind_node(framebuffer_image)); - blank_image_binding = Some(frame.render_graph.unbind_node(blank_image)); - temp_image_binding = Some(frame.render_graph.unbind_node(temp_image)); + flowers_image_binding = Some(frame.graph.resource(flowers_image).clone()); + noise_image_binding = Some(frame.graph.resource(noise_image).clone()); + framebuffer_image_binding = Some(frame.graph.resource(framebuffer_image).clone()); + blank_image_binding = Some(frame.graph.resource(blank_image).clone()); + temp_image_binding = Some(frame.graph.resource(temp_image).clone()); }) .context("Unable to run event loop")?; diff --git a/examples/skeletal-anim/Cargo.toml b/examples/skeletal-anim/Cargo.toml index 115cfa32..74700171 100644 --- a/examples/skeletal-anim/Cargo.toml +++ b/examples/skeletal-anim/Cargo.toml @@ -1,23 +1,24 @@ [package] -name = "animation" -version = "0.1.0" +name = "skeletal-anim" authors = ["John Wells "] -edition = "2021" -license = "MIT OR Apache-2.0" -readme = "README.md" +edition.workspace = true +license.workspace = true +publish = false +readme.workspace = true +repository.workspace = true [dependencies] -bytemuck = "1.14" +bytemuck = { version = "1.25", features = ["derive"] } clap = { version = "4.5", features = ["derive"] } -glam = { version = "0.27", features = ["bytemuck"] } -pak = "=0.5.0" +glam = { version = "0.32", features = ["bytemuck"] } +pak = "0.7.1" pretty_env_logger = "0.5" -screen-13 = { path = "../.." } -screen-13-window = { path = "../../contrib/screen-13-window" } +vk-graph.workspace = true +vk-graph-window.workspace = true [build-dependencies] anyhow = "1.0" -log = "0.4" -pak = { version = "=0.5.0", features = ["bake"] } -shaderc = "0.8" +log.workspace = true +pak = { version = "0.7.1", features = ["bake"] } +shaderc = "0.10" simplelog = "0.12" diff --git a/examples/skeletal-anim/README.md b/examples/skeletal-anim/README.md index 960ddbf1..f6f1be12 100644 --- a/examples/skeletal-anim/README.md +++ b/examples/skeletal-anim/README.md @@ -2,10 +2,10 @@ # Animation Example -Demonstrates loading and rendering skeletal mesh animation using [`pak`] and [`screen-13`]. +Demonstrates loading and rendering skeletal mesh animation using [`pak`] and [`vk-graph`]. [`pak`]: https://github.com/attackgoat/pak -[`screen-13`]: https://github.com/attackgoat/screen-13 +[`vk-graph`]: https://github.com/attackgoat/vk-graph # Assets diff --git a/examples/skeletal-anim/build.rs b/examples/skeletal-anim/build.rs index dae28c07..c03e79fa 100644 --- a/examples/skeletal-anim/build.rs +++ b/examples/skeletal-anim/build.rs @@ -1,11 +1,11 @@ use { - anyhow::Context, + anyhow::{Context, bail}, pak::PakBuf, shaderc::{Compiler, ShaderKind}, simplelog::{CombinedLogger, ConfigBuilder, LevelFilter, WriteLogger}, std::{ env::var, - fs::{read_to_string, write, File}, + fs::{File, read_to_string, write}, path::{Path, PathBuf}, }, }; @@ -66,7 +66,7 @@ fn compile_shader( { "frag" => ShaderKind::Fragment, "vert" => ShaderKind::Vertex, - _ => unimplemented!(), + ext => bail!("unsupported shader extension: {ext}"), }, &source_path.to_string_lossy(), "main", diff --git a/examples/skeletal-anim/res/animated_characters_3/character_medium.toml b/examples/skeletal-anim/res/animated_characters_3/character_medium.toml index 094d8b20..dcf7ebd7 100644 --- a/examples/skeletal-anim/res/animated_characters_3/character_medium.toml +++ b/examples/skeletal-anim/res/animated_characters_3/character_medium.toml @@ -1,3 +1,2 @@ -[model] -src = 'character_medium.glb' +[mesh] tangents = false diff --git a/examples/skeletal-anim/res/animated_characters_3/human_female.toml b/examples/skeletal-anim/res/animated_characters_3/human_female.toml index 2fa1a092..912d6c4d 100644 --- a/examples/skeletal-anim/res/animated_characters_3/human_female.toml +++ b/examples/skeletal-anim/res/animated_characters_3/human_female.toml @@ -1,3 +1,2 @@ [bitmap] -src = 'human_female.png' swizzle = 'rgba' diff --git a/examples/skeletal-anim/res/animated_characters_3/human_male.toml b/examples/skeletal-anim/res/animated_characters_3/human_male.toml index 2c801c11..912d6c4d 100644 --- a/examples/skeletal-anim/res/animated_characters_3/human_male.toml +++ b/examples/skeletal-anim/res/animated_characters_3/human_male.toml @@ -1,3 +1,2 @@ [bitmap] -src = 'human_male.png' swizzle = 'rgba' diff --git a/examples/skeletal-anim/res/animated_characters_3/idle.toml b/examples/skeletal-anim/res/animated_characters_3/idle.toml index 70dda5e8..76f1da8b 100644 --- a/examples/skeletal-anim/res/animated_characters_3/idle.toml +++ b/examples/skeletal-anim/res/animated_characters_3/idle.toml @@ -1,3 +1,2 @@ [animation] name = 'Root|Root|Idle' -src = 'idle.glb' \ No newline at end of file diff --git a/examples/skeletal-anim/res/animated_characters_3/run.toml b/examples/skeletal-anim/res/animated_characters_3/run.toml index 50c98c17..f5577ede 100644 --- a/examples/skeletal-anim/res/animated_characters_3/run.toml +++ b/examples/skeletal-anim/res/animated_characters_3/run.toml @@ -1,3 +1,2 @@ [animation] name = 'Root|Root|Run' -src = 'run.glb' \ No newline at end of file diff --git a/examples/skeletal-anim/res/animated_characters_3/zombie_female.toml b/examples/skeletal-anim/res/animated_characters_3/zombie_female.toml index 5faa0c58..912d6c4d 100644 --- a/examples/skeletal-anim/res/animated_characters_3/zombie_female.toml +++ b/examples/skeletal-anim/res/animated_characters_3/zombie_female.toml @@ -1,3 +1,2 @@ [bitmap] -src = 'zombie_female.png' swizzle = 'rgba' diff --git a/examples/skeletal-anim/res/animated_characters_3/zombie_male.toml b/examples/skeletal-anim/res/animated_characters_3/zombie_male.toml index 92bc3e1c..912d6c4d 100644 --- a/examples/skeletal-anim/res/animated_characters_3/zombie_male.toml +++ b/examples/skeletal-anim/res/animated_characters_3/zombie_male.toml @@ -1,3 +1,2 @@ [bitmap] -src = 'zombie_male.png' swizzle = 'rgba' diff --git a/examples/skeletal-anim/src/main.rs b/examples/skeletal-anim/src/main.rs index 684c0602..d274f88c 100644 --- a/examples/skeletal-anim/src/main.rs +++ b/examples/skeletal-anim/src/main.rs @@ -1,15 +1,13 @@ use { - bytemuck::{bytes_of, cast_slice, NoUninit}, + bytemuck::{Pod, Zeroable, bytes_of, cast_slice}, clap::Parser, glam::{Mat4, Quat, Vec3}, pak::{ + Pak, PakBuf, anim::{Channel, Interpolation, Outputs}, bitmap::BitmapFormat, - model::{Joint, Vertex}, - Pak, PakBuf, + mesh::{Joint, VertexType}, }, - screen_13::prelude::*, - screen_13_window::{WindowBuilder, WindowError}, std::{ cmp::Ordering, env::current_exe, @@ -18,6 +16,22 @@ use { sync::Arc, time::{Duration, Instant}, }, + vk_graph::{ + Graph, + cmd::{LoadOp, StoreOp}, + driver::{ + DriverError, + ash::vk, + buffer::{Buffer, BufferInfo}, + device::Device, + graphic::{DepthStencilInfo, GraphicPipeline, GraphicPipelineInfoBuilder}, + image::{Image, ImageInfo}, + shader::Shader, + sync::AccessType, + }, + pool::{Pool as _, hash::HashPool, lazy::LazyPool}, + }, + vk_graph_window::{Window, WindowError}, }; // This blog has a really good overview of what is happening here: @@ -29,7 +43,7 @@ fn main() -> Result<(), WindowError> { let mut pak = PakBuf::open(pak_path).unwrap(); let args = Args::parse(); - let window = WindowBuilder::default().debug(args.debug).build()?; + let window = Window::builder().debug(args.debug).build()?; let device = &window.device; let pipeline = create_pipeline(device, &mut pak)?; @@ -42,15 +56,21 @@ fn main() -> Result<(), WindowError> { let mut run = Animation::load(&character, &mut pak, "animated_characters_3/run")?; let mut pool = LazyPool::new(device); - let started = Instant::now(); + let started_at = Instant::now(); + let mut prev_frame_at = started_at; window.run(|frame| { - let elapsed = (Instant::now() - started).as_secs_f32(); + let now = Instant::now(); + + let dt = now - prev_frame_at; + prev_frame_at = now; + + let elapsed = now - started_at; - let index_buf = frame.render_graph.bind_node(&character.index_buf); - let vertex_buf = frame.render_graph.bind_node(&character.vertex_buf); - let depth_image = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let index_buf = frame.graph.bind_resource(&character.index_buf); + let vertex_buf = frame.graph.bind_resource(&character.vertex_buf); + let depth_image = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( frame.width, frame.height, vk::Format::D32_SFLOAT, @@ -59,103 +79,109 @@ fn main() -> Result<(), WindowError> { .unwrap(), ); - let texture = frame - .render_graph - .bind_node(match (elapsed / 2.0).rem_euclid(4.0) { - t if t < 1.0 => &human_female, - t if t < 2.0 => &human_male, - t if t < 3.0 => &zombie_female, - _ => &zombie_male, - }); - - let camera_buf = frame.render_graph.bind_node({ + let texture = + frame + .graph + .bind_resource(match (elapsed.as_secs_f32() / 2.0).rem_euclid(4.0) { + t if t < 1.0 => &human_female, + t if t < 2.0 => &human_male, + t if t < 3.0 => &zombie_female, + _ => &zombie_male, + }); + + let camera_buf = frame.graph.bind_resource({ let position = Vec3::ONE * 3.0; let aspect_ratio = frame.render_aspect_ratio(); let projection = Mat4::perspective_rh(45.0, aspect_ratio, 0.1, 100.0); let view = Mat4::look_at_rh(position, Vec3::Y * 2.0, -Vec3::Y); let mut buf = pool - .lease(BufferInfo::host_mem( + .resource(BufferInfo::host_mem( size_of::() as _, vk::BufferUsageFlags::UNIFORM_BUFFER, )) .unwrap(); - Buffer::copy_from_slice( - &mut buf, + buf.copy_from_slice( 0, bytes_of(&CameraUniform { projection, view, position, + ..Default::default() }), ); buf }); - let animation_buf = frame.render_graph.bind_node({ - let animation = match (elapsed / 4.0).rem_euclid(2.0) { + let animation_buf = frame.graph.bind_resource({ + let animation = match (elapsed.as_secs_f32() / 4.0).rem_euclid(2.0) { t if t < 1.0 => &mut run, _ => &mut idle, }; - let joints = animation.update(0.016); + let joints = animation.update(dt); let mut buf = pool - .lease(BufferInfo::host_mem( + .resource(BufferInfo::host_mem( size_of_val(joints) as _, vk::BufferUsageFlags::STORAGE_BUFFER, )) .unwrap(); - Buffer::copy_from_slice(&mut buf, 0, cast_slice(joints)); + buf.copy_from_slice(0, cast_slice(joints)); buf }); frame - .render_graph - .begin_pass("🦴") + .graph + .begin_cmd() + .debug_name("🦴") .bind_pipeline(&pipeline) - .set_depth_stencil(DepthStencilMode::DEPTH_WRITE) - .access_node(index_buf, AccessType::IndexBuffer) - .access_node(vertex_buf, AccessType::VertexBuffer) - .access_descriptor(0, camera_buf, AccessType::VertexShaderReadUniformBuffer) - .access_descriptor(1, animation_buf, AccessType::VertexShaderReadOther) - .read_descriptor(2, texture) - .clear_color(0, frame.swapchain_image) - .store_color(0, frame.swapchain_image) - .clear_depth_stencil(depth_image) - .record_subpass(move |subpass, _| { - subpass - .bind_index_buffer(index_buf, vk::IndexType::UINT16) - .bind_vertex_buffer(vertex_buf) - .push_constants(bytes_of(&Mat4::IDENTITY)) + .depth_stencil(DepthStencilInfo::DEPTH_WRITE_LESS_IGNORE_STENCIL) + .resource_access(index_buf, AccessType::IndexBuffer) + .resource_access(vertex_buf, AccessType::VertexBuffer) + .shader_resource_access(0, camera_buf, AccessType::VertexShaderReadUniformBuffer) + .shader_resource_access(1, animation_buf, AccessType::VertexShaderReadOther) + .shader_resource_access( + 2, + texture, + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + .color_attachment_image( + 0, + frame.swapchain_image, + LoadOp::CLEAR_BLACK_ALPHA_ZERO, + StoreOp::Store, + ) + .depth_stencil_attachment_image( + depth_image, + LoadOp::CLEAR_ONE_STENCIL_ZERO, + StoreOp::DontCare, + ) + .record_cmd(move |cmd| { + cmd.bind_index_buffer(index_buf, 0, vk::IndexType::UINT16) + .bind_vertex_buffer(0, vertex_buf, 0) + .push_constants(0, bytes_of(&Mat4::IDENTITY)) .draw_indexed(character.index_count, 1, 0, 0, 0); }); }) } -fn create_pipeline( - device: &Arc, - pak: &mut PakBuf, -) -> Result, DriverError> { +fn create_pipeline(device: &Device, pak: &mut PakBuf) -> Result { let vert_spirv = pak.read_blob("shader/animated_mesh_vert.spirv").unwrap(); let frag_spirv = pak.read_blob("shader/mesh_frag.spirv").unwrap(); - Ok(Arc::new(GraphicPipeline::create( + GraphicPipeline::create( device, GraphicPipelineInfoBuilder::default().front_face(vk::FrontFace::CLOCKWISE), [ Shader::new_vertex(vert_spirv.as_slice()), Shader::new_fragment(frag_spirv.as_slice()), ], - )?)) + ) } -fn load_texture( - device: &Arc, - pak: &mut PakBuf, - key: &str, -) -> Result, DriverError> { +fn load_texture(device: &Device, pak: &mut PakBuf, key: &str) -> Result, DriverError> { let bitmap = pak.read_bitmap(key).unwrap(); assert_eq!(bitmap.format(), BitmapFormat::Rgba); @@ -163,7 +189,7 @@ fn load_texture( assert_eq!(bitmap.height().count_ones(), 1); // NOTE: This is the most basic way to load an image; you probably want to use something like - // screen-13-fx::ImageLoader instead! + // vk-graph-fx::ImageLoader instead! // We will stage the pixels in a host-accessible buffer let buffer = Arc::new(Buffer::create_from_slice( @@ -182,13 +208,15 @@ fn load_texture( )?); // Copy the host-accessible pixels into the device-only image - let mut render_graph = RenderGraph::new(); - let image_node = render_graph.bind_node(&image); - let buffer_node = render_graph.bind_node(&buffer); - render_graph.copy_buffer_to_image(buffer_node, image_node); - render_graph - .resolve() - .submit(&mut HashPool::new(device), 0, 0)?; + { + let mut graph = Graph::default(); + let image = graph.bind_resource(&image); + let buffer = graph.bind_resource(&buffer); + graph.copy_buffer_to_image(buffer, image); + graph + .into_submission() + .queue_submit(&mut HashPool::new(device), 0, 0)?; + } Ok(image) } @@ -198,8 +226,8 @@ struct Animation { joints: Vec, local_joints: Vec, channels: Vec, - time: u32, - total_time: u32, + time: Duration, + total_time: Duration, } impl Animation { @@ -208,12 +236,13 @@ impl Animation { let joints = model.joints.clone(); let animation = pak.read_animation(key).unwrap(); - let total_time = animation + let total_millis = animation .channels() .iter() .map(|channel| channel.inputs().last().copied().unwrap_or_default()) .max() .unwrap_or_default(); + let total_time = Duration::from_millis(total_millis as _); // TODO: Here is where you probably want to flatten out the channels into a constant // framerate animation for each joint - it would make it easier to run the update code @@ -231,14 +260,16 @@ impl Animation { joints, local_joints: repeat_n(Mat4::IDENTITY, model.joints.len()).collect(), channels, - time: 0, + time: Duration::ZERO, total_time, }) } - fn update(&mut self, dt: f32) -> &[Mat4] { - self.time += Duration::from_secs_f32(dt).as_millis() as u32; - self.time %= self.total_time; + fn update(&mut self, dt: Duration) -> &[Mat4] { + self.time += dt; + while self.time > self.total_time { + self.time -= self.total_time; + } for transform in self.local_joints.iter_mut() { *transform = Mat4::IDENTITY; @@ -315,8 +346,8 @@ impl Animation { // Uncomment to show how to manually target a bone (this twists the chest to the right) // let animation_transform = if joint.name.as_str() == "Chest" { - // self.joints[joint.parent_index].inverse_bind * joint.inverse_bind.inverse() * Mat4::from_rotation_y(90f32.to_radians()) - // } else { + // self.joints[joint.parent_index].inverse_bind * joint.inverse_bind.inverse() * + // Mat4::from_rotation_y(90f32.to_radians()) } else { // animation_transform // }; @@ -331,16 +362,17 @@ impl Animation { /// Given an array of keyframe times, returns the two keyframe indices and the weight factor /// to use when interpolating between them. fn pick_weighted_keyframes(&self, inputs: &[u32]) -> (usize, usize, f32) { - let (idx_a, idx_b) = match inputs.binary_search(&self.time) { + let time = self.time.as_millis() as u32; + let (idx_a, idx_b) = match inputs.binary_search(&time) { Err(idx) if idx == 0 || idx == inputs.len() => (inputs.len() - 1, 0), Err(idx) => (idx - 1, idx), Ok(idx) => (idx, idx), }; let ab = match idx_a.cmp(&idx_b) { Ordering::Equal => 0.0, - Ordering::Greater => self.time as f32 / inputs[idx_b] as f32, + Ordering::Greater => self.time.as_secs_f32() / inputs[idx_b] as f32, Ordering::Less => { - (self.time - inputs[idx_a]) as f32 / (inputs[idx_b] - inputs[idx_a]) as f32 + (time - inputs[idx_a]) as f32 / (inputs[idx_b] - inputs[idx_a]) as f32 } }; @@ -356,15 +388,14 @@ struct Args { } #[repr(C)] -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Default, Pod, Zeroable)] struct CameraUniform { projection: Mat4, view: Mat4, position: Vec3, + __: u32, } -unsafe impl NoUninit for CameraUniform {} - struct Model { index_buf: Arc, index_count: u32, @@ -373,27 +404,25 @@ struct Model { } impl Model { - fn load(device: &Arc, pak: &mut PakBuf, key: &str) -> Result { - let model = pak.read_model(key).unwrap(); + fn load(device: &Device, pak: &mut PakBuf, key: &str) -> Result { + let mesh = pak.read_mesh(key).unwrap(); // This obviously makes some assumptions about the input model! - let mesh = model - .meshes() - .iter() - .find(|mesh| mesh.skin().is_some()) - .unwrap(); let joints = mesh.skin().unwrap().joints().to_vec(); - let parts = mesh.parts(); + let primitives = mesh.primitives(); - assert_eq!(parts.len(), 1); + assert_eq!(primitives.len(), 1); - let part = &parts[0]; - let lods = part.lods(); + let primitive = &primitives[0]; + let lods = primitive.lods(); assert_eq!( - part.vertex(), - Vertex::POSITION | Vertex::NORMAL | Vertex::TEXTURE0 | Vertex::JOINTS_WEIGHTS + primitive.vertex_type(), + VertexType::POSITION + | VertexType::NORMAL + | VertexType::TEXTURE0 + | VertexType::JOINTS_WEIGHTS ); assert!(!lods.is_empty()); @@ -404,19 +433,13 @@ impl Model { let indices = indices.as_u16().unwrap(); let index_data = cast_slice(&indices); - let vertex_data = part.vertex_data(); + let vertex_data = primitive.vertex_data(); // Host-accessible staging buffers - let index_staging_buf = Arc::new(Buffer::create_from_slice( - device, - vk::BufferUsageFlags::TRANSFER_SRC, - index_data, - )?); - let vertex_staging_buf = Arc::new(Buffer::create_from_slice( - device, - vk::BufferUsageFlags::TRANSFER_SRC, - vertex_data, - )?); + let index_staging_buf = + Buffer::create_from_slice(device, vk::BufferUsageFlags::TRANSFER_SRC, index_data)?; + let vertex_staging_buf = + Buffer::create_from_slice(device, vk::BufferUsageFlags::TRANSFER_SRC, vertex_data)?; // Device-only buffers let index_buf = Arc::new(Buffer::create( @@ -435,16 +458,19 @@ impl Model { )?); // Copy the host-accessible staging buffers to device-only buffers - let mut render_graph = RenderGraph::new(); - let index_staging_buf_node = render_graph.bind_node(index_staging_buf); - let vertex_staging_buf_node = render_graph.bind_node(vertex_staging_buf); - let index_buf_node = render_graph.bind_node(&index_buf); - let vertex_buf_node = render_graph.bind_node(&vertex_buf); - render_graph.copy_buffer(index_staging_buf_node, index_buf_node); - render_graph.copy_buffer(vertex_staging_buf_node, vertex_buf_node); - render_graph - .resolve() - .submit(&mut HashPool::new(device), 0, 0)?; + let mut graph = Graph::default(); + { + let index_staging_buf = graph.bind_resource(index_staging_buf); + let vertex_staging_buf = graph.bind_resource(vertex_staging_buf); + let index_buf = graph.bind_resource(&index_buf); + let vertex_buf = graph.bind_resource(&vertex_buf); + graph + .copy_buffer(index_staging_buf, index_buf) + .copy_buffer(vertex_staging_buf, vertex_buf); + graph + .into_submission() + .queue_submit(&mut HashPool::new(device), 0, 0)?; + } Ok(Model { index_buf, diff --git a/examples/subgroup_ops.rs b/examples/subgroup_ops.rs index bc26363e..1687417b 100644 --- a/examples/subgroup_ops.rs +++ b/examples/subgroup_ops.rs @@ -1,9 +1,22 @@ use { + ash::vk, bytemuck::cast_slice, clap::Parser, - inline_spirv::inline_spirv, - screen_13::prelude::*, std::{mem::size_of, sync::Arc, time::Instant}, + vk_graph::{ + Graph, + driver::{ + DriverError, + buffer::{Buffer, BufferInfo}, + compute::{ComputePipeline, ComputePipelineInfo}, + device::{Device, DeviceInfo}, + physical_device::Vulkan11Properties, + shader::{Shader, SpecializationMap}, + }, + pool::hash::HashPool, + }, + vk_shader_macros::glsl, + vk_sync::AccessType, }; /// Advanced example demonstrating subgroup operations (arithmetic and ballot). @@ -30,8 +43,8 @@ fn main() -> Result<(), DriverError> { pretty_env_logger::init(); let args = Args::parse(); - let device_info = DeviceInfoBuilder::default().debug(args.debug); - let device = Arc::new(Device::create_headless(device_info)?); + let device_info = DeviceInfo::builder().debug(args.debug); + let device = Device::create(device_info)?; let Vulkan11Properties { subgroup_size, subgroup_supported_operations, @@ -61,20 +74,20 @@ fn main() -> Result<(), DriverError> { } fn exclusive_sum( - device: &Arc, - reduce_pipeline: &Arc, - scan_pipeline: &Arc, + device: &Device, + reduce_pipeline: &ComputePipeline, + scan_pipeline: &ComputePipeline, input_data: &[u32], ) -> Result, DriverError> { - let mut render_graph = RenderGraph::new(); + let mut graph = Graph::default(); - let input_buf = render_graph.bind_node(Buffer::create_from_slice( + let input_buf = graph.bind_resource(Buffer::create_from_slice( device, vk::BufferUsageFlags::STORAGE_BUFFER, cast_slice(input_data), )?); - let output_buf = render_graph.bind_node(Arc::new(Buffer::create( + let output_buf = graph.bind_resource(Arc::new(Buffer::create( device, BufferInfo::host_mem( input_data.len() as vk::DeviceSize * size_of::() as vk::DeviceSize, @@ -85,7 +98,7 @@ fn exclusive_sum( let workgroup_count = input_data.len() as u32 / device.physical_device.properties_v1_1.subgroup_size; let reduce_count = workgroup_count - 1; - let workgroup_buf = render_graph.bind_node(Buffer::create( + let workgroup_buf = graph.bind_resource(Buffer::create( device, BufferInfo::device_mem( reduce_count.max(1) as vk::DeviceSize * size_of::() as vk::DeviceSize, @@ -94,33 +107,35 @@ fn exclusive_sum( )?); if reduce_count > 0 { - render_graph - .begin_pass("exclusive sum reduce") + graph + .begin_cmd() + .debug_name("exclusive sum reduce") .bind_pipeline(reduce_pipeline) - .read_descriptor(0, input_buf) - .write_descriptor(1, workgroup_buf) - .record_compute(move |compute, _| { - compute.dispatch(reduce_count, 1, 1); + .shader_resource_access(0, input_buf, AccessType::ComputeShaderReadOther) + .shader_resource_access(1, workgroup_buf, AccessType::ComputeShaderWrite) + .record_cmd(move |cmd| { + cmd.dispatch(reduce_count, 1, 1); }); } - render_graph - .begin_pass("exclusive sum scan") + graph + .begin_cmd() + .debug_name("exclusive sum scan") .bind_pipeline(scan_pipeline) - .read_descriptor(0, workgroup_buf) - .read_descriptor(1, input_buf) - .write_descriptor(2, output_buf) - .record_compute(move |compute, _| { - compute.dispatch(workgroup_count, 1, 1); + .shader_resource_access(0, workgroup_buf, AccessType::ComputeShaderReadOther) + .shader_resource_access(1, input_buf, AccessType::ComputeShaderReadOther) + .shader_resource_access(2, output_buf, AccessType::ComputeShaderWrite) + .record_cmd(move |cmd| { + cmd.dispatch(workgroup_count, 1, 1); }); - let output_buf = render_graph.unbind_node(output_buf); - let mut cmd_buf = render_graph - .resolve() - .submit(&mut HashPool::new(device), 0, 0)?; + let output_buf = graph.resource(output_buf).clone(); + let mut cmd = graph + .into_submission() + .queue_submit(&mut HashPool::new(device), 0, 0)?; let started = Instant::now(); - cmd_buf.wait_until_executed()?; + cmd.wait_until_executed()?; println!( "Waited {}μs (len={})", @@ -150,16 +165,18 @@ fn assert_output_data(input_data: &[u32], output_data: &[u32]) { } } -fn create_reduce_pipeline(device: &Arc) -> Result, DriverError> { - Ok(Arc::new(ComputePipeline::create( +fn create_reduce_pipeline(device: &Device) -> Result { + ComputePipeline::create( device, ComputePipelineInfo::default(), - Shader::new_compute( - inline_spirv!( + Shader::from_spirv( + glsl!( + target: vulkan1_2, r#" #version 460 core #extension GL_EXT_shader_explicit_arithmetic_types_int32 : require #extension GL_KHR_shader_subgroup_arithmetic : require + #pragma shader_stage(compute) layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; @@ -178,84 +195,82 @@ fn create_reduce_pipeline(device: &Arc) -> Result, workgroup_buf[gl_WorkGroupID.x] = sum; } } - "#, - comp, - vulkan1_2, + "# ) .as_slice(), ) - .specialization_info(SpecializationInfo { - data: device - .physical_device - .properties_v1_1 - .subgroup_size - .to_ne_bytes() - .to_vec(), - map_entries: vec![vk::SpecializationMapEntry { - constant_id: 0, - offset: 0, - size: size_of::(), - }], - }), - )?)) + .specialization( + SpecializationMap::new( + device + .physical_device + .properties_v1_1 + .subgroup_size + .to_ne_bytes(), + ) + .constant(0, 0, 4), + ), + ) } -fn create_exclusive_sum_pipeline( - device: &Arc, -) -> Result, DriverError> { - Ok( Arc::new( - ComputePipeline::create( - device, - ComputePipelineInfo::default(), - Shader::new_compute(inline_spirv!( - r#" - #version 460 core - #extension GL_EXT_shader_explicit_arithmetic_types_int32 : require - #extension GL_KHR_shader_subgroup_arithmetic : require - - layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; - - layout(binding = 0) restrict readonly buffer WorkgroupBuffer { - uint32_t workgroup_buf[]; - }; - - layout(binding = 1) restrict readonly buffer InputBuffer { - uint32_t input_buf[]; - }; - - layout(binding = 2) restrict writeonly buffer OutputBuffer { - uint32_t output_buf[]; - }; - - void main() { - uint32_t subgroup_sum = subgroupExclusiveAdd(input_buf[gl_GlobalInvocationID.x]); - uint32_t workgroup_sum = 0; - - uint workgroups_per_subgroup_invocation = (gl_NumWorkGroups.x + gl_SubgroupSize - 1) / gl_SubgroupSize; - uint start = gl_SubgroupInvocationID * workgroups_per_subgroup_invocation; - uint end = min(start + workgroups_per_subgroup_invocation, gl_WorkGroupID.x); - for (uint workgroup_id = start; workgroup_id < end; workgroup_id++) { - workgroup_sum += workgroup_buf[workgroup_id]; +fn create_exclusive_sum_pipeline(device: &Device) -> Result { + ComputePipeline::create( + device, + ComputePipelineInfo::default(), + Shader::new_compute( + glsl!( + target: vulkan1_2, + r#" + #version 460 core + #extension GL_EXT_shader_explicit_arithmetic_types_int32 : require + #extension GL_KHR_shader_subgroup_arithmetic : require + #pragma shader_stage(compute) + + layout(local_size_x_id = 0, local_size_y = 1, local_size_z = 1) in; + + layout(binding = 0) restrict readonly buffer WorkgroupBuffer { + uint32_t workgroup_buf[]; + }; + + layout(binding = 1) restrict readonly buffer InputBuffer { + uint32_t input_buf[]; + }; + + layout(binding = 2) restrict writeonly buffer OutputBuffer { + uint32_t output_buf[]; + }; + + void main() { + uint32_t subgroup_sum = + subgroupExclusiveAdd(input_buf[gl_GlobalInvocationID.x]); + uint32_t workgroup_sum = 0; + + uint workgroups_per_subgroup_invocation = + (gl_NumWorkGroups.x + gl_SubgroupSize - 1) / gl_SubgroupSize; + uint start = gl_SubgroupInvocationID * workgroups_per_subgroup_invocation; + uint end = min(start + workgroups_per_subgroup_invocation, gl_WorkGroupID.x); + for (uint workgroup_id = start; workgroup_id < end; workgroup_id++) { + workgroup_sum += workgroup_buf[workgroup_id]; + } + + workgroup_sum = subgroupAdd(workgroup_sum); + + output_buf[gl_GlobalInvocationID.x] = subgroup_sum + workgroup_sum; } - - workgroup_sum = subgroupAdd(workgroup_sum); - - output_buf[gl_GlobalInvocationID.x] = subgroup_sum + workgroup_sum; - } - "#, - comp, - vulkan1_2 - ).as_slice()) - .specialization_info(SpecializationInfo { - data: device.physical_device.properties_v1_1.subgroup_size.to_ne_bytes().to_vec(), - map_entries: vec![vk::SpecializationMapEntry { - constant_id: 0, - offset: 0, - size: size_of::(), - }], - }), - )? - )) + "# + ) + .as_slice(), + ) + .specialization( + SpecializationMap::new( + device + .physical_device + .properties_v1_1 + .subgroup_size + .to_ne_bytes(), + ) + .constant(0, 0, 4), + ), + ) } #[derive(Parser)] diff --git a/examples/transitions.rs b/examples/transitions.rs index eef529b4..44dbab76 100644 --- a/examples/transitions.rs +++ b/examples/transitions.rs @@ -4,11 +4,11 @@ use { clap::Parser, image::ImageReader, log::info, - screen_13::prelude::LazyPool, - screen_13_fx::*, - screen_13_imgui::prelude::*, - screen_13_window::WindowBuilder, std::{io::Cursor, time::Instant}, + vk_graph::pool::lazy::LazyPool, + vk_graph_fx::*, + vk_graph_imgui::prelude::*, + vk_graph_window::Window, winit::dpi::LogicalSize, }; @@ -16,9 +16,9 @@ fn main() -> anyhow::Result<()> { pretty_env_logger::init(); profile_with_puffin::init(); - // Create Screen 13 things any similar program might need + // Create vk-graph things any similar program might need let args = Args::parse(); - let window = WindowBuilder::default() + let window = Window::builder() .debug(args.debug) .window(|builder| builder.with_inner_size(LogicalSize::new(1024.0f64, 768.0f64))) .build()?; @@ -58,46 +58,45 @@ fn main() -> anyhow::Result<()> { // Hold some app state which is displayed/mutated by imgui each frame let mut curr_transition_idx = 0; - let mut start_time = Instant::now(); + let mut started_at = Instant::now(); + let mut prev_frame_at = started_at; window.run(|frame| { - // Update the demo "state" let now = Instant::now(); - let elapsed = (now - start_time).as_secs_f32(); - let progress = if elapsed > 4.0 { - start_time = now; - 0.0 - } else if elapsed > 3.0 { - 1.0 - (elapsed - 3.0) - } else if elapsed > 2.0 { - 1.0 - } else if elapsed > 1.0 { - elapsed - 1.0 - } else { - 0.0 + + let dt = now - prev_frame_at; + prev_frame_at = now; + + let elapsed = now - started_at; + + // Update the demo "state" + let progress = match elapsed.as_secs_f32() { + t if t > 4.0 => { + started_at = now; + 0.0 + } + t if t > 3.0 => 1.0 - (t - 3.0), + t if t > 2.0 => 1.0, + t if t > 1.0 => t - 1.0, + _ => 0.0, }; // Bind images so we can graph them - let bart_image = frame.render_graph.bind_node(&bart_image); - let gulf_image = frame.render_graph.bind_node(&gulf_image); + let bart_image = frame.graph.bind_resource(&bart_image); + let gulf_image = frame.graph.bind_resource(&gulf_image); // Apply the current transition to the images and get a resultant image out; "blend_image" let transition = TRANSITIONS[curr_transition_idx]; - let blend_image = transition_pipeline.apply( - frame.render_graph, - bart_image, - gulf_image, - transition, - progress, - ); + let blend_image = + transition_pipeline.apply(frame.graph, bart_image, gulf_image, transition, progress); // Draw UI: TODO: Sliders and value setters? That would be fun. let gui_image = imgui.draw( - 0.016, + dt.as_secs_f32(), frame.events, frame.window, &mut pool, - frame.render_graph, + frame.graph, |ui, _, _| { ui.window("Transitions example") .position([10.0, 10.0], Condition::FirstUseEver) @@ -121,12 +120,7 @@ fn main() -> anyhow::Result<()> { ); // Display the GUI + Blend images on screen - display.present_images( - frame.render_graph, - gui_image, - blend_image, - frame.swapchain_image, - ); + display.present_images(frame.graph, gui_image, blend_image, frame.swapchain_image); })?; Ok(()) diff --git a/examples/triangle.rs b/examples/triangle.rs index e66654ea..b66ab43a 100644 --- a/examples/triangle.rs +++ b/examples/triangle.rs @@ -1,12 +1,20 @@ mod profile_with_puffin; use { + ash::vk, bytemuck::cast_slice, clap::Parser, - inline_spirv::inline_spirv, - screen_13::prelude::*, - screen_13_window::{WindowBuilder, WindowError}, std::sync::Arc, + vk_graph::{ + cmd::{LoadOp, StoreOp}, + driver::{ + buffer::Buffer, + graphic::{GraphicPipeline, GraphicPipelineInfo}, + }, + }, + vk_graph_window::{Window, WindowError}, + vk_shader_macros::glsl, + vk_sync::AccessType, }; // A Vulkan triangle using a graphic pipeline, vertex/fragment shaders, and index/vertex buffers. @@ -15,15 +23,15 @@ fn main() -> Result<(), WindowError> { profile_with_puffin::init(); let args = Args::parse(); - let window = WindowBuilder::default().debug(args.debug).build()?; - let triangle_pipeline = Arc::new(GraphicPipeline::create( + let window = Window::builder().debug(args.debug).build()?; + let triangle_pipeline = GraphicPipeline::create( &window.device, GraphicPipelineInfo::default(), [ - Shader::new_vertex( - inline_spirv!( - r#" + glsl!( + r#" #version 460 core + #pragma shader_stage(vertex) layout(location = 0) in vec3 position; layout(location = 1) in vec3 color; @@ -34,15 +42,13 @@ fn main() -> Result<(), WindowError> { gl_Position = vec4(position, 1); vk_Color = color; } - "#, - vert - ) - .as_slice(), - ), - Shader::new_fragment( - inline_spirv!( - r#" + "# + ) + .as_slice(), + glsl!( + r#" #version 460 core + #pragma shader_stage(fragment) layout(location = 0) in vec3 color; @@ -51,13 +57,11 @@ fn main() -> Result<(), WindowError> { void main() { vk_Color = vec4(color, 1); } - "#, - frag - ) - .as_slice(), - ), + "# + ) + .as_slice(), ], - )?); + )?; let index_buf = Arc::new(Buffer::create_from_slice( &window.device, @@ -79,21 +83,26 @@ fn main() -> Result<(), WindowError> { )?); window.run(|frame| { - let index_node = frame.render_graph.bind_node(&index_buf); - let vertex_node = frame.render_graph.bind_node(&vertex_buf); + let index_node = frame.graph.bind_resource(&index_buf); + let vertex_node = frame.graph.bind_resource(&vertex_buf); frame - .render_graph - .begin_pass("Triangle Example") + .graph + .begin_cmd() + .debug_name("Triangle Example") .bind_pipeline(&triangle_pipeline) - .access_node(index_node, AccessType::IndexBuffer) - .access_node(vertex_node, AccessType::VertexBuffer) - .clear_color(0, frame.swapchain_image) - .store_color(0, frame.swapchain_image) - .record_subpass(move |subpass, _| { - subpass.bind_index_buffer(index_node, vk::IndexType::UINT16); - subpass.bind_vertex_buffer(vertex_node); - subpass.draw_indexed(3, 1, 0, 0, 0); + .resource_access(index_node, AccessType::IndexBuffer) + .resource_access(vertex_node, AccessType::VertexBuffer) + .color_attachment_image( + 0, + frame.swapchain_image, + LoadOp::CLEAR_BLACK_ALPHA_ZERO, + StoreOp::Store, + ) + .record_cmd(move |cmd| { + cmd.bind_index_buffer(index_node, 0, vk::IndexType::UINT16) + .bind_vertex_buffer(0, vertex_node, 0) + .draw_indexed(3, 1, 0, 0, 0); }); }) } diff --git a/examples/vertex_layout.rs b/examples/vertex_layout.rs index e43e4e47..7410c2fa 100644 --- a/examples/vertex_layout.rs +++ b/examples/vertex_layout.rs @@ -1,13 +1,24 @@ mod profile_with_puffin; use { + ash::vk, bytemuck::{Pod, Zeroable, cast_slice}, clap::Parser, half::f16, - inline_spirv::inline_spirv, - screen_13::prelude::*, - screen_13_window::{FrameContext, WindowBuilder}, std::{mem::size_of, sync::Arc}, + vk_graph::{ + cmd::{ClearColorValue, LoadOp, StoreOp}, + driver::{ + DriverError, + buffer::Buffer, + device::Device, + graphic::{GraphicPipeline, GraphicPipelineInfo}, + shader::{Shader, ShaderBuilder}, + }, + }, + vk_graph_window::{FrameContext, Window}, + vk_shader_macros::glsl, + vk_sync::AccessType, }; /// This example draws two triangles using two different vertex formats. @@ -24,7 +35,7 @@ fn main() -> anyhow::Result<()> { // https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#fxvertex-attrib let args = Args::parse(); - let window = WindowBuilder::default().debug(args.debug).build()?; + let window = Window::builder().debug(args.debug).build()?; let f16_pipeline = create_f16_pipeline(&window.device).ok(); let f16_vertex_buf = { @@ -80,12 +91,17 @@ fn main() -> anyhow::Result<()> { }; window.run(|mut frame| { - draw_triangle(&mut frame, &f32_pipeline, &f32_vertex_buf); + draw_triangle( + &mut frame, + &f32_pipeline, + &f32_vertex_buf, + LoadOp::CLEAR_BLACK_ALPHA_ZERO, + ); if let Some(f64_pipeline) = &f64_pipeline { - draw_triangle(&mut frame, f64_pipeline, &f64_vertex_buf); + draw_triangle(&mut frame, f64_pipeline, &f64_vertex_buf, LoadOp::Load); } else if let Some(f16_pipeline) = &f16_pipeline { - draw_triangle(&mut frame, f16_pipeline, &f16_vertex_buf); + draw_triangle(&mut frame, f16_pipeline, &f16_vertex_buf, LoadOp::Load); } })?; @@ -94,24 +110,25 @@ fn main() -> anyhow::Result<()> { fn draw_triangle( frame: &mut FrameContext, - pipeline: &Arc, + pipeline: &GraphicPipeline, vertex_buf: &Arc, + load: LoadOp, ) { - let vertex_buf = frame.render_graph.bind_node(vertex_buf); + let vertex_buf = frame.graph.bind_resource(vertex_buf); frame - .render_graph - .begin_pass("Triangle") + .graph + .begin_cmd() + .debug_name("Triangle") .bind_pipeline(pipeline) - .load_color(0, frame.swapchain_image) - .store_color(0, frame.swapchain_image) - .access_node(vertex_buf, AccessType::VertexBuffer) - .record_subpass(move |subpass, _| { - subpass.bind_vertex_buffer(vertex_buf).draw(3, 1, 0, 0); + .color_attachment_image(0, frame.swapchain_image, load, StoreOp::Store) + .resource_access(vertex_buf, AccessType::VertexBuffer) + .record_cmd(move |cmd| { + cmd.bind_vertex_buffer(0, vertex_buf, 0).draw(3, 1, 0, 0); }); } -fn create_f16_pipeline(device: &Arc) -> Result, DriverError> { +fn create_f16_pipeline(device: &Device) -> Result { if !supports_vertex_buffer(device, vk::Format::R16G16_SFLOAT) { return Err(DriverError::Unsupported); } @@ -144,14 +161,14 @@ fn create_f16_pipeline(device: &Arc) -> Result, Dri create_pipeline(device, vertex) } -fn create_f32_pipeline(device: &Arc) -> Result, DriverError> { +fn create_f32_pipeline(device: &Device) -> Result { // Uses automatic vertex input layout let vertex = create_vertex_shader(false); create_pipeline(device, vertex) } -fn create_f64_pipeline(device: &Arc) -> Result, DriverError> { +fn create_f64_pipeline(device: &Device) -> Result { if !supports_vertex_buffer(device, vk::Format::R64G64_SFLOAT) { return Err(DriverError::Unsupported); } @@ -195,26 +212,27 @@ fn create_vertex_shader(is_double: bool) -> ShaderBuilder { // dvec2 when using 64-bit positions; and for the purposes of this example we don't want to // duplicate this shader code. You probably don't want to do this, or you may have different // facilities for generating SPIR-V code - either way ignore the macro unless you're interested - // in the inline_spirv! wizardry it contains which is unrelated to this example. + // in the include_glsl! wizardry it contains which is unrelated to this example. macro_rules! compile_vert { ($vec2_ty:literal) => { - inline_spirv!( - r#" - #version 460 core - - layout(location = 0) in VEC2_TY position_in; - layout(location = 1) in vec3 color_in; - - layout(location = 0) out vec3 color_out; - - void main() { - gl_Position = vec4(position_in, 0, 1); - color_out = color_in; - } - "#, - vert, - D VEC2_TY = $vec2_ty, - )}; + glsl!( + define: VEC2_TY $vec2_ty, + r#" + #version 460 core + #pragma shader_stage(vertex) + + layout(location = 0) in VEC2_TY position_in; + layout(location = 1) in vec3 color_in; + + layout(location = 0) out vec3 color_out; + + void main() { + gl_Position = vec4(position_in, 0, 1); + color_out = color_in; + } + "# + ) + }; } let spirv = if is_double { @@ -226,34 +244,37 @@ fn create_vertex_shader(is_double: bool) -> ShaderBuilder { Shader::new_vertex(spirv) } -fn create_pipeline( - device: &Arc, - vertex: ShaderBuilder, -) -> Result, DriverError> { - let fragment_spirv = inline_spirv!( - r#" - #version 460 core - - layout(location = 0) in vec3 color_in; - - layout(location = 0) out vec4 color_out; - - void main() { - color_out = vec4(color_in, 1.0); - } - "#, - frag - ); - - Ok(Arc::new(GraphicPipeline::create( +fn create_pipeline(device: &Device, vertex: ShaderBuilder) -> Result { + GraphicPipeline::create( device, GraphicPipelineInfo::default(), - [vertex, Shader::new_fragment(fragment_spirv.as_slice())], - )?)) + [ + vertex, + Shader::from_spirv( + glsl!( + r#" + #version 460 core + #pragma shader_stage(fragment) + + layout(location = 0) in vec3 color_in; + + layout(location = 0) out vec4 color_out; + + void main() { + color_out = vec4(color_in, 1.0); + } + "# + ) + .as_slice(), + ), + ], + ) } fn supports_vertex_buffer(device: &Device, format: vk::Format) -> bool { - Device::format_properties(device, format) + device + .physical_device + .format_properties(format) .buffer_features .contains(vk::FormatFeatureFlags::VERTEX_BUFFER) } diff --git a/examples/vr/Cargo.toml b/examples/vr/Cargo.toml index b67bf1c8..26de9860 100644 --- a/examples/vr/Cargo.toml +++ b/examples/vr/Cargo.toml @@ -1,25 +1,29 @@ [package] name = "vr" -version = "0.1.0" authors = ["John Wells "] -edition = "2021" -license = "MIT OR Apache-2.0" -readme = "README.md" +edition.workspace = true +license.workspace = true +publish = false +readme.workspace = true +repository.workspace = true -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["loaded"] +loaded = ["openxr/loaded", "vk-graph/loaded"] +linked = ["openxr/linked", "vk-graph/linked"] [dependencies] anyhow = "1.0" -bytemuck = { version = "1.14", features = ["derive"] } +bytemuck = { version = "1.25", features = ["derive"] } ctrlc = "3.4" -glam = { version = "0.27", features = ["bytemuck", "mint"] } +glam = { version = "0.32", features = ["bytemuck", "mint"] } image = "0.25" -log = "0.4" -meshopt = "0.2" -mikktspace = "0.3" +log.workspace = true +meshopt = "0.6" +mikktspace = { version = "1.0", package = "bevy_mikktspace" } mint = "0.5" -openxr = { version = "0.18", features = ["mint", "static"] } +openxr = { version = "0.21", default-features = false, features = ["mint"] } pretty_env_logger = "0.5" -screen-13 = { path = "../.." } -screen-13-hot = { path = "../../contrib/screen-13-hot" } +vk-graph.workspace = true +vk-graph-hot.workspace = true tobj = "4.0" diff --git a/examples/vr/README.md b/examples/vr/README.md index 1843e797..415b3377 100644 --- a/examples/vr/README.md +++ b/examples/vr/README.md @@ -6,6 +6,8 @@ _Exaimine the skeleton of a Woolly Mammoth using the hands of Abraham Lincoln_ This example uses assets from the Smithsonian collection to create a real-life-size display in VR. Any OpenXR-supported VR headset should work. +The example defaults to the runtime OpenXR loader (`loaded`); enable the `linked` feature if you want to +link against a system loader instead. ## Features @@ -16,4 +18,4 @@ Any OpenXR-supported VR headset should work. ## Tested Platforms -- Windows/Valve Index on SteamVR @ 144hz \ No newline at end of file +- Windows/Valve Index on SteamVR @ 144hz diff --git a/examples/vr/src/driver/instance.rs b/examples/vr/src/driver/instance.rs index 1cfca1ab..07e2f06c 100644 --- a/examples/vr/src/driver/instance.rs +++ b/examples/vr/src/driver/instance.rs @@ -1,31 +1,30 @@ use { log::{debug, error}, openxr as xr, - screen_13::driver::{ - ash::{ - self, - vk::{self, Handle as _}, - }, - device::Device, - physical_device::PhysicalDevice, - }, std::{ ffi::c_void, fmt::{Debug, Formatter}, mem::transmute, ops::Deref, - sync::Arc, + }, + vk_graph::driver::{ + ash::{ + self, + vk::{self, Handle as _}, + }, + device::Device, + instance::Instance as VkInstance, }, }; -pub struct Instance { - device: Arc, +pub struct XrInstance { + device: Device, event_buf: xr::EventDataBuffer, instance: xr::Instance, system: xr::SystemId, } -impl Instance { +impl XrInstance { const VK_TARGET_VERSION: u32 = vk::make_api_version( 0, Self::XR_TARGET_VERSION.major() as _, @@ -35,13 +34,15 @@ impl Instance { const XR_TARGET_VERSION: xr::Version = xr::Version::new(1, 2, 0); pub fn new() -> Result { - let xr_entry = unsafe { xr::Entry::load().ok() }; + #[cfg(feature = "linked")] + let xr_entry = xr::Entry::linked(); - if xr_entry.is_none() { - debug!("Using statically linked OpenXR") - } + #[cfg(not(feature = "linked"))] + let xr_entry = unsafe { xr::Entry::load() }.map_err(|err| { + debug!("OpenXR loader unavailable: {err}"); - let xr_entry = xr_entry.unwrap_or_else(xr::Entry::linked); + InstanceCreateError::OpenXRUnsupported + })?; let available_extensions = xr_entry.enumerate_extensions().unwrap_or_default(); let mut required_extensions = xr::ExtensionSet::default(); required_extensions.khr_vulkan_enable2 = available_extensions.khr_vulkan_enable2; @@ -51,9 +52,10 @@ impl Instance { } let app_info = xr::ApplicationInfo { - application_name: "screen-13-example-vr", + api_version: xr::Version::new(1, 0, 0), + application_name: "vk-graph-example-vr", application_version: 0, - engine_name: "screen-13-example-vr", + engine_name: "vk-graph-example-vr", engine_version: 0, }; let xr_instance = xr_entry @@ -121,7 +123,7 @@ impl Instance { let create_info = vk::InstanceCreateInfo::default().application_info(&app_info); unsafe { - let vk_entry = ash::Entry::load().map_err(|err| { + let entry = ash::Entry::load().map_err(|err| { error!("Vulkan entry point: {err}"); InstanceCreateError::VulkanUnsupported @@ -132,7 +134,7 @@ impl Instance { unsafe extern "system" fn(T, *const i8) -> Option; type AshFn = Fn; type OpenXrFn = Fn<*const c_void>; - transmute::(vk_entry.static_fn().get_instance_proc_addr) + transmute::(entry.static_fn().get_instance_proc_addr) }; let vk_instance = { @@ -155,14 +157,14 @@ impl Instance { })?; let vk_instance = vk::Instance::from_raw(vk_instance as _); - screen_13::driver::Instance::load(vk_entry, vk_instance).map_err(|err| { + VkInstance::try_from_entry(entry, vk_instance).map_err(|err| { error!("Vulkan instance load: {err}"); InstanceCreateError::VulkanUnsupported })? }; - let vk_physical_device = vk::PhysicalDevice::from_raw( + let physical_device = vk::PhysicalDevice::from_raw( xr_instance .vulkan_graphics_device(system, vk_instance.handle().as_raw() as _) .map_err(|err| { @@ -171,20 +173,20 @@ impl Instance { InstanceCreateError::OpenXRUnsupported })? as _, ); - let vk_physical_device = PhysicalDevice::new(&vk_instance, vk_physical_device) + let physical_device = VkInstance::physical_device(&vk_instance, physical_device) .map_err(|err| { error!("Vulkan physical device: {err}"); InstanceCreateError::VulkanUnsupported })?; - let device = - Device::create_ash_device(&vk_instance, &vk_physical_device, true, |create_info| { + let ash_device = physical_device + .create_ash_device(|create_info| { let device = xr_instance .create_vulkan_device( system, get_instance_proc_addr, - vk_physical_device.as_raw() as _, + physical_device.handle.as_raw() as _, &create_info as *const _ as *const _, ) .map_err(|err| { @@ -202,13 +204,12 @@ impl Instance { InstanceCreateError::VulkanUnsupported })?; - let device = Arc::new( - Device::load(vk_instance, vk_physical_device, device, true).map_err(|err| { + let device = + Device::try_from_ash_device(ash_device, physical_device).map_err(|err| { error!("Vulkan device: {err}"); InstanceCreateError::VulkanUnsupported - })?, - ); + })?; let event_buf = xr::EventDataBuffer::new(); Ok(Self { @@ -230,14 +231,12 @@ impl Instance { xr::FrameWaiter, xr::FrameStream, )> { - let vk_instance = Device::instance(&this.device); - unsafe { this.instance.create_session::( this.system, &xr::vulkan::SessionCreateInfo { - instance: vk_instance.handle().as_raw() as _, - physical_device: this.device.physical_device.as_raw() as _, + instance: this.device.physical_device.instance.handle().as_raw() as _, + physical_device: this.device.physical_device.handle.as_raw() as _, device: this.device.handle().as_raw() as _, queue_family_index, queue_index, @@ -246,7 +245,7 @@ impl Instance { } } - pub fn device(this: &Self) -> &Arc { + pub fn device(this: &Self) -> &Device { &this.device } @@ -272,13 +271,13 @@ impl Instance { } } -impl Debug for Instance { +impl Debug for XrInstance { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str("Instance") } } -impl Deref for Instance { +impl Deref for XrInstance { type Target = xr::Instance; fn deref(&self) -> &Self::Target { diff --git a/examples/vr/src/driver/mod.rs b/examples/vr/src/driver/mod.rs index fc48fb76..0b0b7230 100644 --- a/examples/vr/src/driver/mod.rs +++ b/examples/vr/src/driver/mod.rs @@ -1,4 +1,4 @@ mod instance; mod swapchain; -pub use self::{instance::Instance, swapchain::Swapchain}; +pub use self::{instance::XrInstance, swapchain::Swapchain}; diff --git a/examples/vr/src/driver/swapchain.rs b/examples/vr/src/driver/swapchain.rs index e851af4d..58424fe8 100644 --- a/examples/vr/src/driver/swapchain.rs +++ b/examples/vr/src/driver/swapchain.rs @@ -1,14 +1,14 @@ use { - super::Instance, + super::XrInstance, openxr as xr, - screen_13::driver::{ - ash::vk::{self, Handle as _}, - image::{Image, ImageInfo}, - }, std::{ ops::{Deref, DerefMut}, sync::Arc, }, + vk_graph::driver::{ + ash::vk::{self, Handle as _}, + image::{Image, ImageInfo}, + }, }; pub struct Swapchain { @@ -18,10 +18,10 @@ pub struct Swapchain { } impl Swapchain { - pub fn new(instance: &Instance, session: &xr::Session) -> Self { - let device = Instance::device(instance); + pub fn new(instance: &XrInstance, session: &xr::Session) -> Self { + let device = XrInstance::device(instance); - let views = Instance::enumerate_view_configuration_views( + let views = XrInstance::enumerate_view_configuration_views( instance, xr::ViewConfigurationType::PRIMARY_STEREO, ) diff --git a/examples/vr/src/main.rs b/examples/vr/src/main.rs index a4f9c782..83744bff 100644 --- a/examples/vr/src/main.rs +++ b/examples/vr/src/main.rs @@ -1,38 +1,40 @@ mod driver; use { - self::driver::{Instance, Swapchain}, - bytemuck::{bytes_of, cast_slice, Pod, Zeroable}, - glam::{vec3, vec4, Mat3, Mat4, Quat, Vec2, Vec3}, + self::driver::{Swapchain, XrInstance}, + bytemuck::{Pod, Zeroable, bytes_of, cast_slice}, + glam::{Mat3, Mat4, Quat, Vec2, Vec3, vec3, vec4}, log::{debug, error, trace}, meshopt::{generate_vertex_remap, remap_index_buffer, remap_vertex_buffer}, openxr::{self as xr, EnvironmentBlendMode, ViewConfigurationType}, - screen_13::{ - driver::{ - ash::vk::{self}, - buffer::{Buffer, BufferInfo}, - device::Device, - graphic::{DepthStencilMode, GraphicPipelineInfo}, - image::{Image, ImageInfo}, - AccessType, - }, - graph::RenderGraph, - pool::{lazy::LazyPool, Pool as _}, - }, - screen_13_hot::{graphic::HotGraphicPipeline, shader::HotShader}, std::{ - fs::{metadata, File}, + fs::{File, metadata}, io::BufReader, + iter::repeat_with, path::{Path, PathBuf}, ptr::copy_nonoverlapping, sync::{ - atomic::{AtomicBool, Ordering}, Arc, + atomic::{AtomicBool, Ordering}, }, thread::sleep, time::Duration, }, - tobj::{load_obj, GPU_LOAD_OPTIONS}, + tobj::{GPU_LOAD_OPTIONS, load_obj}, + vk_graph::{ + Graph, + cmd::{LoadOp, StoreOp}, + driver::{ + ash::vk::{self}, + buffer::{Buffer, BufferInfo}, + device::Device, + graphic::{DepthStencilInfo, GraphicPipelineInfo}, + image::{Image, ImageInfo}, + sync::AccessType, + }, + pool::{Pool as _, lazy::LazyPool}, + }, + vk_graph_hot::{HotGraphicPipeline, HotShader}, }; // Sets bits with index 0 and 1 for stereoscopic rendering @@ -53,15 +55,15 @@ fn main() -> anyhow::Result<()> { trace!("Starting"); // Initialize OpenXR and Vulkan - let mut instance = Instance::new().unwrap(); - let device = Instance::device(&instance); + let mut instance = XrInstance::new().unwrap(); + let device = XrInstance::device(&instance); let queue_family_index = device_queue_family_index(device, vk::QueueFlags::GRAPHICS | vk::QueueFlags::TRANSFER) .unwrap(); // Start a VR session let (session, mut frame_wait, mut frame_stream) = - Instance::create_session(&instance, queue_family_index, 0).unwrap(); + XrInstance::create_session(&instance, queue_family_index, 0).unwrap(); let action_set = instance .create_action_set("input", "input pose information", 0) .unwrap(); @@ -95,10 +97,10 @@ fn main() -> anyhow::Result<()> { session.attach_action_sets(&[&action_set]).unwrap(); let right_space = right_action - .create_space(session.clone(), xr::Path::NULL, xr::Posef::IDENTITY) + .create_space(&session, xr::Path::NULL, xr::Posef::IDENTITY) .unwrap(); let left_space = left_action - .create_space(session.clone(), xr::Path::NULL, xr::Posef::IDENTITY) + .create_space(&session, xr::Path::NULL, xr::Posef::IDENTITY) .unwrap(); let stage = session .create_reference_space(xr::ReferenceSpaceType::STAGE, xr::Posef::IDENTITY) @@ -115,14 +117,14 @@ fn main() -> anyhow::Result<()> { }; let mut pool = LazyPool::new(device); - let mut graphs = Vec::with_capacity(Swapchain::images(&swapchain).len()); - for _ in 0..graphs.capacity() { - graphs.push(None); - } + let swapchain_image_count = Swapchain::images(&swapchain).len(); + let mut swapchain_queues = repeat_with(|| None) + .take(swapchain_image_count) + .collect::>(); let res_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("res"); - let mut hands_pipeline = HotGraphicPipeline::create( + let hands_pipeline = HotGraphicPipeline::create( device, GraphicPipelineInfo::default(), [ @@ -130,7 +132,7 @@ fn main() -> anyhow::Result<()> { HotShader::new_fragment(res_dir.join("hands.frag")), ], )?; - let mut mammoth_pipeline = HotGraphicPipeline::create( + let mammoth_pipeline = HotGraphicPipeline::create( device, GraphicPipelineInfo::default(), [ @@ -144,7 +146,12 @@ fn main() -> anyhow::Result<()> { let woolly_mammoth_dir = example_assets_dir.join("woolly-mammoth"); if metadata(&lincoln_hands_dir).is_err() { - panic!("Asset submodule missing! You must first initialize the submodules and then update them using:\ngit submodule init\ngit submodule update"); + panic!(concat!( + "Asset submodule missing! You must first initialize the submodules and then update ", + "them using:\n", + "git submodule init\n", + "git submodule update" + )); } // Load a model and textures for the left hand @@ -217,7 +224,7 @@ fn main() -> anyhow::Result<()> { } } - while let Some(event) = Instance::poll_event(&mut instance).unwrap() { + while let Some(event) = XrInstance::poll_event(&mut instance).unwrap() { use xr::Event::*; match event { SessionStateChanged(e) => { @@ -268,9 +275,9 @@ fn main() -> anyhow::Result<()> { continue; } - let mut render_graph = RenderGraph::new(); - let depth_image = render_graph.bind_node( - pool.lease(ImageInfo::image_2d_array( + let mut graph = Graph::default(); + let depth_image = graph.bind_resource( + pool.resource(ImageInfo::image_2d_array( resolution.width, resolution.height, 2, @@ -282,7 +289,7 @@ fn main() -> anyhow::Result<()> { let swapchain_image_index = swapchain.acquire_image().unwrap(); let swapchain_image = Swapchain::image(&swapchain, swapchain_image_index as _); - let swapchain_image = render_graph.bind_node(swapchain_image); + let swapchain_image = graph.bind_resource(swapchain_image); // Get the XR views and copy them into a leased uniform buffer let (_, views) = session.locate_views( @@ -293,12 +300,12 @@ fn main() -> anyhow::Result<()> { let camera_buf = { let cameras = [CameraBuffer::new(views[0]), CameraBuffer::new(views[1])]; let data = cast_slice(&cameras); - let mut buf = pool.lease(BufferInfo::host_mem( + let mut buf = pool.resource(BufferInfo::host_mem( data.len() as _, vk::BufferUsageFlags::UNIFORM_BUFFER, ))?; - Buffer::copy_from_slice(&mut buf, 0, data); - render_graph.bind_node(buf) + buf.copy_from_slice(0, data); + graph.bind_resource(buf) }; session.sync_actions(&[(&action_set).into()]).unwrap(); @@ -332,137 +339,149 @@ fn main() -> anyhow::Result<()> { let light_buf = { let light = LightBuffer::new(light_position); let data = bytes_of(&light); - let mut buf = pool.lease(BufferInfo::host_mem( + let mut buf = pool.resource(BufferInfo::host_mem( data.len() as _, vk::BufferUsageFlags::UNIFORM_BUFFER, ))?; - Buffer::copy_from_slice(&mut buf, 0, data); - render_graph.bind_node(buf) + buf.copy_from_slice(0, data); + graph.bind_resource(buf) }; - render_graph.clear_color_image_value(swapchain_image, [0x00, 0x00, 0x00, 0xff]); + graph.clear_color_image(swapchain_image, [0x00, 0x00, 0x00, 0xff]); if let Some(location) = left_hand_location { - let index_buf = render_graph.bind_node(&lincoln_hand_left.index_buf); - let vertex_buf = render_graph.bind_node(&lincoln_hand_left.vertex_buf); - let diffuse_texture = render_graph.bind_node(&lincoln_hand_left_diffuse); - let normal_texture = render_graph.bind_node(&lincoln_hand_left_normal); - let occlusion_texture = render_graph.bind_node(&lincoln_hand_left_occlusion); + let index_buf = graph.bind_resource(&lincoln_hand_left.index_buf); + let vertex_buf = graph.bind_resource(&lincoln_hand_left.vertex_buf); + let diffuse_texture = graph.bind_resource(&lincoln_hand_left_diffuse); + let normal_texture = graph.bind_resource(&lincoln_hand_left_normal); + let occlusion_texture = graph.bind_resource(&lincoln_hand_left_occlusion); let model_transform = pose_transform(location.pose); let push_consts = PushConstants::new(model_transform); - render_graph - .begin_pass("Left hand") - .bind_pipeline(hands_pipeline.hot()) - .set_depth_stencil(DepthStencilMode::DEPTH_WRITE) - .set_multiview(VIEW_MASK, VIEW_MASK) - .store_color(0, swapchain_image) - .clear_depth_stencil(depth_image) - .access_node(index_buf, AccessType::IndexBuffer) - .access_node(vertex_buf, AccessType::VertexBuffer) - .access_descriptor(0, camera_buf, AccessType::VertexShaderReadUniformBuffer) - .access_descriptor(1, light_buf, AccessType::VertexShaderReadUniformBuffer) - .access_descriptor( + graph + .begin_cmd() + .debug_name("Left hand") + .bind_pipeline(&hands_pipeline) + .depth_stencil(DepthStencilInfo::DEPTH_WRITE_LESS_IGNORE_STENCIL) + .multiview(VIEW_MASK, VIEW_MASK) + .resource_access(index_buf, AccessType::IndexBuffer) + .resource_access(vertex_buf, AccessType::VertexBuffer) + .shader_resource_access(0, camera_buf, AccessType::VertexShaderReadUniformBuffer) + .shader_resource_access(1, light_buf, AccessType::VertexShaderReadUniformBuffer) + .shader_resource_access( 2, diffuse_texture, AccessType::FragmentShaderReadSampledImageOrUniformTexelBuffer, ) - .access_descriptor( + .shader_resource_access( 3, normal_texture, AccessType::FragmentShaderReadSampledImageOrUniformTexelBuffer, ) - .access_descriptor( + .shader_resource_access( 4, occlusion_texture, AccessType::FragmentShaderReadSampledImageOrUniformTexelBuffer, ) - .record_subpass(move |subpass, _| { - subpass - .bind_index_buffer(index_buf, vk::IndexType::UINT32) - .bind_vertex_buffer(vertex_buf) - .push_constants(bytes_of(&push_consts)) + .depth_stencil_attachment_image( + depth_image, + LoadOp::CLEAR_ZERO_STENCIL_ZERO, + StoreOp::DontCare, + ) + .color_attachment_image(0, swapchain_image, LoadOp::DontCare, StoreOp::Store) + .record_cmd(move |cmd| { + cmd.bind_index_buffer(index_buf, 0, vk::IndexType::UINT32) + .bind_vertex_buffer(0, vertex_buf, 0) + .push_constants(0, bytes_of(&push_consts)) .draw_indexed(lincoln_hand_left.index_count, 1, 0, 0, 0); }); } if let Some(location) = right_hand_location { - let index_buf = render_graph.bind_node(&lincoln_hand_right.index_buf); - let vertex_buf = render_graph.bind_node(&lincoln_hand_right.vertex_buf); - let diffuse_texture = render_graph.bind_node(&lincoln_hand_right_diffuse); - let normal_texture = render_graph.bind_node(&lincoln_hand_right_normal); - let occlusion_texture = render_graph.bind_node(&lincoln_hand_right_occlusion); + let index_buf = graph.bind_resource(&lincoln_hand_right.index_buf); + let vertex_buf = graph.bind_resource(&lincoln_hand_right.vertex_buf); + let diffuse_texture = graph.bind_resource(&lincoln_hand_right_diffuse); + let normal_texture = graph.bind_resource(&lincoln_hand_right_normal); + let occlusion_texture = graph.bind_resource(&lincoln_hand_right_occlusion); let model_transform = pose_transform(location.pose); let push_consts = PushConstants::new(model_transform); - render_graph - .begin_pass("Right hand") - .bind_pipeline(hands_pipeline.hot()) - .set_depth_stencil(DepthStencilMode::DEPTH_WRITE) - .set_multiview(VIEW_MASK, VIEW_MASK) - .store_color(0, swapchain_image) - .clear_depth_stencil(depth_image) - .access_node(index_buf, AccessType::IndexBuffer) - .access_node(vertex_buf, AccessType::VertexBuffer) - .access_descriptor(0, camera_buf, AccessType::VertexShaderReadUniformBuffer) - .access_descriptor(1, light_buf, AccessType::VertexShaderReadUniformBuffer) - .access_descriptor( + graph + .begin_cmd() + .debug_name("Right hand") + .bind_pipeline(&hands_pipeline) + .depth_stencil(DepthStencilInfo::DEPTH_WRITE_LESS_IGNORE_STENCIL) + .multiview(VIEW_MASK, VIEW_MASK) + .resource_access(index_buf, AccessType::IndexBuffer) + .resource_access(vertex_buf, AccessType::VertexBuffer) + .shader_resource_access(0, camera_buf, AccessType::VertexShaderReadUniformBuffer) + .shader_resource_access(1, light_buf, AccessType::VertexShaderReadUniformBuffer) + .shader_resource_access( 2, diffuse_texture, AccessType::FragmentShaderReadSampledImageOrUniformTexelBuffer, ) - .access_descriptor( + .shader_resource_access( 3, normal_texture, AccessType::FragmentShaderReadSampledImageOrUniformTexelBuffer, ) - .access_descriptor( + .shader_resource_access( 4, occlusion_texture, AccessType::FragmentShaderReadSampledImageOrUniformTexelBuffer, ) - .record_subpass(move |subpass, _| { - subpass - .bind_index_buffer(index_buf, vk::IndexType::UINT32) - .bind_vertex_buffer(vertex_buf) - .push_constants(bytes_of(&push_consts)) + .depth_stencil_attachment_image( + depth_image, + LoadOp::CLEAR_ZERO_STENCIL_ZERO, + StoreOp::DontCare, + ) + .color_attachment_image(0, swapchain_image, LoadOp::DontCare, StoreOp::Store) + .record_cmd(move |cmd| { + cmd.bind_index_buffer(index_buf, 0, vk::IndexType::UINT32) + .bind_vertex_buffer(0, vertex_buf, 0) + .push_constants(0, bytes_of(&push_consts)) .draw_indexed(lincoln_hand_right.index_count, 1, 0, 0, 0); }); } { - let index_buf = render_graph.bind_node(&woolly_mammoth.index_buf); - let vertex_buf = render_graph.bind_node(&woolly_mammoth.vertex_buf); - let normal_texture = render_graph.bind_node(&woolly_mammoth_normal); - let occlusion_texture = render_graph.bind_node(&woolly_mammoth_occlusion); + let index_buf = graph.bind_resource(&woolly_mammoth.index_buf); + let vertex_buf = graph.bind_resource(&woolly_mammoth.vertex_buf); + let normal_texture = graph.bind_resource(&woolly_mammoth_normal); + let occlusion_texture = graph.bind_resource(&woolly_mammoth_occlusion); let push_consts = PushConstants::new(Mat4::IDENTITY); - render_graph - .begin_pass("Woolly Mammoth") - .bind_pipeline(mammoth_pipeline.hot()) - .set_depth_stencil(DepthStencilMode::DEPTH_WRITE) - .set_multiview(VIEW_MASK, VIEW_MASK) - .store_color(0, swapchain_image) - .clear_depth_stencil(depth_image) - .access_node(index_buf, AccessType::IndexBuffer) - .access_node(vertex_buf, AccessType::VertexBuffer) - .access_descriptor(0, camera_buf, AccessType::VertexShaderReadUniformBuffer) - .access_descriptor(1, light_buf, AccessType::VertexShaderReadUniformBuffer) - .access_descriptor( + graph + .begin_cmd() + .debug_name("Woolly Mammoth") + .bind_pipeline(&mammoth_pipeline) + .resource_access(index_buf, AccessType::IndexBuffer) + .resource_access(vertex_buf, AccessType::VertexBuffer) + .shader_resource_access(0, camera_buf, AccessType::VertexShaderReadUniformBuffer) + .shader_resource_access(1, light_buf, AccessType::VertexShaderReadUniformBuffer) + .shader_resource_access( 2, normal_texture, AccessType::FragmentShaderReadSampledImageOrUniformTexelBuffer, ) - .access_descriptor( + .shader_resource_access( 3, occlusion_texture, AccessType::FragmentShaderReadSampledImageOrUniformTexelBuffer, ) - .record_subpass(move |subpass, _| { - subpass - .bind_index_buffer(index_buf, vk::IndexType::UINT32) - .bind_vertex_buffer(vertex_buf) - .push_constants(bytes_of(&push_consts)) + .multiview(VIEW_MASK, VIEW_MASK) + .color_attachment_image(0, swapchain_image, LoadOp::DontCare, StoreOp::Store) + .depth_stencil(DepthStencilInfo::DEPTH_WRITE_LESS_IGNORE_STENCIL) + .depth_stencil_attachment_image( + depth_image, + LoadOp::CLEAR_ZERO_STENCIL_ZERO, + StoreOp::DontCare, + ) + .record_cmd(move |cmd| { + cmd.bind_index_buffer(index_buf, 0, vk::IndexType::UINT32) + .bind_vertex_buffer(0, vertex_buf, 0) + .push_constants(0, bytes_of(&push_consts)) .draw_indexed(lincoln_hand_right.index_count, 1, 0, 0, 0); }); } @@ -471,12 +490,12 @@ fn main() -> anyhow::Result<()> { // the image - afterwards we keep the submitted command buffer around (including all // in-flight resources) so that nothing is dropped until that image is actually done. swapchain.wait_image(xr::Duration::INFINITE).unwrap(); - let cmd_buf = render_graph - .resolve() - .submit(&mut pool, queue_family_index as _, 0) + let swapchain_queue = graph + .into_submission() + .queue_submit(&mut pool, queue_family_index as _, 0) .unwrap(); swapchain.release_image().unwrap(); - graphs[swapchain_image_index as usize] = Some(cmd_buf); + swapchain_queues[swapchain_image_index as usize] = Some(swapchain_queue); frame_stream.end( xr_frame_state.predicted_display_time, @@ -556,7 +575,7 @@ fn device_queue_family_index(device: &Device, flags: vk::QueueFlags) -> Option, path: impl AsRef) -> anyhow::Result { +fn load_model(device: &Device, path: impl AsRef) -> anyhow::Result { trace!("Loading model {}", path.as_ref().display()); let (mut models, _) = load_obj(path.as_ref(), &GPU_LOAD_OPTIONS)?; @@ -679,13 +698,20 @@ fn load_model(device: &Arc, path: impl AsRef) -> anyhow::Result, + face: usize, + vert: usize, + ) { + if let Some(tangent) = tangent { + self.vertex_mut(face, vert)[3..7].copy_from_slice(&tangent.tangent_encoded()); + } } } let mut mesh = Mesh(vertices); - assert!(mikktspace::generate_tangents(&mut mesh)); + assert!(mikktspace::generate_tangents(&mut mesh).is_ok()); let vertices = mesh.0; // Re-index and de-dupe the model vertices using meshopt @@ -717,7 +743,7 @@ fn load_model(device: &Arc, path: impl AsRef) -> anyhow::Result, + device: &Device, path: impl AsRef, fmt: vk::Format, ) -> anyhow::Result> { @@ -759,15 +785,15 @@ fn load_texture( ), )?); - let mut render_graph = RenderGraph::new(); - let staging_buf = render_graph.bind_node(staging_buf); - let texture_image = render_graph.bind_node(&texture); - render_graph.copy_buffer_to_image(staging_buf, texture_image); + let mut graph = Graph::default(); + let staging_buf = graph.bind_resource(staging_buf); + let texture_image = graph.bind_resource(&texture); + graph.copy_buffer_to_image(staging_buf, texture_image); let queue_family_index = device_queue_family_index(device, vk::QueueFlags::TRANSFER).unwrap(); - render_graph - .resolve() - .submit(&mut LazyPool::new(device), queue_family_index as _, 0)? + graph + .into_submission() + .queue_submit(&mut LazyPool::new(device), queue_family_index as _, 0)? .wait_until_executed()?; Ok(texture) diff --git a/examples/vsm_omni.rs b/examples/vsm_omni.rs index 8f471585..a19f4208 100644 --- a/examples/vsm_omni.rs +++ b/examples/vsm_omni.rs @@ -1,21 +1,37 @@ mod profile_with_puffin; use { + ash::vk, bytemuck::{NoUninit, Pod, Zeroable, bytes_of, cast_slice}, clap::Parser, glam::{Mat4, Quat, Vec3, vec3}, - inline_spirv::inline_spirv, log::info, meshopt::remap::{generate_vertex_remap, remap_index_buffer, remap_vertex_buffer}, - screen_13::prelude::*, - screen_13_window::WindowBuilder, std::{ env::current_exe, fs::{metadata, write}, path::{Path, PathBuf}, sync::Arc, + time::Instant, }, tobj::{GPU_LOAD_OPTIONS, load_obj}, + vk_graph::{ + cmd::{LoadOp, StoreOp}, + driver::{ + DriverError, + buffer::{Buffer, BufferInfo}, + compute::{ComputePipeline, ComputePipelineInfo}, + device::Device, + graphic::{DepthStencilInfo, GraphicPipeline, GraphicPipelineInfo}, + image::ImageInfo, + physical_device::Vulkan10Features, + shader::{Shader, SpecializationMap}, + }, + pool::{Lease, Pool, fifo::FifoPool}, + }, + vk_graph_window::Window, + vk_shader_macros::glsl, + vk_sync::AccessType, winit::{dpi::LogicalSize, event::Event, keyboard::KeyCode, window::Fullscreen}, winit_input_helper::WinitInputHelper, }; @@ -48,7 +64,7 @@ fn main() -> anyhow::Result<()> { let mut input = WinitInputHelper::default(); let args = Args::parse(); - let window = WindowBuilder::default() + let window = Window::builder() .debug(args.debug) .window(|window| window.with_inner_size(LogicalSize::new(800, 600))) .build()?; @@ -102,27 +118,29 @@ fn main() -> anyhow::Result<()> { let mut pool = FifoPool::new(&window.device); let mut elapsed = 0.0; + let mut prev_frame_at = Instant::now(); window.run(|frame| { - input.step_with_window_events( - &frame - .events - .iter() - .filter_map(|event| { - if let Event::WindowEvent { event, .. } = event { - Some(event.clone()) - } else { - None - } - }) - .collect::>(), - ); + let now = Instant::now(); + let dt = now - prev_frame_at; + prev_frame_at = now; + + input.step(); + for event in frame.events { + match event { + Event::WindowEvent { event, .. } => { + let _ = input.process_window_event(event); + } + Event::DeviceEvent { event, .. } => { + input.process_device_event(event); + } + _ => {} + } + } + input.end_step(); // Hold spacebar to stop the light if !input.key_held(KeyCode::Space) { - elapsed += input - .delta_time() - .map(|dt| dt.as_secs_f32()) - .unwrap_or(0.016); + elapsed += dt.as_secs_f32(); } // Hit F11 to enable borderless fullscreen @@ -177,24 +195,24 @@ fn main() -> anyhow::Result<()> { }; // Bind resources to the render graph of the current frame - let cube_mesh_index_buf = frame.render_graph.bind_node(&cube_mesh.index_buf); - let cube_mesh_vertex_buf = frame.render_graph.bind_node(&cube_mesh.vertex_buf); - let cube_shadow_index_buf = frame.render_graph.bind_node(&cube_shadow.index_buf); - let cube_shadow_vertex_buf = frame.render_graph.bind_node(&cube_shadow.vertex_buf); - let model_mesh_index_buf = frame.render_graph.bind_node(&model_mesh.index_buf); - let model_mesh_vertex_buf = frame.render_graph.bind_node(&model_mesh.vertex_buf); - let model_shadow_index_buf = frame.render_graph.bind_node(&model_shadow.index_buf); - let model_shadow_vertex_buf = frame.render_graph.bind_node(&model_shadow.vertex_buf); + let cube_mesh_index_buf = frame.graph.bind_resource(&cube_mesh.index_buf); + let cube_mesh_vertex_buf = frame.graph.bind_resource(&cube_mesh.vertex_buf); + let cube_shadow_index_buf = frame.graph.bind_resource(&cube_shadow.index_buf); + let cube_shadow_vertex_buf = frame.graph.bind_resource(&cube_shadow.vertex_buf); + let model_mesh_index_buf = frame.graph.bind_resource(&model_mesh.index_buf); + let model_mesh_vertex_buf = frame.graph.bind_resource(&model_mesh.vertex_buf); + let model_shadow_index_buf = frame.graph.bind_resource(&model_shadow.index_buf); + let model_shadow_vertex_buf = frame.graph.bind_resource(&model_shadow.vertex_buf); let camera_uniform_buf = frame - .render_graph - .bind_node(lease_uniform_buffer(&mut pool, &camera).unwrap()); + .graph + .bind_resource(lease_uniform_buffer(&mut pool, &camera).unwrap()); let light_uniform_buf = frame - .render_graph - .bind_node(lease_uniform_buffer(&mut pool, &light).unwrap()); + .graph + .bind_resource(lease_uniform_buffer(&mut pool, &light).unwrap()); // Lease and bind a cube-compatible shadow 2D image array to the graph of the current frame let shadow_faces_image = pool - .lease( + .resource( ImageInfo::image_2d_array( CUBEMAP_SIZE, CUBEMAP_SIZE, @@ -204,21 +222,21 @@ fn main() -> anyhow::Result<()> { | vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::STORAGE, ) - .to_builder() + .into_builder() .flags(vk::ImageCreateFlags::CUBE_COMPATIBLE), ) .unwrap(); let shadow_faces_info = shadow_faces_image.info; - let shadow_faces_node = frame.render_graph.bind_node(shadow_faces_image); + let shadow_faces_node = frame.graph.bind_resource(shadow_faces_image); // Lease and bind a temporary image we'll use during blur passes let temp_image = frame - .render_graph - .bind_node(pool.lease(shadow_faces_info).unwrap()); + .graph + .bind_resource(pool.resource(shadow_faces_info).unwrap()); // Lastly we lease and bind depth images needed for rendering - let shadow_depth_image = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d_array( + let shadow_depth_image = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d_array( frame.width, frame.height, if use_geometry_shader { 6 } else { 1 }, @@ -227,8 +245,8 @@ fn main() -> anyhow::Result<()> { )) .unwrap(), ); - let depth_image = frame.render_graph.bind_node( - pool.lease(ImageInfo::image_2d( + let depth_image = frame.graph.bind_resource( + pool.resource(ImageInfo::image_2d( frame.width, frame.height, depth_format, @@ -240,65 +258,88 @@ fn main() -> anyhow::Result<()> { // Hold tab to view a debug mode if input.key_held(KeyCode::Tab) { frame - .render_graph - .begin_pass("DEBUG") + .graph + .begin_cmd() + .debug_name("DEBUG") .bind_pipeline(&debug_pipeline) - .set_depth_stencil(DepthStencilMode::DEPTH_WRITE) - .access_descriptor( + .depth_stencil(DepthStencilInfo::DEPTH_WRITE_LESS_IGNORE_STENCIL) + .shader_resource_access( 0, camera_uniform_buf, AccessType::AnyShaderReadUniformBuffer, ) - .access_descriptor(1, light_uniform_buf, AccessType::AnyShaderReadUniformBuffer) - .access_node(model_mesh_index_buf, AccessType::IndexBuffer) - .access_node(model_mesh_vertex_buf, AccessType::VertexBuffer) - .access_node(cube_mesh_index_buf, AccessType::IndexBuffer) - .access_node(cube_mesh_vertex_buf, AccessType::VertexBuffer) - .clear_color(0, frame.swapchain_image) - .store_color(0, frame.swapchain_image) - .clear_depth_stencil(depth_image) - .store_depth_stencil(depth_image) - .record_subpass(move |subpass, _| { - subpass - .bind_index_buffer(model_mesh_index_buf, vk::IndexType::UINT32) - .bind_vertex_buffer(model_mesh_vertex_buf) - .push_constants(cast_slice(&model_transform)) + .shader_resource_access( + 1, + light_uniform_buf, + AccessType::AnyShaderReadUniformBuffer, + ) + .resource_access(model_mesh_index_buf, AccessType::IndexBuffer) + .resource_access(model_mesh_vertex_buf, AccessType::VertexBuffer) + .resource_access(cube_mesh_index_buf, AccessType::IndexBuffer) + .resource_access(cube_mesh_vertex_buf, AccessType::VertexBuffer) + .depth_stencil_attachment_image( + depth_image, + LoadOp::CLEAR_ONE_STENCIL_ZERO, + StoreOp::Store, + ) + .color_attachment_image( + 0, + frame.swapchain_image, + LoadOp::CLEAR_BLACK_ALPHA_ZERO, + StoreOp::Store, + ) + .record_cmd(move |cmd| { + cmd.bind_index_buffer(model_mesh_index_buf, 0, vk::IndexType::UINT32) + .bind_vertex_buffer(0, model_mesh_vertex_buf, 0) + .push_constants(0, cast_slice(&model_transform)) .draw_indexed(model_mesh.index_count, 1, 0, 0, 0) - .bind_index_buffer(cube_mesh_index_buf, vk::IndexType::UINT32) - .bind_vertex_buffer(cube_mesh_vertex_buf) - .push_constants(cast_slice(&cube_transform)) + .bind_index_buffer(cube_mesh_index_buf, 0, vk::IndexType::UINT32) + .bind_vertex_buffer(0, cube_mesh_vertex_buf, 0) + .push_constants(0, cast_slice(&cube_transform)) .draw_indexed(cube_mesh.index_count, 1, 0, 0, 0); }); } else { // Render the omni light point of view into the six-layer image we leased above if use_geometry_shader { frame - .render_graph - .begin_pass("Shadow (Using geometry shader)") + .graph + .begin_cmd() + .debug_name("Shadow (Using geometry shader)") .bind_pipeline(&shadow_pipeline) - .set_depth_stencil(DepthStencilMode::DEPTH_WRITE) - .access_descriptor(0, light_uniform_buf, AccessType::AnyShaderReadUniformBuffer) - .access_node(model_shadow_index_buf, AccessType::IndexBuffer) - .access_node(model_shadow_vertex_buf, AccessType::VertexBuffer) - .access_node(cube_shadow_index_buf, AccessType::IndexBuffer) - .access_node(cube_shadow_vertex_buf, AccessType::VertexBuffer) - .clear_color_value(0, shadow_faces_node, [light.range, light.range, 0.0, 0.0]) - .store_color(0, shadow_faces_node) - .clear_depth_stencil(shadow_depth_image) - .record_subpass(move |subpass, _| { - subpass - .bind_index_buffer(model_shadow_index_buf, vk::IndexType::UINT32) - .bind_vertex_buffer(model_shadow_vertex_buf) - .push_constants(cast_slice(&model_transform)) + .depth_stencil(DepthStencilInfo::DEPTH_WRITE_LESS_IGNORE_STENCIL) + .shader_resource_access( + 0, + light_uniform_buf, + AccessType::AnyShaderReadUniformBuffer, + ) + .resource_access(model_shadow_index_buf, AccessType::IndexBuffer) + .resource_access(model_shadow_vertex_buf, AccessType::VertexBuffer) + .resource_access(cube_shadow_index_buf, AccessType::IndexBuffer) + .resource_access(cube_shadow_vertex_buf, AccessType::VertexBuffer) + .depth_stencil_attachment_image( + shadow_depth_image, + LoadOp::CLEAR_ONE_STENCIL_ZERO, + StoreOp::DontCare, + ) + .color_attachment_image( + 0, + shadow_faces_node, + LoadOp::clear_rgba(light.range, light.range, 0.0, 0.0), + StoreOp::Store, + ) + .record_cmd(move |cmd| { + cmd.bind_index_buffer(model_shadow_index_buf, 0, vk::IndexType::UINT32) + .bind_vertex_buffer(0, model_shadow_vertex_buf, 0) + .push_constants(0, cast_slice(&model_transform)) .draw_indexed(model_shadow.index_count, 1, 0, 0, 0) - .bind_index_buffer(cube_shadow_index_buf, vk::IndexType::UINT32) - .bind_vertex_buffer(cube_shadow_vertex_buf) - .push_constants(cast_slice(&cube_transform)) + .bind_index_buffer(cube_shadow_index_buf, 0, vk::IndexType::UINT32) + .bind_vertex_buffer(0, cube_shadow_vertex_buf, 0) + .push_constants(0, cast_slice(&cube_transform)) .draw_indexed(cube_shadow.index_count, 1, 0, 0, 0); }); } else { for array_layer in 0..6 { - let mut shadow_faces_view_info = shadow_faces_info.default_view_info(); + let mut shadow_faces_view_info = shadow_faces_info.into_image_view(); shadow_faces_view_info.array_layer_count = 1; shadow_faces_view_info.base_array_layer = array_layer; @@ -313,40 +354,44 @@ fn main() -> anyhow::Result<()> { }; let light_uniform_buf = frame - .render_graph - .bind_node(lease_uniform_buffer(&mut pool, &light).unwrap()); + .graph + .bind_resource(lease_uniform_buffer(&mut pool, &light).unwrap()); frame - .render_graph - .begin_pass("Shadow") + .graph + .begin_cmd() + .debug_name("Shadow") .bind_pipeline(&shadow_pipeline) - .set_depth_stencil(DepthStencilMode::DEPTH_WRITE) - .access_descriptor( + .depth_stencil(DepthStencilInfo::DEPTH_WRITE_LESS_IGNORE_STENCIL) + .shader_resource_access( 0, light_uniform_buf, AccessType::AnyShaderReadUniformBuffer, ) - .access_node(model_shadow_index_buf, AccessType::IndexBuffer) - .access_node(model_shadow_vertex_buf, AccessType::VertexBuffer) - .access_node(cube_shadow_index_buf, AccessType::IndexBuffer) - .access_node(cube_shadow_vertex_buf, AccessType::VertexBuffer) - .clear_color_value_as( + .resource_access(model_shadow_index_buf, AccessType::IndexBuffer) + .resource_access(model_shadow_vertex_buf, AccessType::VertexBuffer) + .resource_access(cube_shadow_index_buf, AccessType::IndexBuffer) + .resource_access(cube_shadow_vertex_buf, AccessType::VertexBuffer) + .depth_stencil_attachment_image( + shadow_depth_image, + LoadOp::CLEAR_ONE_STENCIL_ZERO, + StoreOp::DontCare, + ) + .color_attachment_image_view( 0, shadow_faces_node, - [light.range, light.range, 0.0, 0.0], shadow_faces_view_info, + LoadOp::clear_rgba(light.range, light.range, 0.0, 0.0), + StoreOp::Store, ) - .store_color_as(0, shadow_faces_node, shadow_faces_view_info) - .clear_depth_stencil(shadow_depth_image) - .record_subpass(move |subpass, _| { - subpass - .bind_index_buffer(model_shadow_index_buf, vk::IndexType::UINT32) - .bind_vertex_buffer(model_shadow_vertex_buf) - .push_constants(cast_slice(&model_transform)) + .record_cmd(move |cmd| { + cmd.bind_index_buffer(model_shadow_index_buf, 0, vk::IndexType::UINT32) + .bind_vertex_buffer(0, model_shadow_vertex_buf, 0) + .push_constants(0, cast_slice(&model_transform)) .draw_indexed(model_shadow.index_count, 1, 0, 0, 0) - .bind_index_buffer(cube_shadow_index_buf, vk::IndexType::UINT32) - .bind_vertex_buffer(cube_shadow_vertex_buf) - .push_constants(cast_slice(&cube_transform)) + .bind_index_buffer(cube_shadow_index_buf, 0, vk::IndexType::UINT32) + .bind_vertex_buffer(0, cube_shadow_vertex_buf, 0) + .push_constants(0, cast_slice(&cube_transform)) .draw_indexed(cube_shadow.index_count, 1, 0, 0, 0); }); } @@ -357,60 +402,84 @@ fn main() -> anyhow::Result<()> { // Flip-flop between the shadow image and a temporary image using a // separable box blur filter which approximates a gaussian blur frame - .render_graph - .begin_pass("Blur X") + .graph + .begin_cmd() + .debug_name("Blur X") .bind_pipeline(&blur_x_pipeline) - .read_descriptor(0, shadow_faces_node) - .write_descriptor(1, temp_image) - .record_compute(move |compute, _| { - compute.dispatch(1, CUBEMAP_SIZE, 6); + .shader_resource_access( + 0, + shadow_faces_node, + AccessType::ComputeShaderReadOther, + ) + .shader_resource_access(1, temp_image, AccessType::ComputeShaderWrite) + .record_cmd(move |cmd| { + cmd.dispatch(1, CUBEMAP_SIZE, 6); }) - .submit_pass() - .begin_pass("Blur Y") + .end_cmd() + .begin_cmd() + .debug_name("Blur Y") .bind_pipeline(&blur_y_pipeline) - .read_descriptor(0, temp_image) - .write_descriptor(1, shadow_faces_node) - .record_compute(move |compute, _| { - compute.dispatch(CUBEMAP_SIZE, 1, 6); + .shader_resource_access(0, temp_image, AccessType::ComputeShaderReadOther) + .shader_resource_access( + 1, + shadow_faces_node, + AccessType::ComputeShaderWrite, + ) + .record_cmd(move |cmd| { + cmd.dispatch(CUBEMAP_SIZE, 1, 6); }); } } // Render the scene directly to the swapchain using the shadow map from the above pass frame - .render_graph - .begin_pass("Mesh objects") + .graph + .begin_cmd() + .debug_name("Mesh objects") .bind_pipeline(&mesh_pipeline) - .set_depth_stencil(DepthStencilMode::DEPTH_WRITE) - .access_descriptor( + .depth_stencil(DepthStencilInfo::DEPTH_WRITE_LESS_IGNORE_STENCIL) + .shader_resource_access( 0, camera_uniform_buf, AccessType::AnyShaderReadUniformBuffer, ) - .access_descriptor(1, light_uniform_buf, AccessType::AnyShaderReadUniformBuffer) - .read_descriptor_as( + .shader_resource_access( + 1, + light_uniform_buf, + AccessType::AnyShaderReadUniformBuffer, + ) + .shader_subresource_access( 2, shadow_faces_node, shadow_faces_info - .default_view_info() + .into_image_view() .with_type(vk::ImageViewType::CUBE), + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, ) - .access_node(model_mesh_index_buf, AccessType::IndexBuffer) - .access_node(model_mesh_vertex_buf, AccessType::VertexBuffer) - .access_node(cube_mesh_index_buf, AccessType::IndexBuffer) - .access_node(cube_mesh_vertex_buf, AccessType::VertexBuffer) - .clear_color(0, frame.swapchain_image) - .store_color(0, frame.swapchain_image) - .clear_depth_stencil(depth_image) - .record_subpass(move |subpass, _| { - subpass - .bind_index_buffer(model_mesh_index_buf, vk::IndexType::UINT32) - .bind_vertex_buffer(model_mesh_vertex_buf) - .push_constants(cast_slice(&model_transform)) - .draw_indexed(model_mesh.index_count, 1, 0, 0, 0) - .bind_index_buffer(cube_mesh_index_buf, vk::IndexType::UINT32) - .bind_vertex_buffer(cube_mesh_vertex_buf) - .push_constants(cast_slice(&cube_transform)) + .resource_access(model_mesh_index_buf, AccessType::IndexBuffer) + .resource_access(model_mesh_vertex_buf, AccessType::VertexBuffer) + .resource_access(cube_mesh_index_buf, AccessType::IndexBuffer) + .resource_access(cube_mesh_vertex_buf, AccessType::VertexBuffer) + .depth_stencil_attachment_image( + depth_image, + LoadOp::CLEAR_ONE_STENCIL_ZERO, + StoreOp::DontCare, + ) + .color_attachment_image( + 0, + frame.swapchain_image, + LoadOp::CLEAR_BLACK_ALPHA_ZERO, + StoreOp::Store, + ) + .record_cmd(move |cmd| { + cmd.bind_index_buffer(model_mesh_index_buf, 0, vk::IndexType::UINT32) + .bind_vertex_buffer(0, model_mesh_vertex_buf, 0) + .push_constants(0, cast_slice(&model_transform)) + .draw_indexed(model_mesh.index_count, 1, 0, 0, 0); + + cmd.bind_index_buffer(cube_mesh_index_buf, 0, vk::IndexType::UINT32) + .bind_vertex_buffer(0, cube_mesh_vertex_buf, 0) + .push_constants(0, cast_slice(&cube_transform)) .draw_indexed(cube_mesh.index_count, 1, 0, 0, 0); }); } @@ -426,8 +495,7 @@ fn best_2d_optimal_format( flags: vk::ImageCreateFlags, ) -> vk::Format { for format in formats { - let format_props = Device::image_format_properties( - device, + let format_props = device.physical_device.image_format_properties( *format, vk::ImageType::TYPE_2D, vk::ImageTiling::OPTIMAL, @@ -443,10 +511,11 @@ fn best_2d_optimal_format( panic!("Unsupported format"); } -fn create_blur_x_pipeline(device: &Arc) -> Result, DriverError> { - let comp = inline_spirv!( +fn create_blur_x_pipeline(device: &Device) -> Result { + let comp = glsl!( r#" #version 450 core + #pragma shader_stage(compute) #define POS_X 0 #define NEG_X 1 @@ -536,40 +605,26 @@ fn create_blur_x_pipeline(device: &Arc) -> Result, accumulator -= imageLoad(image, ivec3(x - RADIUS, y, face)).rg; } } - "#, - comp + "# ); - let shader = Shader::new_compute(comp.as_slice()).specialization_info(SpecializationInfo::new( - vec![ - vk::SpecializationMapEntry { - constant_id: 0, - offset: 0, - size: 4, - }, - vk::SpecializationMapEntry { - constant_id: 1, - offset: 4, - size: 4, - }, - ], - bytes_of(&Blur { + let shader = Shader::new_compute(comp.as_slice()).specialization( + SpecializationMap::new(bytes_of(&Blur { image_size: CUBEMAP_SIZE, radius: BLUR_RADIUS, - }), - )); + })) + .constant(0, 0, 4) + .constant(1, 4, 4), + ); - Ok(Arc::new(ComputePipeline::create( - device, - ComputePipelineInfo::default(), - shader, - )?)) + ComputePipeline::create(device, ComputePipelineInfo::default(), shader) } -fn create_blur_y_pipeline(device: &Arc) -> Result, DriverError> { - let comp = inline_spirv!( +fn create_blur_y_pipeline(device: &Device) -> Result { + let comp = glsl!( r#" #version 450 core + #pragma shader_stage(compute) #define POS_X 0 #define NEG_X 1 @@ -659,40 +714,26 @@ fn create_blur_y_pipeline(device: &Arc) -> Result, accumulator -= imageLoad(image, ivec3(x, y - RADIUS, face)).rg; } } - "#, - comp + "# ); - let shader = Shader::new_compute(comp.as_slice()).specialization_info(SpecializationInfo::new( - vec![ - vk::SpecializationMapEntry { - constant_id: 0, - offset: 0, - size: 4, - }, - vk::SpecializationMapEntry { - constant_id: 1, - offset: 4, - size: 4, - }, - ], - bytes_of(&Blur { + let shader = Shader::new_compute(comp.as_slice()).specialization( + SpecializationMap::new(bytes_of(&Blur { image_size: CUBEMAP_SIZE, radius: BLUR_RADIUS, - }), - )); + })) + .constant(0, 0, 4) + .constant(1, 4, 4), + ); - Ok(Arc::new(ComputePipeline::create( - device, - ComputePipelineInfo::default(), - shader, - )?)) + ComputePipeline::create(device, ComputePipelineInfo::default(), shader) } -fn create_debug_pipeline(device: &Arc) -> Result, DriverError> { - let vert = inline_spirv!( +fn create_debug_pipeline(device: &Device) -> Result { + let vert = glsl!( r#" #version 450 core + #pragma shader_stage(vertex) layout(push_constant) uniform PushConstants { layout(offset = 0) mat4 model; @@ -714,12 +755,12 @@ fn create_debug_pipeline(device: &Arc) -> Result, D world_normal_out = normalize((push_const.model * vec4(normal, 1)).xyz); gl_Position = camera.projection * camera.view * vec4(world_position_out, 1); } - "#, - vert + "# ); - let frag = inline_spirv!( + let frag = glsl!( r#" #version 450 core + #pragma shader_stage(fragment) layout(binding = 1) uniform Light { vec3 position; @@ -749,26 +790,24 @@ fn create_debug_pipeline(device: &Arc) -> Result, D color_out.rgb = vec3(lambertian * attenuation); } } - "#, - frag + "# ); - let info = GraphicPipelineInfo::default(); - - Ok(Arc::new(GraphicPipeline::create( + GraphicPipeline::create( device, - info, + GraphicPipelineInfo::default(), [ Shader::new_vertex(vert.as_slice()), Shader::new_fragment(frag.as_slice()), ], - )?)) + ) } -fn create_mesh_pipeline(device: &Arc) -> Result, DriverError> { - let vert = inline_spirv!( +fn create_mesh_pipeline(device: &Device) -> Result { + let vert = glsl!( r#" #version 450 core + #pragma shader_stage(vertex) layout(push_constant) uniform PushConstants { layout(offset = 0) mat4 model; @@ -794,12 +833,12 @@ fn create_mesh_pipeline(device: &Arc) -> Result, Dr world_position_out = push_const.model * vec4(position, 1.0); gl_Position = camera.projection * camera.view * world_position_out; } - "#, - vert + "# ); - let frag = inline_spirv!( + let frag = glsl!( r#" #version 450 core + #pragma shader_stage(fragment) #define EPSILON 0.0001 #define MIN_SHADOW 0.1 @@ -861,33 +900,25 @@ fn create_mesh_pipeline(device: &Arc) -> Result, Dr color_out.rgb = vec3(attenuation * lambertian * shadow); } } - "#, - frag + "# ); - let info = GraphicPipelineInfo::default(); - - Ok(Arc::new(GraphicPipeline::create( + GraphicPipeline::create( device, - info, + GraphicPipelineInfo::default(), [ Shader::new_vertex(vert.as_slice()), - Shader::new_fragment(frag.as_slice()).specialization_info(SpecializationInfo::new( - vec![vk::SpecializationMapEntry { - constant_id: 0, - offset: 0, - size: 4, - }], - bytes_of(&SHADOW_BIAS), - )), + Shader::new_fragment(frag.as_slice()) + .specialization(SpecializationMap::new(bytes_of(&SHADOW_BIAS)).constant(0, 0, 4)), ], - )?)) + ) } -fn create_shadow_pipeline(device: &Arc) -> Result, DriverError> { - let vert = inline_spirv!( +fn create_shadow_pipeline(device: &Device) -> Result { + let vert = glsl!( r#" #version 450 core + #pragma shader_stage(vertex) layout(push_constant) uniform PushConstants { layout(offset = 0) mat4 model; @@ -914,12 +945,12 @@ fn create_shadow_pipeline(device: &Arc) -> Result, gl_Position = light.projection * light.view * world_position; world_position_out = world_position.xyz; } - "#, - vert + "# ); - let frag = inline_spirv!( + let frag = glsl!( r#" #version 450 core + #pragma shader_stage(fragment) layout(binding = 0) uniform Light { vec3 position; @@ -937,28 +968,26 @@ fn create_shadow_pipeline(device: &Arc) -> Result, color_out.x = dist; color_out.y = dist * dist; } - "#, - frag + "# ); - let info = GraphicPipelineInfo::default(); - - Ok(Arc::new(GraphicPipeline::create( + GraphicPipeline::create( device, - info, + GraphicPipelineInfo::default(), [ Shader::new_vertex(vert.as_slice()), Shader::new_fragment(frag.as_slice()), ], - )?)) + ) } fn create_shadow_pipeline_with_geometry_shader( - device: &Arc, -) -> Result, DriverError> { - let vert = inline_spirv!( + device: &Device, +) -> Result { + let vert = glsl!( r#" #version 450 core + #pragma shader_stage(vertex) #define TAN_HALF_FOVY 1.0 // tan(45deg) #define ASPECT_RATIO 1.0 @@ -1040,14 +1069,19 @@ fn create_shadow_pipeline_with_geometry_shader( view_positions_out.positions[4] = view4_pos; view_positions_out.positions[5] = view5_pos; } - "#, - vert + "# ); - let geom = inline_spirv!( + let geom = glsl!( r#" #version 450 core + #pragma shader_stage(geometry) - #define CLIP mat4(1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5, 1.0) + const mat4 CLIP = mat4( + 1.0, 0.0, 0.0, 0.0, + 0.0, -1.0, 0.0, 0.0, + 0.0, 0.0, 0.5, 0.0, + 0.0, 0.0, 0.5, 1.0 + ); struct ViewPositions { vec4 positions[6]; @@ -1111,12 +1145,12 @@ fn create_shadow_pipeline_with_geometry_shader( gl_Layer = 5; emit(0x64, 5); } - "#, - geom + "# ); - let frag = inline_spirv!( + let frag = glsl!( r#" #version 450 core + #pragma shader_stage(fragment) layout(binding = 0) uniform Light { vec3 position; @@ -1134,13 +1168,12 @@ fn create_shadow_pipeline_with_geometry_shader( color_out.x = dist; color_out.y = dist * dist; } - "#, - frag + "# ); let info = GraphicPipelineInfo::default(); - Ok(Arc::new(GraphicPipeline::create( + GraphicPipeline::create( device, info, [ @@ -1148,7 +1181,7 @@ fn create_shadow_pipeline_with_geometry_shader( Shader::new_geometry(geom.as_slice()), Shader::new_fragment(frag.as_slice()), ], - )?)) + ) } fn download_model_from_github(model_name: &str) -> anyhow::Result { @@ -1175,11 +1208,11 @@ fn lease_uniform_buffer( data: &impl NoUninit, ) -> Result, DriverError> { let data = bytes_of(data); - let mut buf = pool.lease(BufferInfo::host_mem( + let mut buf = pool.resource(BufferInfo::host_mem( data.len() as _, vk::BufferUsageFlags::UNIFORM_BUFFER, ))?; - Buffer::copy_from_slice(&mut buf, 0, data); + buf.copy_from_slice(0, data); Ok(buf) } @@ -1270,7 +1303,7 @@ fn load_cube_data() -> [[f32; 6]; 36] { } /// Loads a cube as indexed position and normal vertices -fn load_cube_mesh(device: &Arc) -> Result { +fn load_cube_mesh(device: &Device) -> Result { let vertices = load_cube_data(); let indices = (0u32..vertices.len() as u32).collect::>(); @@ -1293,7 +1326,7 @@ fn load_cube_mesh(device: &Arc) -> Result { } /// Loads a cube as indexed position vertices -fn load_cube_shadow(device: &Arc) -> Result { +fn load_cube_shadow(device: &Device) -> Result { let vertices = load_cube_data() .iter() .map(|vertex| [vertex[0], vertex[1], vertex[2]]) @@ -1319,7 +1352,7 @@ fn load_cube_shadow(device: &Arc) -> Result { } fn load_model( - device: &Arc, + device: &Device, path: impl AsRef, face_fn: fn(a: Vec3, b: Vec3, c: Vec3) -> [T; 3], ) -> anyhow::Result @@ -1389,9 +1422,9 @@ where } /// Loads an .obj model as indexed position and normal vertices -fn load_model_mesh(device: &Arc, path: impl AsRef) -> anyhow::Result { +fn load_model_mesh(device: &Device, path: impl AsRef) -> anyhow::Result { #[repr(C)] - #[derive(Clone, Copy, Default, NoUninit)] + #[derive(Clone, Copy, Default, Pod, Zeroable)] struct Vertex { position: Vec3, normal: Vec3, @@ -1426,9 +1459,9 @@ fn load_model_mesh(device: &Arc, path: impl AsRef) -> anyhow::Resu } /// Loads an .obj model as indexed position vertices -fn load_model_shadow(device: &Arc, path: impl AsRef) -> anyhow::Result { +fn load_model_shadow(device: &Device, path: impl AsRef) -> anyhow::Result { #[repr(C)] - #[derive(Clone, Copy, Default, NoUninit)] + #[derive(Clone, Copy, Default, Pod, Zeroable)] struct Vertex { position: Vec3, } diff --git a/guide/.gitignore b/guide/.gitignore new file mode 100644 index 00000000..e9c07289 --- /dev/null +++ b/guide/.gitignore @@ -0,0 +1 @@ +book \ No newline at end of file diff --git a/guide/Cargo.toml b/guide/Cargo.toml new file mode 100644 index 00000000..b3a8ce66 --- /dev/null +++ b/guide/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "guide" +authors = ["John Wells "] +edition.workspace = true +license.workspace = true +publish = false + +[lib] +name = "guide" +path = "doctest.rs" + +[features] +default = [] +hot = ["dep:vk-graph-hot"] + +[dependencies] +bytemuck = "1.25" +vk-graph.workspace = true +vk-graph-hot = { optional = true, workspace = true } +vk-graph-window.workspace = true diff --git a/guide/README.md b/guide/README.md new file mode 100644 index 00000000..8c0d02fa --- /dev/null +++ b/guide/README.md @@ -0,0 +1 @@ +# Guide diff --git a/guide/book.toml b/guide/book.toml new file mode 100644 index 00000000..8cc33366 --- /dev/null +++ b/guide/book.toml @@ -0,0 +1,13 @@ +[book] +title = "vk-graph" +authors = ["John Wells"] +language = "en" + +[build] +extra-watch-dirs = ["guide-helper/src"] + +[output.html] +smart-punctuation = true + +[preprocessor.guide-helper] +command = "cargo run --quiet --manifest-path guide-helper/Cargo.toml" diff --git a/guide/doctest.rs b/guide/doctest.rs new file mode 100644 index 00000000..b7fc3ca9 --- /dev/null +++ b/guide/doctest.rs @@ -0,0 +1,47 @@ +// HACK: Test tooling for mbdook is lacking so this fix is applied: +// https://github.com/BurntSushi/jiff/blob/985b4156c4dbaf2ed69150a4ec4e6d5352f1d47e/book/doctest.rs + +#[doc = include_str!("src/cmd.md")] +pub mod cmd {} + +#[doc = include_str!("src/cmd_compute.md")] +pub mod cmd_compute {} + +#[doc = include_str!("src/cmd_graphic.md")] +pub mod cmd_graphic {} + +#[doc = include_str!("src/cmd_ray_trace.md")] +pub mod cmd_ray_trace {} + +#[doc = include_str!("src/pipeline.md")] +pub mod pipeline {} + +#[doc = include_str!("src/pipeline_hot_reload.md")] +pub mod pipeline_hot_reload {} + +#[doc = include_str!("src/pipeline_push_const.md")] +pub mod pipeline_push_const {} + +#[doc = include_str!("src/pipeline_sync.md")] +pub mod pipeline_sync {} + +#[doc = include_str!("src/pipeline_spec.md")] +pub mod pipeline_spec {} + +#[doc = include_str!("src/resource_accel_struct.md")] +pub mod resource_accel_struct {} + +#[doc = include_str!("src/resource_buffer.md")] +pub mod resource_buffer {} + +#[doc = include_str!("src/resource_image.md")] +pub mod resource_image {} + +#[doc = include_str!("src/usage.md")] +pub mod usage {} + +#[doc = include_str!("src/usage_device.md")] +pub mod usage_device {} + +#[doc = include_str!("src/usage_shader.md")] +pub mod usage_shader {} diff --git a/guide/guide-helper/Cargo.toml b/guide/guide-helper/Cargo.toml new file mode 100644 index 00000000..c9e1e550 --- /dev/null +++ b/guide/guide-helper/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "guide-helper" +authors = ["John Wells "] +edition.workspace = true +license.workspace = true +publish = false + +[dependencies] +cargo_toml = "0.22" +mdbook-preprocessor = { version = "0.5.2" } +semver = "1.0.27" +serde_json = "1.0.149" diff --git a/guide/guide-helper/README.md b/guide/guide-helper/README.md new file mode 100644 index 00000000..60d4315c --- /dev/null +++ b/guide/guide-helper/README.md @@ -0,0 +1,8 @@ +# Guide Book source code + +To build the book you will need +[`mdbook`](https://rust-lang.github.io/mdBook/guide/installation.html): + +```bash +cargo install mdbook +``` diff --git a/guide/guide-helper/src/lib.rs b/guide/guide-helper/src/lib.rs new file mode 100644 index 00000000..f0f2abb9 --- /dev/null +++ b/guide/guide-helper/src/lib.rs @@ -0,0 +1,302 @@ +//! Preprocessor for the vk-graph guide. + +use { + cargo_toml::Manifest, + mdbook_preprocessor::{ + Preprocessor, PreprocessorContext, + book::Book, + errors::{Error, Result}, + }, + semver::{Version, VersionReq}, + std::{io, path::PathBuf, sync::LazyLock}, +}; + +const LATEST_KNOWN_VULKAN_SDK_VERSION: &str = VULKAN_SDK_VERSION_1_3_281; +const VULKAN_SDK_VERSION_1_3_281: &str = "1.3.281"; + +static WORKSPACE_ROOT: LazyLock = LazyLock::new(|| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("missing guide dir") + .parent() + .expect("missing workspace root") + .to_path_buf() +}); + +/// Preprocessing entry point. +pub fn handle_preprocessing() -> Result<()> { + let pre = GuideHelper; + let (ctx, book) = mdbook_preprocessor::parse_input(io::stdin())?; + + let book_version = Version::parse(&ctx.mdbook_version)?; + let version_req = VersionReq::parse(mdbook_preprocessor::MDBOOK_VERSION)?; + + if !version_req.matches(&book_version) { + eprintln!( + "warning: The {} plugin was built against version {} of mdbook, \ + but we're being called from version {}", + pre.name(), + mdbook_preprocessor::MDBOOK_VERSION, + ctx.mdbook_version + ); + } + + let processed_book = pre.run(&ctx, book)?; + serde_json::to_writer(io::stdout(), &processed_book)?; + + Ok(()) +} + +struct GuideHelper; + +impl Preprocessor for GuideHelper { + fn name(&self) -> &str { + "guide-helper" + } + + fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result { + insert_crate_version(&mut book); + insert_dependency_req(&mut book, "log"); + insert_dependency_req(&mut book, "profiling"); + insert_member_crate_version(&mut book, "vk-graph-hot", "crates/vk-graph-hot/Cargo.toml"); + insert_member_crate_version( + &mut book, + "vk-graph-window", + "crates/vk-graph-window/Cargo.toml", + ); + insert_vulkan_sdk_version(&mut book); + + // mdBook renders the raw `#` lines literally, but compile-only example + // lines should still keep those lines available for doctests. + if _ctx.renderer == "html" { + hide_hidden_lines(&mut book); + } + + let mut ok = true; + book.for_each_chapter_mut(|ch| { + ok &= !ch.content.contains("{{"); + ok &= !ch.content.contains("}}"); + }); + + if ok { + Ok(book) + } else { + Err(Error::msg("unredacted formatting marks")) + } + } +} + +fn manifest() -> Manifest { + manifest_at("Cargo.toml") +} + +fn manifest_at(manifest_path: &str) -> Manifest { + let path = WORKSPACE_ROOT.join(manifest_path); + let res = Manifest::from_path(&path).expect("invalid manifest"); + + if manifest_path == "Cargo.toml" { + assert_eq!( + res.package.as_ref().expect("missing package").name, + "vk-graph" + ); + } + + res +} + +fn insert_crate_version(book: &mut Book) { + let Version { major, minor, .. } = + Version::parse(manifest().package.expect("missing package").version()) + .expect("invalid version"); + let version = format!("{major}.{minor}"); + + const MARKER: &str = "{{ crate.version }}"; + + book.for_each_chapter_mut(|ch| { + if ch.content.contains(MARKER) { + ch.content = ch.content.replace(MARKER, &version); + } + }); +} + +fn insert_dependency_req(book: &mut Book, dep: &str) { + let req = manifest() + .dependencies + .get(dep) + .expect("missing dependency") + .req() + .to_owned(); + let marker = format!("{{{{ {dep}.version }}}}"); + + book.for_each_chapter_mut(|ch| { + if ch.content.contains(&marker) { + ch.content = ch.content.replace(&marker, &req); + } + }); +} + +fn insert_member_crate_version(book: &mut Book, dep: &str, manifest_path: &str) { + let Version { major, minor, .. } = Version::parse( + manifest_at(manifest_path) + .package + .as_ref() + .expect("missing package") + .version(), + ) + .expect("invalid version"); + let version = format!("{major}.{minor}"); + let marker = format!("{{{{ {dep}.version }}}}"); + + book.for_each_chapter_mut(|ch| { + if ch.content.contains(&marker) { + ch.content = ch.content.replace(&marker, &version); + } + }); +} + +fn insert_vulkan_sdk_version(book: &mut Book) { + // Technically this is a VersionReq but we're not using it that way! + let Version { major, minor, .. } = Version::parse( + manifest() + .dependencies + .get("ash") + .expect("missing dependency") + .req(), + ) + .expect("invalid version"); + + let vulkan_sdk_version = vulkan_sdk_version_for_ash(major, minor); + + const MARKER: &str = "{{ vulkan_sdk.version }}"; + + book.for_each_chapter_mut(|ch| { + if ch.content.contains(MARKER) { + ch.content = ch.content.replace(MARKER, vulkan_sdk_version); + } + }); +} + +fn hide_hidden_lines(book: &mut Book) { + book.for_each_chapter_mut(|ch| { + ch.content = hide_hidden_lines_in_markdown(&ch.content); + }); +} + +fn hide_hidden_lines_in_markdown(input: &str) -> String { + let mut out = String::with_capacity(input.len()); + let mut in_fence = false; + let mut hide_lines = false; + + for line in input.split_inclusive('\n') { + let trimmed = line.trim_start(); + + if let Some(fence) = trimmed.strip_prefix("```") { + if in_fence { + in_fence = false; + hide_lines = false; + } else { + in_fence = true; + hide_lines = should_hide_lines_in_fence(fence); + } + + out.push_str(line); + continue; + } + + if hide_lines && is_rustdoc_hidden_line(line) { + continue; + } + + out.push_str(line); + } + + out +} + +fn is_rustdoc_hidden_line(line: &str) -> bool { + let trimmed = line.trim_start(); + let Some(rest) = trimmed.strip_prefix('#') else { + return false; + }; + + matches!(rest.chars().next(), None | Some(' ' | '\t')) +} + +fn should_hide_lines_in_fence(info: &str) -> bool { + let info = info.trim(); + + if info.is_empty() { + return true; + } + + let lang = info.split(',').map(str::trim).next().unwrap_or_default(); + + !matches!(lang, "text" | "plain" | "plaintext" | "md" | "markdown") +} + +fn vulkan_sdk_version_for_ash(major: u64, minor: u64) -> &'static str { + match (major, minor) { + (0, 38) => LATEST_KNOWN_VULKAN_SDK_VERSION, + _ => panic!("unknown ash version; update Vulkan SDK guide mapping"), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn current_ash_version_maps_to_expected_sdk() { + assert_eq!( + vulkan_sdk_version_for_ash(0, 38), + VULKAN_SDK_VERSION_1_3_281 + ); + } + + #[test] + #[should_panic(expected = "unknown ash version; update Vulkan SDK guide mapping")] + fn unknown_ash_version_panics() { + let _ = vulkan_sdk_version_for_ash(0, 99); + } + + #[test] + fn hidden_lines_are_removed_from_code_fences() { + let input = concat!( + "```rust\n", + "# use vk_graph::Graph;\n", + "let graph = Graph::new();\n", + "# let _ = graph;\n", + "```\n\n", + "```text\n", + "# keep this line\n", + "```\n\n", + "```toml\n", + "# Cargo.toml\n", + "[dependencies]\n", + "```\n\n", + "```bash\n", + "# See: \"Shader Compilation\"\n", + "run-example\n", + "```\n", + ); + let output = hide_hidden_lines_in_markdown(input); + + assert_eq!( + output, + concat!( + "```rust\n", + "let graph = Graph::new();\n", + "```\n\n", + "```text\n", + "# keep this line\n", + "```\n\n", + "```toml\n", + "[dependencies]\n", + "```\n\n", + "```bash\n", + "run-example\n", + "```\n", + ) + ); + } +} diff --git a/guide/guide-helper/src/main.rs b/guide/guide-helper/src/main.rs new file mode 100644 index 00000000..52d02074 --- /dev/null +++ b/guide/guide-helper/src/main.rs @@ -0,0 +1,21 @@ +//! Preprocessor for the mdBook guide. + +fn main() { + let mut args = std::env::args().skip(1); + match args.next().as_deref() { + Some("supports") => { + // Supports all renderers. + return; + } + Some(arg) => { + eprintln!("unknown argument: {arg}"); + std::process::exit(1); + } + None => {} + } + + if let Err(e) = guide_helper::handle_preprocessing() { + eprintln!("{e:?}"); + std::process::exit(1); + } +} diff --git a/guide/src/README.md b/guide/src/README.md new file mode 100644 index 00000000..9577531c --- /dev/null +++ b/guide/src/README.md @@ -0,0 +1,59 @@ +# Introduction + +`vk-graph` is a high-performance Vulkan driver for the Rust programming language featuring automated +resource management and execution. It is _blazingly_-fast, built for real-world use, and supports +modern Vulkan commands[^modern]. + +This guide book will walk you through the mental model of this crate and help explain how it maps to +Vulkan API usage. + +> [!IMPORTANT] +> Users should be familiar with the Vulkan +[_specification_](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html) +. + +## Design + +This guide provides a tour of the main public types: + +Driver + : _Buffer, Image, Shader, etc.._ + +Graph + : _Builder-pattern for Vulkan commands_ + +Submission + : _Automated graph execution_ + +A `Graph` is built dynamically by your program each frame. Once complete, it is optimized into a +`Submission` that can be queued for execution on the Vulkan device. + +Building and submitting a graph typically takes only a few hundred microseconds. + +## Philosophy + +Vulkan is hard. Synchronization is _extremely_ hard. `vk-graph` makes Vulkan *less painful* to write +and *a joy* to maintain. + +The driver is based off the popular `ash` crate and `vk-sync`; reasoned as follows: +- _Everything_ is constructed from "`Info`" structs; all info is `Copy` +- Match the naming described in the specification +- Support all modern Vulkan usage[^modern] except video[^video] +- Don't use macro-magic or anything that needs to be learned +- Don't rely on "helper" functions unless absolutely required + +## History + +- 2018 --- Project started privately as a game engine using +[_`Corange`_](https://github.com/orangeduck/Corange) + +- 2020 --- Project migrated to Github and named `screen-13` +- 2022 --- v0.2 released with `RenderGraph` type based on +[_`Kajiya`_](https://github.com/EmbarkStudios/kajiya) + +- 2026 --- Project renamed `vk-graph` (v0.14) + +[^modern]: Modern Vulkan usage means no pixel queries. Anything else unsupported is due to there +being better options, no current need, or no interest. Please open an issue. +[^video]: Video encode/decode is interesting but unsupported. As an alternative consider `ffmpeg`, +`libavcodec`, or one of the experimental Rust bindings to the Vulkan video API. diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md new file mode 100644 index 00000000..a069a165 --- /dev/null +++ b/guide/src/SUMMARY.md @@ -0,0 +1,32 @@ +# Summary + +[Introduction](README.md) + +--- + +# User Guide + +1. [Installation](./install.md) +1. [Usage](./usage.md) + 1. [Device Creation](./usage_device.md) + 1. [Shader Compilation](./usage_shader.md) + 1. [Threading Behavior](./usage_thread.md) + 1. [Window Handling](./usage_window.md) + 1. [MoltenVK](./usage_moltenvk.md) + 1. [Debugging](./usage_debugging.md) + +# Reference Guide + +1. [Resources](./resource.md) + 1. [Buffers](./resource_buffer.md) + 1. [Images](./resource_image.md) + 1. [Acceleration Structures](./resource_accel_struct.md) +1. [Pipelines](./pipeline.md) + 1. [Hot Reload](./pipeline_hot_reload.md) + 1. [Push Constants](./pipeline_push_const.md) + 1. [Specialization](./pipeline_spec.md) + 1. [Synchronization](./pipeline_sync.md) +1. [Commands](./cmd.md) + 1. [Computing](./cmd_compute.md) + 1. [Graphics](./cmd_graphic.md) + 1. [Ray Tracing](./cmd_ray_trace.md) diff --git a/guide/src/cmd.md b/guide/src/cmd.md new file mode 100644 index 00000000..1b1a9d9d --- /dev/null +++ b/guide/src/cmd.md @@ -0,0 +1,141 @@ +# Commands + +`vk-graph` exposes two styles of commands: + +API docs: [`Graph::begin_cmd`](https://docs.rs/vk-graph/latest/vk_graph/struct.Graph.html#method.begin_cmd), +[`Command::record_cmd`](https://docs.rs/vk-graph/latest/vk_graph/cmd/struct.Command.html#method.record_cmd), +[`Graph::into_submission`](https://docs.rs/vk-graph/latest/vk_graph/struct.Graph.html#method.into_submission). + +- Built-in graph commands such as `copy_buffer`, `clear_color_image`, and `update_buffer` +- Explicit command-buffer recording through `begin_cmd().record_cmd(...)` + +The built-in commands are the easiest place to start. They automatically describe the required +transfer access and insert the synchronization they need. + +## Built-In Commands + +These helpers cover common transfer-style work: + +Command | Typical use +-|- +`blit_image` | Scale or format-convert one image into another +`clear_color_image` | Clear a color render target, staging image, or scratch image +`clear_depth_stencil_image` | Initialize or reset a depth/stencil image +`copy_buffer` | Copy data between buffers +`copy_buffer_to_image` | Upload staging-buffer contents into an image +`copy_image` | Copy texels between images without filtering +`copy_image_to_buffer` | Read back image data into a buffer +`fill_buffer` | Fill a buffer region with a repeated `u32` value +`update_buffer` | Upload up to 64 KiB of inline data directly into a buffer + +## Typical Flow + +The most common pattern is to stage data in a buffer, upload it into an image, and then clear or +copy other resources as part of the same graph: + +```no_run +# use vk_graph::Graph; +# use vk_graph::driver::DriverError; +# use vk_graph::driver::ash::vk; +# use vk_graph::driver::buffer::{Buffer, BufferInfo}; +# use vk_graph::driver::device::{Device, DeviceInfo}; +# use vk_graph::driver::image::{Image, ImageInfo}; +# fn main() -> Result<(), DriverError> { +# let device = Device::create(DeviceInfo::default())?; +let mut graph = Graph::default(); + +let staging = Buffer::create( + &device, + BufferInfo::host_mem( + 256 * 256 * 4, + vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST, + ), +)?; +let upload_image = Image::create( + &device, + ImageInfo::image_2d( + 256, + 256, + vk::Format::R8G8B8A8_UNORM, + vk::ImageUsageFlags::TRANSFER_DST + | vk::ImageUsageFlags::TRANSFER_SRC + | vk::ImageUsageFlags::SAMPLED, + ), +)?; +let mip_preview = Image::create( + &device, + ImageInfo::image_2d( + 128, + 128, + vk::Format::R8G8B8A8_UNORM, + vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED, + ), +)?; +let readback = Buffer::create( + &device, + BufferInfo::host_mem( + 128 * 128 * 4, + vk::BufferUsageFlags::TRANSFER_DST, + ), +)?; + +let staging = graph.bind_resource(staging); +let upload_image = graph.bind_resource(upload_image); +let mip_preview = graph.bind_resource(mip_preview); +let readback = graph.bind_resource(readback); + +graph + .update_buffer(staging, 0, [0xff; 64]) + .copy_buffer_to_image(staging, upload_image) + .blit_image(upload_image, mip_preview, vk::Filter::LINEAR) + .clear_color_image(mip_preview, [0.1, 0.2, 0.3, 1.0]) + .copy_image_to_buffer(mip_preview, readback) + .fill_buffer(readback, 0..64, 0); +# Ok(()) } +``` + +## Choosing The Right Command + +- Use `update_buffer` for small inline uploads that fit in Vulkan's `cmd_update_buffer` limits. +- Use `fill_buffer` when you need a repeated `u32` pattern, often for resets or counters. +- Use `copy_buffer_to_image` and `copy_image_to_buffer` for upload and readback paths. +- Use `copy_image` when source and destination texel footprints already match. +- Use `blit_image` when you need scaling or filtering. +- Use the `*_region` variants when you need precise offsets, layers, mip levels, or partial copies. + +## Region Variants + +Each built-in helper also has a more explicit form such as `copy_buffer_region` or +`copy_buffer_to_image_region`. Use those variants when the whole-resource convenience behavior is +too broad. + +```no_run +# use vk_graph::Graph; +# use vk_graph::driver::DriverError; +# use vk_graph::driver::ash::vk; +# use vk_graph::driver::buffer::{Buffer, BufferInfo}; +# use vk_graph::driver::device::{Device, DeviceInfo}; +# fn main() -> Result<(), DriverError> { +# let device = Device::create(DeviceInfo::default())?; +let mut graph = Graph::default(); + +let src = graph.bind_resource(Buffer::create( + &device, + BufferInfo::host_mem(4096, vk::BufferUsageFlags::TRANSFER_SRC), +)?); +let dst = graph.bind_resource(Buffer::create( + &device, + BufferInfo::device_mem(4096, vk::BufferUsageFlags::TRANSFER_DST), +)?); + +graph.copy_buffer_region( + src, + dst, + [vk::BufferCopy { + src_offset: 512, + dst_offset: 1024, + size: 256, + }], +); +# Ok(()) } +``` diff --git a/guide/src/cmd_compute.md b/guide/src/cmd_compute.md new file mode 100644 index 00000000..e8130bdb --- /dev/null +++ b/guide/src/cmd_compute.md @@ -0,0 +1,177 @@ +# Computing + +Compute commands are recorded after binding a `ComputePipeline`. They are typically paired with +`shader_resource_access` for storage buffers or images, and `resource_access` for indirect argument +buffers. + +API docs: [`ComputeCommandRef::dispatch`](https://docs.rs/vk-graph/latest/vk_graph/cmd/compute/struct.ComputeCommandRef.html#method.dispatch), +[`ComputeCommandRef::dispatch_base`](https://docs.rs/vk-graph/latest/vk_graph/cmd/compute/struct.ComputeCommandRef.html#method.dispatch_base), +[`ComputeCommandRef::dispatch_indirect`](https://docs.rs/vk-graph/latest/vk_graph/cmd/compute/struct.ComputeCommandRef.html#method.dispatch_indirect), +[`ComputeCommandRef::push_constants`](https://docs.rs/vk-graph/latest/vk_graph/cmd/compute/struct.ComputeCommandRef.html#method.push_constants). + +## Available Commands + +Command | Typical use +-|- +`dispatch` | Launch workgroups directly from CPU-provided dimensions +`dispatch_base` | Launch workgroups with a non-zero base workgroup ID +`dispatch_indirect` | Read dispatch dimensions from a buffer on the device +`push_constants` | Update small pipeline constants without a buffer upload + +## Direct Dispatch + +`dispatch` is the default option. Use it when the CPU already knows the workgroup count. + +```no_run +# use vk_graph::Graph; +# use vk_graph::driver::ash::vk; +# use vk_graph::driver::{DriverError, sync::AccessType}; +# use vk_graph::driver::buffer::{Buffer, BufferInfo}; +# use vk_graph::driver::compute::{ComputePipeline, ComputePipelineInfo}; +# use vk_graph::driver::device::{Device, DeviceInfo}; +# use vk_graph::driver::shader::Shader; +# fn main() -> Result<(), DriverError> { +# let device = Device::create(DeviceInfo::default())?; +let mut graph = Graph::default(); + +let output = graph.bind_resource(Buffer::create( + &device, + BufferInfo::device_mem( + 4096, + vk::BufferUsageFlags::STORAGE_BUFFER, + ), +)?); + +let pipeline = ComputePipeline::create( + &device, + ComputePipelineInfo::default(), + Shader::new_compute([0u8; 4].as_slice()), +)?; + +graph + .begin_cmd() + .debug_name("prefix sum") + .bind_pipeline(&pipeline) + .shader_resource_access(0, output, AccessType::ComputeShaderWrite) + .record_cmd(move |cmd| { + cmd.dispatch(64, 1, 1); + }); +# Ok(()) } +``` + +## Offset Dispatches + +`dispatch_base` is useful when a pipeline processes a tiled domain and each invocation needs a +non-zero `gl_WorkGroupID` origin. + +```no_run +# use vk_graph::Graph; +# use vk_graph::driver::ash::vk; +# use vk_graph::driver::{DriverError, sync::AccessType}; +# use vk_graph::driver::buffer::{Buffer, BufferInfo}; +# use vk_graph::driver::compute::{ComputePipeline, ComputePipelineInfo}; +# use vk_graph::driver::device::{Device, DeviceInfo}; +# use vk_graph::driver::shader::Shader; +# fn main() -> Result<(), DriverError> { +# let device = Device::create(DeviceInfo::default())?; +let mut graph = Graph::default(); + +let output = graph.bind_resource(Buffer::create( + &device, + BufferInfo::device_mem(4096, vk::BufferUsageFlags::STORAGE_BUFFER), +)?); +let pipeline = ComputePipeline::create( + &device, + ComputePipelineInfo::default(), + Shader::new_compute([0u8; 4].as_slice()), +)?; + +graph + .begin_cmd() + .bind_pipeline(&pipeline) + .shader_resource_access(0, output, AccessType::ComputeShaderWrite) + .record_cmd(move |cmd| { + cmd.dispatch_base(4, 2, 0, 16, 8, 1); + }); +# Ok(()) } +``` + +## GPU-Driven Dispatch + +`dispatch_indirect` lets an earlier pass write the group counts into a buffer. The compute pass +then consumes those parameters without CPU intervention. + +```no_run +# use vk_graph::Graph; +# use vk_graph::driver::ash::vk; +# use vk_graph::driver::{DriverError, sync::AccessType}; +# use vk_graph::driver::buffer::{Buffer, BufferInfo}; +# use vk_graph::driver::compute::{ComputePipeline, ComputePipelineInfo}; +# use vk_graph::driver::device::{Device, DeviceInfo}; +# use vk_graph::driver::shader::Shader; +# fn main() -> Result<(), DriverError> { +# let device = Device::create(DeviceInfo::default())?; +let mut graph = Graph::default(); +let output = graph.bind_resource(Buffer::create( + &device, + BufferInfo::device_mem(4096, vk::BufferUsageFlags::STORAGE_BUFFER), +)?); +let args = vk::DispatchIndirectCommand { x: 32, y: 8, z: 1 }; +let args_buffer = graph.bind_resource(Buffer::create_from_slice( + &device, + vk::BufferUsageFlags::INDIRECT_BUFFER | vk::BufferUsageFlags::TRANSFER_DST, + bytemuck::cast_slice::(&[args.x, args.y, args.z]), +)?); +let pipeline = ComputePipeline::create( + &device, + ComputePipelineInfo::default(), + Shader::new_compute([0u8; 4].as_slice()), +)?; + +graph + .begin_cmd() + .bind_pipeline(&pipeline) + .resource_access(args_buffer, AccessType::IndirectBuffer) + .shader_resource_access(0, output, AccessType::ComputeShaderWrite) + .record_cmd(move |cmd| { + cmd.dispatch_indirect(args_buffer, 0); + }); +# Ok(()) } +``` + +## Push Constants + +Use [`ComputeCommandRef::push_constants`](https://docs.rs/vk-graph/latest/vk_graph/cmd/compute/struct.ComputeCommandRef.html#method.push_constants) +for small values that change often, such as frame indices, dispatch parameters, +or other compact state. + +```no_run +# use vk_graph::Graph; +# use vk_graph::driver::DriverError; +# use vk_graph::driver::device::{Device, DeviceInfo}; +# use vk_graph::driver::compute::{ComputePipeline, ComputePipelineInfo}; +# use vk_graph::driver::shader::Shader; +# fn main() -> Result<(), DriverError> { +# let device = Device::create(DeviceInfo::default())?; +# let pipeline = ComputePipeline::create( +# &device, +# ComputePipelineInfo::default(), +# Shader::new_compute([0u8; 4].as_slice()), +# )?; +# let mut graph = Graph::default(); +graph + .begin_cmd() + .bind_pipeline(&pipeline) + .record_cmd(move |cmd| { + cmd.push_constants(0, &[42]) + .dispatch(1, 1, 1); + }); +# Ok(()) } +``` + +## Notes + +- `dispatch` and `dispatch_base` are the simplest and cheapest commands to drive from CPU code. +- `dispatch_indirect` is the usual choice for GPU-generated work queues or culling results. +- The bound pipeline and declared resource access determine the synchronization requirements around + the dispatch. diff --git a/guide/src/cmd_graphic.md b/guide/src/cmd_graphic.md new file mode 100644 index 00000000..80c74ec1 --- /dev/null +++ b/guide/src/cmd_graphic.md @@ -0,0 +1,306 @@ +# Graphics + +Graphic commands are recorded after binding a `GraphicPipeline` and declaring attachments such as +`color_attachment_image` or `depth_stencil_attachment_image`. + +API docs: [`GraphicCommandRef::draw`](https://docs.rs/vk-graph/latest/vk_graph/cmd/graphic/struct.GraphicCommandRef.html#method.draw), +[`GraphicCommandRef::draw_indexed`](https://docs.rs/vk-graph/latest/vk_graph/cmd/graphic/struct.GraphicCommandRef.html#method.draw_indexed), +[`GraphicCommandRef::draw_indirect`](https://docs.rs/vk-graph/latest/vk_graph/cmd/graphic/struct.GraphicCommandRef.html#method.draw_indirect), +[`GraphicCommandRef::push_constants`](https://docs.rs/vk-graph/latest/vk_graph/cmd/graphic/struct.GraphicCommandRef.html#method.push_constants). + +## Available Commands + +Command | Typical use +-|- +`bind_index_buffer` | Provide indices for indexed drawing +`bind_vertex_buffers` | Bind one or more vertex streams +`draw` | Draw non-indexed geometry +`draw_indexed` | Draw indexed geometry +`draw_indexed_indirect` | Read indexed draw parameters from a buffer +`draw_indexed_indirect_count` | GPU-driven indexed draws with a count buffer +`draw_indirect` | Read non-indexed draw parameters from a buffer +`draw_indirect_count` | GPU-driven non-indexed draws with a count buffer +`set_scissor` | Restrict drawing to one or more rectangles +`set_viewport` | Override the default viewport dynamically +`push_constants` | Update small pipeline constants without a buffer upload + +## Direct Draws + +The most common pattern is to bind vertex and index buffers, then issue `draw` or `draw_indexed`. + +```no_run +# use vk_graph::Graph; +# use vk_graph::cmd::{LoadOp, StoreOp}; +# use vk_graph::driver::ash::vk; +# use vk_graph::cmd::ClearColorValue; +# use vk_graph::driver::{DriverError, sync::AccessType}; +# use vk_graph::driver::buffer::{Buffer, BufferInfo}; +# use vk_graph::driver::device::{Device, DeviceInfo}; +# use vk_graph::driver::graphic::{GraphicPipeline, GraphicPipelineInfo}; +# use vk_graph::driver::image::{Image, ImageInfo}; +# use vk_graph::driver::shader::Shader; +# fn main() -> Result<(), DriverError> { +# let device = Device::create(DeviceInfo::default())?; +let mut graph = Graph::default(); + +let color = graph.bind_resource(Image::create( + &device, + ImageInfo::image_2d( + 1280, + 720, + vk::Format::R8G8B8A8_UNORM, + vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::TRANSFER_SRC, + ), +)?); +let vertices = graph.bind_resource(Buffer::create( + &device, + BufferInfo::device_mem(4096, vk::BufferUsageFlags::VERTEX_BUFFER), +)?); +let indices = graph.bind_resource(Buffer::create( + &device, + BufferInfo::device_mem(1024, vk::BufferUsageFlags::INDEX_BUFFER), +)?); + +let pipeline = GraphicPipeline::create( + &device, + GraphicPipelineInfo::default(), + [ + Shader::new_vertex([0u8; 4].as_slice()), + Shader::new_fragment([0u8; 4].as_slice()), + ], +)?; + +graph + .begin_cmd() + .debug_name("main geometry pass") + .bind_pipeline(&pipeline) + .color_attachment_image( + 0, + color, + LoadOp::Clear(ClearColorValue::Float32([0.0, 0.0, 0.0, 1.0])), + StoreOp::Store, + ) + .resource_access(vertices, AccessType::VertexBuffer) + .resource_access(indices, AccessType::IndexBuffer) + .record_cmd(move |cmd| { + cmd + .bind_vertex_buffers(0, [(vertices, 0)]) + .bind_index_buffer(indices, 0, vk::IndexType::UINT32) + .draw_indexed(36, 1, 0, 0, 0); + }); +# Ok(()) } +``` + +## Dynamic Viewports And Scissors + +The default viewport covers the full attachment extent and the default scissor does not clip. +Override them when a pass renders only part of the target. + +```no_run +# use vk_graph::Graph; +# use vk_graph::cmd::{LoadOp, StoreOp}; +# use vk_graph::driver::ash::vk; +# use vk_graph::driver::{DriverError, sync::AccessType}; +# use vk_graph::driver::buffer::{Buffer, BufferInfo}; +# use vk_graph::driver::device::{Device, DeviceInfo}; +# use vk_graph::driver::graphic::{GraphicPipeline, GraphicPipelineInfo}; +# use vk_graph::driver::image::{Image, ImageInfo}; +# use vk_graph::driver::shader::Shader; +# fn main() -> Result<(), DriverError> { +# let device = Device::create(DeviceInfo::default())?; +let mut graph = Graph::default(); +let color = graph.bind_resource(Image::create( + &device, + ImageInfo::image_2d( + 1280, + 720, + vk::Format::R8G8B8A8_UNORM, + vk::ImageUsageFlags::COLOR_ATTACHMENT, + ), +)?); +let vertices = graph.bind_resource(Buffer::create( + &device, + BufferInfo::device_mem(4096, vk::BufferUsageFlags::VERTEX_BUFFER), +)?); +let pipeline = GraphicPipeline::create( + &device, + GraphicPipelineInfo::default(), + [ + Shader::new_vertex([0u8; 4].as_slice()), + Shader::new_fragment([0u8; 4].as_slice()), + ], +)?; + +graph + .begin_cmd() + .bind_pipeline(&pipeline) + .color_attachment_image(0, color, LoadOp::DontCare, StoreOp::Store) + .resource_access(vertices, AccessType::VertexBuffer) + .record_cmd(move |cmd| { + cmd + .set_viewport( + 0, + &[vk::Viewport { + x: 0.0, + y: 0.0, + width: 640.0, + height: 360.0, + min_depth: 0.0, + max_depth: 1.0, + }], + ) + .set_scissor( + 0, + &[vk::Rect2D { + offset: vk::Offset2D { x: 0, y: 0 }, + extent: vk::Extent2D { width: 640, height: 360 }, + }], + ) + .bind_vertex_buffers(0, [(vertices, 0)]) + .draw(3, 1, 0, 0); + }); +# Ok(()) } +``` + +## Indirect Draws + +Indirect drawing is the usual next step once culling, LOD selection, or instance generation moves +onto the GPU. + +```no_run +# use std::mem::size_of; +# use vk_graph::Graph; +# use vk_graph::cmd::{LoadOp, StoreOp}; +# use vk_graph::driver::ash::vk; +# use vk_graph::driver::{DriverError, sync::AccessType}; +# use vk_graph::driver::buffer::{Buffer, BufferInfo}; +# use vk_graph::driver::device::{Device, DeviceInfo}; +# use vk_graph::driver::graphic::{GraphicPipeline, GraphicPipelineInfo}; +# use vk_graph::driver::image::{Image, ImageInfo}; +# use vk_graph::driver::shader::Shader; +# fn main() -> Result<(), DriverError> { +# let device = Device::create(DeviceInfo::default())?; +let mut graph = Graph::default(); +let color = graph.bind_resource(Image::create( + &device, + ImageInfo::image_2d( + 1280, + 720, + vk::Format::R8G8B8A8_UNORM, + vk::ImageUsageFlags::COLOR_ATTACHMENT, + ), +)?); +let vertices = graph.bind_resource(Buffer::create( + &device, + BufferInfo::device_mem(4096, vk::BufferUsageFlags::VERTEX_BUFFER), +)?); +let indices = graph.bind_resource(Buffer::create( + &device, + BufferInfo::device_mem(1024, vk::BufferUsageFlags::INDEX_BUFFER), +)?); +let draw_command = vk::DrawIndexedIndirectCommand { + index_count: 36, + instance_count: 1, + first_index: 0, + vertex_offset: 0, + first_instance: 0, +}; +let draw_args = graph.bind_resource(Buffer::create_from_slice( + &device, + vk::BufferUsageFlags::INDIRECT_BUFFER, + bytemuck::cast_slice(&[ + draw_command.index_count as i32, + draw_command.instance_count as i32, + draw_command.first_index as i32, + draw_command.vertex_offset, + draw_command.first_instance as i32, + ]), +)?); +let draw_count = graph.bind_resource(Buffer::create_from_slice( + &device, + vk::BufferUsageFlags::INDIRECT_BUFFER, + &1u32.to_ne_bytes(), +)?); +let pipeline = GraphicPipeline::create( + &device, + GraphicPipelineInfo::default(), + [ + Shader::new_vertex([0u8; 4].as_slice()), + Shader::new_fragment([0u8; 4].as_slice()), + ], +)?; + +graph + .begin_cmd() + .bind_pipeline(&pipeline) + .color_attachment_image(0, color, LoadOp::DontCare, StoreOp::Store) + .resource_access(vertices, AccessType::VertexBuffer) + .resource_access(indices, AccessType::IndexBuffer) + .resource_access(draw_args, AccessType::IndirectBuffer) + .resource_access(draw_count, AccessType::IndirectBuffer) + .record_cmd(move |cmd| { + cmd + .bind_vertex_buffers(0, [(vertices, 0)]) + .bind_index_buffer(indices, 0, vk::IndexType::UINT32) + .draw_indexed_indirect_count( + draw_args, + 0, + draw_count, + 0, + 16, + size_of::() as u32, + ); + }); +# Ok(()) } +``` + +## Push Constants + +Use [`GraphicCommandRef::push_constants`](https://docs.rs/vk-graph/latest/vk_graph/cmd/graphic/struct.GraphicCommandRef.html#method.push_constants) +for compact per-draw state that fits within the device's push-constant limit. + +```no_run +# use vk_graph::cmd::{LoadOp, StoreOp}; +# use vk_graph::driver::{ash::vk, DriverError}; +# use vk_graph::driver::device::{Device, DeviceInfo}; +# use vk_graph::driver::graphic::{GraphicPipeline, GraphicPipelineInfo}; +# use vk_graph::driver::image::{Image, ImageInfo}; +# use vk_graph::driver::shader::Shader; +# use vk_graph::Graph; +# fn main() -> Result<(), DriverError> { +# let device = Device::create(DeviceInfo::default())?; +# let pipeline = GraphicPipeline::create( +# &device, +# GraphicPipelineInfo::default(), +# [ +# Shader::new_vertex([0u8; 4].as_slice()), +# Shader::new_fragment([0u8; 4].as_slice()), +# ], +# )?; +# let image = Image::create( +# &device, +# ImageInfo::image_2d( +# 32, +# 32, +# vk::Format::R8G8B8A8_UNORM, +# vk::ImageUsageFlags::COLOR_ATTACHMENT, +# ), +# )?; +# let mut graph = Graph::default(); +# let image = graph.bind_resource(image); +graph + .begin_cmd() + .bind_pipeline(&pipeline) + .color_attachment_image(0, image, LoadOp::DontCare, StoreOp::Store) + .record_cmd(move |cmd| { + cmd.push_constants(0, &[42]) + .draw(3, 1, 0, 0); + }); +# Ok(()) } +``` + +## Notes + +- `draw` and `draw_indexed` are the best fit for CPU-driven rendering. +- `draw_indirect` and `draw_indexed_indirect` move only the parameters onto the GPU. +- `draw_*_indirect_count` is the usual choice for fully GPU-driven visibility results. diff --git a/guide/src/cmd_ray_trace.md b/guide/src/cmd_ray_trace.md new file mode 100644 index 00000000..bb909f62 --- /dev/null +++ b/guide/src/cmd_ray_trace.md @@ -0,0 +1,256 @@ +# Ray Tracing + +Ray tracing work in `vk-graph` usually has two phases: + +- Build or update acceleration structures with a general command buffer +- Bind a `RayTracePipeline` and issue `trace_rays` or `trace_rays_indirect` + +API docs: [`RayTraceCommandRef::build_accel_struct`](https://docs.rs/vk-graph/latest/vk_graph/cmd/ray_trace/struct.RayTraceCommandRef.html#method.build_accel_struct), +[`RayTraceCommandRef::trace_rays`](https://docs.rs/vk-graph/latest/vk_graph/cmd/ray_trace/struct.RayTraceCommandRef.html#method.trace_rays), +[`RayTraceCommandRef::trace_rays_indirect`](https://docs.rs/vk-graph/latest/vk_graph/cmd/ray_trace/struct.RayTraceCommandRef.html#method.trace_rays_indirect), +[`RayTraceCommandRef::push_constants`](https://docs.rs/vk-graph/latest/vk_graph/cmd/ray_trace/struct.RayTraceCommandRef.html#method.push_constants). + +## Available Commands + +Command | Typical use +-|- +`build_accel_struct` | Build BLAS or TLAS from CPU-provided build ranges +`build_accel_struct_indirect` | Build acceleration structures using device-provided ranges +`set_stack_size` | Override stack size when the pipeline enables dynamic stack sizing +`trace_rays` | Launch rays with CPU-provided dimensions +`trace_rays_indirect` | Launch rays with dimensions read from device memory +`update_accel_struct` | Refit or rebuild an existing structure in-place +`update_accel_struct_indirect` | Device-driven in-place update path +`push_constants` | Update small pipeline constants without a buffer upload + +## Building Acceleration Structures + +Acceleration-structure builds are recorded on a plain `CommandBuffer`, not a pipeline-specific +command buffer. + +```no_run +# use vk_graph::Graph; +# use vk_graph::cmd::BuildAccelerationStructureInfo; +# use vk_graph::driver::ash::vk; +# use vk_graph::driver::{DriverError, sync::AccessType}; +# use vk_graph::driver::accel_struct::{AccelerationStructure, AccelerationStructureGeometry, AccelerationStructureGeometryInfo, AccelerationStructureInfo}; +# use vk_graph::driver::buffer::{Buffer, BufferInfo}; +# use vk_graph::driver::device::{Device, DeviceInfo}; +# fn main() -> Result<(), DriverError> { +# let device = Device::create(DeviceInfo::default())?; +let mut graph = Graph::default(); + +let blas = graph.bind_resource(AccelerationStructure::create( + &device, + AccelerationStructureInfo::blas(1), +)?); +let scratch = graph.bind_resource(Buffer::create( + &device, + BufferInfo::device_mem( + 4096, + vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS, + ), +)?); + +graph + .begin_cmd() + .resource_access(scratch, AccessType::AccelerationStructureBufferWrite) + .resource_access(blas, AccessType::AccelerationStructureBuildWrite) + .record_cmd(move |cmd| { + let scratch_addr = cmd.resource(scratch).device_address(); + let build_info: AccelerationStructureGeometryInfo<( + AccelerationStructureGeometry, + vk::AccelerationStructureBuildRangeInfoKHR, + )> = todo!("geometry setup"); + + cmd.build_accel_struct(&[ + BuildAccelerationStructureInfo::new(blas, scratch_addr, build_info), + ]); + }); +# Ok(()) } +``` + +The indirect form is the same idea, but the range data lives on the device. That is useful when a +previous GPU pass writes primitive counts or build ranges. + +## Tracing Rays + +Once the acceleration structures and shader binding table are ready, bind a `RayTracePipeline` and +issue `trace_rays`. + +```no_run +# use vk_graph::Graph; +# use vk_graph::driver::ash::vk; +# use vk_graph::driver::{DriverError, sync::AccessType}; +# use vk_graph::driver::device::{Device, DeviceInfo}; +# use vk_graph::driver::image::{Image, ImageInfo}; +# use vk_graph::driver::ray_trace::{RayTracePipeline, RayTracePipelineInfo, RayTraceShaderGroup}; +# use vk_graph::driver::shader::Shader; +# fn main() -> Result<(), DriverError> { +# let device = Device::create(DeviceInfo::default())?; +let mut graph = Graph::default(); +let output = graph.bind_resource(Image::create( + &device, + ImageInfo::image_2d( + 1280, + 720, + vk::Format::R16G16B16A16_SFLOAT, + vk::ImageUsageFlags::STORAGE | vk::ImageUsageFlags::TRANSFER_SRC, + ), +)?); + +let pipeline = RayTracePipeline::create( + &device, + RayTracePipelineInfo::default(), + [ + Shader::new_ray_gen([0u8; 4].as_slice()), + Shader::new_miss([0u8; 4].as_slice()), + ], + [ + RayTraceShaderGroup::new_general(0), + RayTraceShaderGroup::new_general(1), + ], +)?; + +let raygen_sbt: vk::StridedDeviceAddressRegionKHR = todo!("raygen shader binding table"); +let miss_sbt: vk::StridedDeviceAddressRegionKHR = todo!("miss shader binding table"); +let hit_sbt = vk::StridedDeviceAddressRegionKHR::default(); +let callable_sbt = vk::StridedDeviceAddressRegionKHR::default(); + +graph + .begin_cmd() + .bind_pipeline(&pipeline) + .shader_resource_access(0, output, AccessType::General) + .record_cmd(move |cmd| { + cmd.trace_rays(&raygen_sbt, &miss_sbt, &hit_sbt, &callable_sbt, 1280, 720, 1); + }); +# Ok(()) } +``` + +## Push Constants + +Use [`RayTraceCommandRef::push_constants`](https://docs.rs/vk-graph/latest/vk_graph/cmd/ray_trace/struct.RayTraceCommandRef.html#method.push_constants) +for small ray tracing state such as frame counters or camera parameters. + +```no_run +# use vk_graph::driver::{ash::vk, DriverError}; +# use vk_graph::driver::device::{Device, DeviceInfo}; +# use vk_graph::driver::image::{Image, ImageInfo}; +# use vk_graph::driver::ray_trace::{RayTracePipeline, RayTracePipelineInfo, RayTraceShaderGroup}; +# use vk_graph::driver::shader::Shader; +# use vk_graph::Graph; +# fn main() -> Result<(), DriverError> { +# let device = Device::create(DeviceInfo::default())?; +# let pipeline = RayTracePipeline::create( +# &device, +# RayTracePipelineInfo::default(), +# [Shader::new_ray_gen([0u8; 4].as_slice())], +# [RayTraceShaderGroup::new_general(0)], +# )?; +# let output = Image::create( +# &device, +# ImageInfo::image_2d( +# 1280, +# 720, +# vk::Format::R16G16B16A16_SFLOAT, +# vk::ImageUsageFlags::STORAGE, +# ), +# )?; +# let mut graph = Graph::default(); +# let output = graph.bind_resource(output); +graph + .begin_cmd() + .bind_pipeline(&pipeline) + .record_cmd(move |cmd| { + cmd.push_constants(0, &[42]) + .trace_rays( + &vk::StridedDeviceAddressRegionKHR::default(), + &vk::StridedDeviceAddressRegionKHR::default(), + &vk::StridedDeviceAddressRegionKHR::default(), + &vk::StridedDeviceAddressRegionKHR::default(), + 1280, + 720, + 1, + ); + }); +# Ok(()) } +``` + +## Dynamic Stack Size And Indirect Trace + +Use `set_stack_size` only when the pipeline was created with `dynamic_stack_size(true)`. Combine it +with `trace_rays_indirect` when another pass writes the trace dimensions into a device-addressable +buffer. + +```no_run +# use vk_graph::Graph; +# use vk_graph::driver::ash::vk; +# use vk_graph::driver::{DriverError, sync::AccessType}; +# use vk_graph::driver::buffer::{Buffer, BufferInfo}; +# use vk_graph::driver::device::{Device, DeviceInfo}; +# use vk_graph::driver::image::{Image, ImageInfo}; +# use vk_graph::driver::ray_trace::{RayTracePipeline, RayTracePipelineInfo, RayTraceShaderGroup}; +# use vk_graph::driver::shader::Shader; +# fn main() -> Result<(), DriverError> { +# let device = Device::create(DeviceInfo::default())?; +let mut graph = Graph::default(); +let output = graph.bind_resource(Image::create( + &device, + ImageInfo::image_2d( + 1280, + 720, + vk::Format::R16G16B16A16_SFLOAT, + vk::ImageUsageFlags::STORAGE, + ), +)?); +let args = graph.bind_resource(Buffer::create( + &device, + BufferInfo::device_mem( + std::mem::size_of::() as u64, + vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS, + ), +)?); +let pipeline = RayTracePipeline::create( + &device, + RayTracePipelineInfo::builder().dynamic_stack_size(true), + [ + Shader::new_ray_gen([0u8; 4].as_slice()), + Shader::new_miss([0u8; 4].as_slice()), + ], + [ + RayTraceShaderGroup::new_general(0), + RayTraceShaderGroup::new_general(1), + ], +)?; + +let raygen_sbt: vk::StridedDeviceAddressRegionKHR = todo!("raygen shader binding table"); +let miss_sbt: vk::StridedDeviceAddressRegionKHR = todo!("miss shader binding table"); +let hit_sbt = vk::StridedDeviceAddressRegionKHR::default(); +let callable_sbt = vk::StridedDeviceAddressRegionKHR::default(); + +graph + .begin_cmd() + .bind_pipeline(&pipeline) + .resource_access(args, AccessType::IndirectBuffer) + .shader_resource_access(0, output, AccessType::General) + .record_cmd(move |cmd| { + cmd + .set_stack_size(4096) + .trace_rays_indirect( + &raygen_sbt, + &miss_sbt, + &hit_sbt, + &callable_sbt, + cmd.resource(args).device_address(), + ); + }); +# Ok(()) } +``` + +## Notes + +- Build/update commands and trace commands are separate because they have different setup needs. +- `trace_rays` is the easiest path when the CPU already knows the launch dimensions. +- `trace_rays_indirect` is the better fit when a GPU pass writes the ray count or image extent. +- `update_accel_struct` and `update_accel_struct_indirect` are for refit-style workloads where the + topology is stable but transforms or vertex positions change. diff --git a/guide/src/install.md b/guide/src/install.md new file mode 100644 index 00000000..990d658f --- /dev/null +++ b/guide/src/install.md @@ -0,0 +1,50 @@ +# Installation + +To get started with `vk-graph`, add it as a project dependency to your `Cargo.toml`: + +```toml +# Cargo.toml + +[dependencies] +vk-graph = "{{ crate.version }}" +``` + +## Features + +_vk-graph_ puts a lot of functionality behind optional features in order to optimize +compile time for the most common use cases. The following features are +available. + +- **`loaded`** *(enabled by default)* — Support searching for the Vulkan loader manually at runtime. +- **`linked`** — Link the Vulkan loader at compile time. +- **`profile-with-*`** — Use the specified profiling backend: + `profile-with-puffin`, `profile-with-optick`, `profile-with-superluminal`, or + `profile-with-tracy` + +## Required Development Packages + +_Linux (Debian-like)_: +- `sudo apt install cmake uuid-dev libfontconfig-dev libssl-dev` + +_Mac OS (10.15 or later)_: +- Xcode 12 +- Python 2.7 +- `brew install cmake ossp-uuid` + +_Windows_: +- Install the Vulkan SDK and the current Visual Studio C++ build tools. + +## Vulkan SDK + +Debug mode (setting the `debug` field of `DeviceInfo` or `InstanceInfo` to `true`) is only supported +when certain validation layers are installed. The [_Vulkan SDK_](https://vulkan.lunarg.com/sdk/home) + provides these layers and a number of helpful +tools. + +> [!IMPORTANT] +> The installed Vulkan SDK version must be at least v{{ vulkan_sdk.version }}. + +### Optional Distribution-Provided Validation Layers + +_Linux (Debian-like)_: +- `sudo apt install vulkan-validationlayers` diff --git a/guide/src/pipeline.md b/guide/src/pipeline.md new file mode 100644 index 00000000..abf985e1 --- /dev/null +++ b/guide/src/pipeline.md @@ -0,0 +1,113 @@ +# Pipelines + +> [!CAUTION] +> All pipelines and resources (_buffers, images, and acceleration structures_) used in a `Graph` +> must have been created using the same `Device`. + +Pipelines are created from `Device` references. They may be bound to graph commands. + +```rust +# macro_rules! include_bytes { ($path:expr) => { [0u8] }; } +# use vk_graph::Graph; +# use vk_graph::driver::{DriverError, device::Device}; +# use vk_graph::driver::compute::{ComputePipeline, ComputePipelineInfo}; +# fn test(device: &Device) -> Result<(), DriverError> { +let info = ComputePipelineInfo::default(); +let shader = include_bytes!("shader.spv"); +let pipeline = ComputePipeline::create(device, info, shader.as_slice())?; + +let mut graph = Graph::default() + .begin_cmd() + .bind_pipeline(&pipeline) + .record_cmd(|cmd| { + // Record vulkan commands here + }) + .end_cmd(); +# Ok(()) } +``` + +Pipelines are cheap to `Clone` and should be cached in between use. The recommendation is to bind a +borrow of a pipeline to when beginning a command. + +## Commands + +A graph command is the smallest unit which the `Submission` type will schedule for execution. + +Calls to `Graph::begin_cmd` (and, optionally `Graph::end_cmd`) define a single graph command which +will execute in physical order as recorded. During graph command recording you may change pipelines, +modify shader descriptor bindings, or otherwise modify the state of the command buffer. + +Example: + +```rust +# macro_rules! include_bytes { ($path:expr) => { [0u8] }; } +# use vk_graph::Graph; +# use vk_graph::driver::{DriverError, device::Device}; +# use vk_graph::driver::compute::{ComputePipeline, ComputePipelineInfo}; +# fn test(device: &Device) -> Result<(), DriverError> { +let info = ComputePipelineInfo::default(); + +let fire = include_bytes!("fire.spv"); +let fire = ComputePipeline::create(device, info, fire.as_slice())?; + +let water = include_bytes!("water.spv"); +let water = ComputePipeline::create(device, info, water.as_slice())?; + +let mut graph = Graph::default(); +graph + .begin_cmd() + .bind_pipeline(&fire) + .record_cmd(|cmd| { + println!("1st"); + }) + .bind_pipeline(&water) + .record_cmd(|cmd| { + println!("2nd"); + }) + .bind_pipeline(&fire) + .record_cmd(|cmd| { + println!("3rd"); + }) + .end_cmd() + .begin_cmd() + .bind_pipeline(&water) + .record_cmd(|cmd| { + println!("4th"); + }); +# Ok(()) } +``` + +A call to `Graph::end_cmd` is not requried. The _end-command_ method exists to support builder-style +function-chaining. In the above example two commands are built and added to the graph. + +## Shaders + +Compute, graphic, and ray trace pipelines require one or more shaders: + +Pipeline Type|Shaders +--|-- +`ComputePipeline`|Single: must be compute stage +`GraphicPipeline`|Multiple: must be a raster stage +`RayTracePipeline`|Multiple: must be a ray tracing stage + +> [!CAUTION] +> All `Shader` constructors panic when provided with invalid SPIR-V shader code. + +The `Shader` type uses a builder pattern: + +```rust +# macro_rules! include_bytes { ($path:expr) => { [0u8] }; } +# use vk_graph::Graph; +# use vk_graph::driver::{DriverError, device::Device}; +# use vk_graph::driver::compute::{ComputePipeline, ComputePipelineInfo}; +# use vk_graph::driver::shader::{SamplerInfo, Shader}; +# fn test(device: &Device) -> Result<(), DriverError> { +// Pipelines may be created using "shader" or "custom": +let code = include_bytes!("raygen.spv"); +let shader = Shader::from_spirv(code.as_slice()); +let custom = shader + .entry_name("main_but_faster") + .image_sampler(0, SamplerInfo::default()) + .image_sampler(1, SamplerInfo::LINEAR); +# Ok(()) } +``` diff --git a/guide/src/pipeline_hot_reload.md b/guide/src/pipeline_hot_reload.md new file mode 100644 index 00000000..08b82924 --- /dev/null +++ b/guide/src/pipeline_hot_reload.md @@ -0,0 +1,53 @@ +# Hot Reload + +An accessory crate is provided to support automatic reloading of changed shader pipelines. + +`vk-graph-hot` uses a file watcher and `Shaderc`. It may be used directly or may be swapped out +using a build feature: + +```toml +# Cargo.toml + +[features] +default = [] +hot = ["dep:vk-graph-hot"] + +[dependencies] +vk-graph = "{{ crate.version }}" +vk-graph-hot = { version = "{{ vk-graph-hot.version }}", optional = true } +``` + +```rust +# macro_rules! include_bytes { ($path:expr) => { [0u8] }; } +use vk_graph::driver::{DriverError, compute::ComputePipelineInfo, device::Device}; + +#[cfg(feature = "hot")] +use vk_graph_hot::{ + HotComputePipeline as ComputePipeline, + HotShader, +}; + +#[cfg(not(feature = "hot"))] +use vk_graph::driver::{ + compute::ComputePipeline, + shader::Shader, +}; + +pub fn create_fire_pipeline( + device: &Device, +) -> Result { + let info = ComputePipelineInfo::default(); + + #[cfg(feature = "hot")] + let shader = HotShader::from_path("fire.glsl"); + + #[cfg(not(feature = "hot"))] + let shader = Shader::from_spirv(include_bytes!("fire.spv").as_slice()); + + ComputePipeline::create(device, info, shader) +} +``` + +> [!NOTE] +> The hot versions of each type support all features, options, and usage provided by the normal +> types. This include public fields, available information, and graph binding features. diff --git a/guide/src/pipeline_push_const.md b/guide/src/pipeline_push_const.md new file mode 100644 index 00000000..a5cb9c4d --- /dev/null +++ b/guide/src/pipeline_push_const.md @@ -0,0 +1,53 @@ +# Push Constants + +Command buffers may update a very small data cache which shaders may read during execution using +push constants. + +The Vulkan minimum is `128` bytes, but many devices expose a larger limit. +Check [`PhysicalDeviceLimits::max_push_constants_size`](https://docs.rs/vk-graph/latest/vk_graph/driver/physical_device/struct.PhysicalDeviceLimits.html#structfield.max_push_constants_size) +and keep the payload small. + +API docs: [`ComputeCommandRef::push_constants`](https://docs.rs/vk-graph/latest/vk_graph/cmd/compute/struct.ComputeCommandRef.html#method.push_constants), +[`GraphicCommandRef::push_constants`](https://docs.rs/vk-graph/latest/vk_graph/cmd/graphic/struct.GraphicCommandRef.html#method.push_constants), +[`RayTraceCommandRef::push_constants`](https://docs.rs/vk-graph/latest/vk_graph/cmd/ray_trace/struct.RayTraceCommandRef.html#method.push_constants). + +```glsl +// render_mesh.glsl +#version 460 core + +layout(push_constant) uniform PushConstants { + layout(offset = 0) uint mesh_index; +}; + +... +``` + +```rust +# macro_rules! include_bytes { ($path:expr) => { [0u8] }; } +# use vk_graph::Graph; +# use vk_graph::driver::{DriverError, device::Device}; +# use vk_graph::driver::compute::{ComputePipeline, ComputePipelineInfo}; +# use vk_graph::driver::shader::Shader; +# fn test(device: &Device) -> Result<(), DriverError> { +let info = ComputePipelineInfo::default(); +let code = include_bytes!("render_mesh.spv"); +let shader = Shader::new_compute(code.as_slice()); +let pipeline = ComputePipeline::create(device, info, shader)?; + +let mut graph = Graph::default(); +let data = 42u32.to_ne_bytes(); + +graph + .begin_cmd() + .bind_pipeline(&pipeline) + .record_cmd(move |cmd| { + cmd + .push_constants(0, &data) + .dispatch(1, 1, 1); + }); +# Ok(()) } +``` + +> [!TIP] +> A crate such as `bytemuck` is helpful for converting Rust structures to bytes suitable for push +> constant usage. See the example code for more. diff --git a/guide/src/pipeline_spec.md b/guide/src/pipeline_spec.md new file mode 100644 index 00000000..a2358a2c --- /dev/null +++ b/guide/src/pipeline_spec.md @@ -0,0 +1,43 @@ +# Specialization + +Pipeline specialization allows pre-compiled SPIR-V binary shaders to be specialized with constant +values specified at run-time. + +The Vulkan implementation may use these constant values to generate optimized shader code. + +`vk-graph` provides `SpecializationMap` as an easy-to-use way of storing the data and lookup entries +required to use this feature. + +```glsl +// kaboom.glsl +#version 460 core + +layout(constant_id = 0) const float INFERNO_EPSILON = 0.999; +layout(constant_id = 1) const float COEFF_OF_BOOM = 1.4; +``` + +```rust +# macro_rules! include_bytes { ($path:expr) => { [0u8] }; } +# use vk_graph::driver::{DriverError, device::Device}; +# use vk_graph::driver::shader::{Shader, SpecializationMap}; +# fn test(device: &Device) -> Result<(), DriverError> { +use bytemuck::bytes_of; + +let kaboom = include_bytes!("kaboom.spv"); + +// Use this shader for the glsl-specified values: +let shader = Shader::new_compute(kaboom.as_slice()); + +let better_consts = [ + 0.99999f32, + 1.0, +]; +let better_consts = bytes_of(&better_consts); +let spec = SpecializationMap::new(better_consts) + .constant(0, 0, 4) + .constant(1, 4, 8); + +// Use this shader for the updated run-time values: +let spec_shader = shader.specialization(spec); +# Ok(()) } +``` diff --git a/guide/src/pipeline_sync.md b/guide/src/pipeline_sync.md new file mode 100644 index 00000000..7bd9ff5d --- /dev/null +++ b/guide/src/pipeline_sync.md @@ -0,0 +1,182 @@ +# Synchronization + +`vk-graph` provides a high-performance abstraction over Vulkan synchronization which retains the +low driver overhead of correctly synchronized command buffers. + +## Pipeline Barriers + +Vulkan specifies that resources and pipelines will have synchronized access when barriers are +inserted into the command stream. Unsynchronized access results in undefined behavior. + +> [!TIP] +> Unsynchronized access *may* be detected through debug assertions or Vulkan SDK debugging layers. + +## Access Type Abstraction + +`vk-graph` uses an enumeration of possible states to define all supported pipeline barriers in an +easy-to-use way. + +Sample access types: + +Type|Usage +-|- +`AccessType::General`|Covers any access - useful for debug, generally avoid for performance reasons +`AccessType::ColorAttachmentWrite`|Written as a color attachment during rendering +`AccessType::ComputeShaderReadUniformBuffer`|Read as a uniform buffer in a compute shader + +([_Full list_](https://docs.rs/vk-graph/latest/vk_graph/driver/sync/enum.AccessType.html)) + +## Resource Access + +The required access varies depending on the function being called and what the Vulkan specification +requires for a given command. + +Generally, access must be specified before each command uses a resource. It appears as an "access" +function call: + +```rust +# use vk_graph::Graph; +# use vk_graph::driver::{DriverError, device::Device, sync::AccessType}; +# use vk_graph::node::{BufferNode, ImageNode}; +# fn test( +# device: &Device, +# some_buffer: BufferNode, +# some_image: ImageNode, +# ) -> Result<(), DriverError> { +# let mut graph = Graph::default(); +graph + .begin_cmd() + .resource_access(some_buffer, AccessType::TransferRead) + .resource_access(some_image, AccessType::TransferWrite) + .record_cmd(|cmd| { + // we are synchronized! + // You may: + // - Read some_buffer + // - Write some_image + }); +# Ok(()) } +``` + +Resource access is specified for and consumed by the following command buffer recording. For +multiple accesses, use multiple "access" and "record" function calls: + +```rust +# use vk_graph::Graph; +# use vk_graph::driver::{DriverError, device::Device, sync::AccessType}; +# use vk_graph::node::{BufferNode, ImageNode}; +# fn test( +# device: &Device, +# buffer: BufferNode, +# image: ImageNode, +# ) -> Result<(), DriverError> { +# let mut graph = Graph::default(); +graph + .begin_cmd() + .resource_access(buffer, AccessType::TransferRead) + .resource_access(image, AccessType::TransferWrite) + .record_cmd(|cmd| { + // Safe to copy buffer to image + }) + .resource_access(image, AccessType::TransferRead) + .resource_access(buffer, AccessType::TransferWrite) + .record_cmd(|cmd| { + // Safe to copy image to buffer + }); +# Ok(()) } +``` + +## Shader Resource Access + +When a resource (buffer, image, or acceleration structure) is accessed from a shader the +`shader_resource_access` function is used: + +```glsl +// clear_image.glsl +#version 460 core +#pragma shader_stage(compute) + +layout(binding = 42, rgba8) writeonly uniform image2D dstImage; + +void main() { + imageStore( + dstImage, + ivec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y), + vec4(0) + ); +} +``` + +```rust +# macro_rules! include_bytes { ($path:expr) => { [0u8] }; } +# use vk_graph::Graph; +# use vk_graph::driver::{DriverError, ash::vk, device::Device, sync::AccessType}; +# use vk_graph::driver::compute::{ComputePipeline, ComputePipelineInfo}; +# use vk_graph::driver::image::{Image, ImageInfo}; +# fn test(device: &Device) -> Result<(), DriverError> { +let mut graph = Graph::default(); + +let fmt = vk::Format::R8G8B8A8_UNORM; +let usage = vk::ImageUsageFlags::STORAGE; +let info = ImageInfo::image_2d(32, 32, fmt, usage); +let image = graph.bind_resource(Image::create( + device, + info, +)?); + +graph + .begin_cmd() + .bind_pipeline(ComputePipeline::create( + device, + ComputePipelineInfo::default(), + include_bytes!("clear_image.spv").as_slice(), + )?) + .shader_resource_access(42, image, AccessType::ComputeShaderWrite) + .record_cmd(|cmd| { + cmd.dispatch(32, 32, 1); + }); +# Ok(()) } +``` + +## Subresource Access + +Buffer ranges and image views are referred to as subresource ranges and accessed using "subresource" +function variants: + +```rust +# macro_rules! include_bytes { ($path:expr) => { [0u8] }; } +# use vk_graph::Graph; +# use vk_graph::driver::{DriverError, ash::vk, device::Device, sync::AccessType}; +# use vk_graph::driver::compute::{ComputePipeline, ComputePipelineInfo}; +# use vk_graph::driver::image::{Image, ImageInfo}; +# fn test(device: &Device) -> Result<(), DriverError> { +let mut graph = Graph::default(); + +let fmt = vk::Format::R8G8B8A8_UNORM; +let usage = vk::ImageUsageFlags::STORAGE; +let info = ImageInfo::image_2d(32, 32, fmt, usage); +let image = graph.bind_resource(Image::create( + device, + info, +)?); + +graph + .begin_cmd() + .bind_pipeline(ComputePipeline::create( + device, + ComputePipelineInfo::default(), + include_bytes!("clear_image.spv").as_slice(), + )?) + .shader_subresource_access(42, image, info, AccessType::ComputeShaderWrite) + .record_cmd(|cmd| { + cmd.dispatch(32, 32, 1); + }); +# Ok(()) } +``` + +## Built-In Commands + +The commands directly attached to a `Graph`, such as `Graph::copy_buffer_to_image`, do not require +any access function calls. + +The source code for these built-in commands uses public graph functions and provides good examples +of typical usage. diff --git a/.github/img/profile.png b/guide/src/profile.png similarity index 100% rename from .github/img/profile.png rename to guide/src/profile.png diff --git a/guide/src/resource.md b/guide/src/resource.md new file mode 100644 index 00000000..ae2e28ce --- /dev/null +++ b/guide/src/resource.md @@ -0,0 +1,67 @@ +# Resources + +API docs: [`Graph::bind_resource`](https://docs.rs/vk-graph/latest/vk_graph/struct.Graph.html#method.bind_resource), +[`Graph::resource`](https://docs.rs/vk-graph/latest/vk_graph/struct.Graph.html#method.resource), +[`Node`](https://docs.rs/vk-graph/latest/vk_graph/node/trait.Node.html), +[`Pool`](https://docs.rs/vk-graph/latest/vk_graph/pool/trait.Pool.html), +[`Cache`](https://docs.rs/vk-graph/latest/vk_graph/pool/cache/struct.Cache.html). + +> [!CAUTION] +> All pipelines and resources (_buffers, images, and acceleration structures_) used in a `Graph` +> must have been created using the same `Device`. + +Owned resources are created from `Device` references. They may be bound directly to graphs. + +An `Arc` or `&Arc` of any resource may be bound to a graph if the resource needs to be +referenced in future graphs. + +## Binding + +Binding resources to a graph produces a "Node" handle which may be used in commands and shader +pipelines. + +Example for buffers using `Graph::bind_resource(&mut self, resource: R) -> N`: + +`R`|`N` +-|- +`Buffer`|`BufferNode` +`Arc`|`BufferNode` +`Lease`|`BufferLeaseNode` +`Arc>`|`BufferLeaseNode` + +## Borrowing + +Resources may be borrowed from a graph. + +Example for buffers using `Graph::resource(&self, node: N) -> &R`: + +`N`|`R` +-|- +`BufferNode`|`Arc` +`BufferLeaseNode`|`Arc>` + +## Bound Resource Nodes + +The concept of binding resources to graphs as node handles exists to support the callback-style +command buffer recording provided by `vk-graph`. + +Commands are recorded in logical order, but the execution is re-ordered for performance and so a +closure argument is provided to call Vulkan command buffer functions. The use of a small and `Copy` +node handle allows resource handles to be moved into command buffer closures without `Arc::clone`. + +Additionally, node handles support internal optimizations by providing direct indexed access to +graph data structures. + +## Pooling Resources + +Pooled resources are requested from `Pool` implementations. Dropped resources return to the pool. + +The `Lease` return type otherwise acts like an owned resource. + +## Cached Resources + +Resource caching is available using [`Cache`](https://docs.rs/vk-graph/latest/vk_graph/pool/cache/struct.Cache.html) +over any `Pool`. + +Cached resources let complex graphs reuse compatible resources while keeping the pooling strategy +separate from the reuse policy. diff --git a/guide/src/resource_accel_struct.md b/guide/src/resource_accel_struct.md new file mode 100644 index 00000000..c571feae --- /dev/null +++ b/guide/src/resource_accel_struct.md @@ -0,0 +1,91 @@ +# Acceleration Structures + +```rust +# use vk_graph::Graph; +# use vk_graph::driver::{DriverError, ash::vk, device::Device}; +# use vk_graph::driver::accel_struct::{ +# AccelerationStructure, AccelerationStructureGeometry, AccelerationStructureGeometryData, +# AccelerationStructureGeometryInfo, AccelerationStructureInfo, AccelerationStructureInfoBuilder, +# AccelerationStructureSize, DeviceOrHostAddress +# }; +# use vk_graph::driver::buffer::Buffer; +# fn test( +# device: &Device, +# ) -> Result<(), DriverError> { +// Some buffer holding geometry data +let buffer: Buffer = todo!(); + +// Some sample geometry to put into a BLAS: +let geometry = AccelerationStructureGeometryData::Triangles { + index_addr: DeviceOrHostAddress::DeviceAddress( + buffer.device_address() + ), + index_type: vk::IndexType::UINT16, + max_vertex: 100, + transform_addr: None, + vertex_addr: DeviceOrHostAddress::DeviceAddress( + buffer.device_address() + 2_048 + ), + vertex_format: vk::Format::R32G32B32_SFLOAT, + vertex_stride: 12, +}; +let geom = AccelerationStructureGeometry { + max_primitive_count: 120, + flags: vk::GeometryFlagsKHR::OPAQUE, + geometry, +}; +let build_range = vk::AccelerationStructureBuildRangeInfoKHR { + primitive_count: 120, + primitive_offset: 0, + first_vertex: 0, + transform_offset: 0, +}; +let ty = vk::AccelerationStructureTypeKHR::BOTTOM_LEVEL; +let geom_info = AccelerationStructureGeometryInfo { + ty, + flags: vk::BuildAccelerationStructureFlagsKHR::ALLOW_UPDATE, + geometries: vec![ + (geom, build_range), + ].into_boxed_slice(), +}; + +// Use helper function to find size +let AccelerationStructureSize { + build_size, + .. +} = AccelerationStructure::size_of(device, &geom_info); + +// Create acceleration structure info multiple ways: +let info = AccelerationStructureInfo { + ty, + size: build_size, +}; +let other_info = AccelerationStructureInfo::blas(build_size); + +assert_eq!(info, other_info); + +// Builder pattern +let same_info = AccelerationStructureInfoBuilder::default() + .ty(ty) + .size(build_size); + +// Create directly from info +let blas = AccelerationStructure::create(device, info)?; + +// Info built from other info +// Note: Never calculate size/always get from function +let more_info = blas + .info + .into_builder() + .size(build_size * 2) + .build(); + +// The provided fields are helpful: +assert_eq!(blas.buffer.device, *device); +assert_eq!(blas.info, info); +assert_ne!(blas.buffer.handle, vk::Buffer::null()); +assert_ne!(blas.handle, vk::AccelerationStructureKHR::null()); + +// Acceleration structures have no "subresources" and are bound whole +# Ok(()) } +``` diff --git a/guide/src/resource_buffer.md b/guide/src/resource_buffer.md new file mode 100644 index 00000000..1f8c76d7 --- /dev/null +++ b/guide/src/resource_buffer.md @@ -0,0 +1,59 @@ +# Buffers + +```rust +# use vk_graph::Graph; +# use vk_graph::driver::{DriverError, ash::vk, device::Device}; +# use vk_graph::driver::buffer::{Buffer, BufferInfo, BufferInfoBuilder}; +# fn test( +# device: &Device, +# ) -> Result<(), DriverError> { +let size = 1_024; +let usage = vk::BufferUsageFlags::STORAGE_BUFFER; + +// Create buffer info multiple ways: +let info = BufferInfo { + alignment: 1, + dedicated: false, + host_read: false, + host_write: false, + size, + usage, +}; +let device_mem = BufferInfo::device_mem(size, usage); +let host_mem = BufferInfo::host_mem(size, usage); + +assert_eq!(info, device_mem); +assert_ne!(info, host_mem); + +// Builder pattern +let same_info = BufferInfoBuilder::default() + .size(size) + .usage(usage); + +// Info built from other info +let more_info = host_mem + .into_builder() + .usage(usage | vk::BufferUsageFlags::INDIRECT_BUFFER) + .build(); + +// There is a helper function for creating buffers from a slice +let data = [1u8, 2, 3, 4]; +let buffer = Buffer::create_from_slice(device, usage, &data)?; + +// This is equivalent to: +let mut buffer = Buffer::create(device, host_mem)?; +buffer.copy_from_slice(0, &data); + +// Or use the std copy_from_slice (it panics if size != range) +let mut buffer = Buffer::create(device, host_mem)?; +buffer.mapped_slice_mut().copy_from_slice(&data); + +// The provided fields are helpful: +assert_eq!(buffer.device, *device); +assert_eq!(buffer.info, host_mem); +assert_ne!(buffer.handle, vk::Buffer::null()); + +// Buffer "subresources" are just ranges of that buffer +let my_subresource = 0..size; +# Ok(()) } +``` diff --git a/guide/src/resource_image.md b/guide/src/resource_image.md new file mode 100644 index 00000000..c81b0385 --- /dev/null +++ b/guide/src/resource_image.md @@ -0,0 +1,90 @@ +# Images + +```rust +# use vk_graph::Graph; +# use vk_graph::driver::{DriverError, ash::vk, device::Device}; +# use vk_graph::driver::image::{Image, ImageInfo, ImageInfoBuilder, SampleCount}; +# use vk_graph::driver::image::{ImageViewInfo, ImageViewInfoBuilder}; +# fn test( +# device: &Device, +# ) -> Result<(), DriverError> { +let (width, height) = (320, 200); +let usage = vk::ImageUsageFlags::SAMPLED; +let fmt = vk::Format::R8G8B8A8_UNORM; + +// Create image info multiple ways +let info = ImageInfo { + array_layer_count: 1, + dedicated: false, + depth: 1, + flags: vk::ImageCreateFlags::empty(), + fmt, + height, + mip_level_count: 1, + sample_count: SampleCount::Type1, + tiling: vk::ImageTiling::OPTIMAL, + ty: vk::ImageType::TYPE_2D, + usage, + width, +}; +let other_info = ImageInfo::image_2d(width, height, fmt, usage); +let cube_info = ImageInfo::cube(width, fmt, usage); + +assert_eq!(info, other_info); +assert_ne!(info, cube_info); + +// Builder pattern +let same_info = ImageInfoBuilder::default() + .width(width) + .height(height) + .depth(1) + .fmt(fmt) + .usage(usage) + .ty(vk::ImageType::TYPE_2D); + +// Info built from other info +let array_info = cube_info + .into_builder() + .flags(vk::ImageCreateFlags::TYPE_2D_ARRAY_COMPATIBLE) + .build(); + +// Images are created simply +let image = Image::create(device, info)?; + +// For interop this may be handy: +let image = Image::from_raw(device, vk::Image::null(), info); + +// The provided fields are helpful: +assert_eq!(image.device, *device); +assert_eq!(image.info, info); +assert_ne!(image.handle, vk::Image::null()); + +// Image "subresources" are the native type: +let my_subresource = vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, +}; + +// Image views are also subresources: +let image_view = ImageViewInfo { + array_layer_count: 1, + aspect_mask: vk::ImageAspectFlags::COLOR, + base_array_layer: 0, + base_mip_level: 0, + fmt, + mip_level_count: 1, + ty: vk::ImageViewType::TYPE_2D, +}; + +// Image views have the same builder functionality: +let other_view = ImageViewInfoBuilder::default(); + +// Image views can be inferred from the whole image info: +let addl_view = info.into_image_view(); + +assert_eq!(image_view, addl_view); +# Ok(()) } +``` diff --git a/guide/src/usage.md b/guide/src/usage.md new file mode 100644 index 00000000..2c4926e8 --- /dev/null +++ b/guide/src/usage.md @@ -0,0 +1,283 @@ +# Usage + +`vk-graph` acts as a safe builder-pattern for the Vulkan API. + +API docs: [`Graph`](https://docs.rs/vk-graph/latest/vk_graph/struct.Graph.html), +[`Device`](https://docs.rs/vk-graph/latest/vk_graph/driver/device/struct.Device.html), +[`Graph::begin_cmd`](https://docs.rs/vk-graph/latest/vk_graph/struct.Graph.html#method.begin_cmd), +[`Graph::bind_resource`](https://docs.rs/vk-graph/latest/vk_graph/struct.Graph.html#method.bind_resource), +[`Graph::resource`](https://docs.rs/vk-graph/latest/vk_graph/struct.Graph.html#method.resource), +[`Graph::into_submission`](https://docs.rs/vk-graph/latest/vk_graph/struct.Graph.html#method.into_submission). + +Typical usage contains: + +```rust +# use vk_graph::driver::DriverError; +# struct Foo { device: vk_graph::driver::device::Device } +# impl Foo { +# fn test( +# &self, +# ) { +use vk_graph::driver::ash::vk; +use vk_graph::driver::device::Device; + +// A borrow of Device is an argument of many vk-graph functions +let device: &Device = &self.device; +# } } +``` + +## Resources + +Resources, such as buffers and images, may be created from "`Info`" structs: + +```rust +# use vk_graph::Graph; +# use vk_graph::driver::{DriverError, ash::vk, device::Device, sync::AccessType}; +# use vk_graph::driver::buffer::{Buffer, BufferInfo}; +# use vk_graph::driver::image::{Image, ImageInfo}; +# fn test( +# device: &Device, +# ) -> Result<(), DriverError> { +let usage = vk::BufferUsageFlags::TRANSFER_SRC; +let buffer_info = BufferInfo::device_mem(320 * 200 * 4, usage); +let buffer = Buffer::create(device, buffer_info)?; + +let usage = vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST; +let image_info = ImageInfo::image_2d(320, 200, vk::Format::R8G8B8A8_UNORM, usage); +let image = Image::create(device, image_info)?; +# Ok(()) } +``` + +### Memory Allocation + +`vk-graph` uses an external memory allocator (currently `gpu-allocator`) for resource memory +allocations. + +The allocation strategy provides a large section of memory which is then sub-allocated for any +resources which use it. This may lead to fragmentation and memory exhaustion in some scenarios. + +Individual buffers or images may use dedicated memory allocations by setting their `dedicated` +field: + +```rust +# use vk_graph::Graph; +# use vk_graph::driver::{DriverError, ash::vk, device::Device, sync::AccessType}; +# use vk_graph::driver::buffer::{Buffer, BufferInfo}; +# use vk_graph::driver::image::{Image, ImageInfo}; +# fn test( +# device: &Device, +# ) -> Result<(), DriverError> { +# let buffer_info = BufferInfo::device_mem(1, vk::BufferUsageFlags::empty()); +# let image_info = ImageInfo::image_2d(32, 32, vk::Format::R16_UNORM, vk::ImageUsageFlags::empty()); +// The info fields may be used or set directly +let uber_mesh_buf = Buffer::create( + device, + BufferInfo { + dedicated: true, + ..buffer_info + } +)?; + +// Builder functions are also availble +// (builder and info types are interchangable) +let dedicated_info = image_info.into_builder().dedicated(true); +let important_image = Image::create(device, dedicated_info)?; +# Ok(()) } +``` + +Resources may be bound to a graph as typed node handles referred to as _"nodes"_: + +```rust +# use vk_graph::Graph; +# use vk_graph::driver::{DriverError, ash::vk, device::Device, sync::AccessType}; +# use vk_graph::driver::buffer::{Buffer, BufferInfo}; +# use vk_graph::driver::image::{Image, ImageInfo}; +# use vk_graph::node::{BufferNode, ImageNode}; +# fn test( +# device: &Device, +# buffer: Buffer, +# image: Image, +# ) -> Result<(), DriverError> { +let mut graph = Graph::default(); +let buffer: BufferNode = graph.bind_resource(buffer); +let image: ImageNode = graph.bind_resource(image); +# Ok(()) } +``` + +Bound resources may be borrowed from graphs, commands, pipeline commands, or command buffers using +their node handle: + +```rust +# use vk_graph::Graph; +# use vk_graph::driver::image::Image; +# use vk_graph::node::ImageNode; +# use std::sync::Arc; +# fn test( +# graph: &mut Graph, +# image: ImageNode, +# ) { +let shared_image: &Arc = graph.resource(image); + +assert_eq!(shared_image.info.width, 320); +# } +``` + +## Commands + +Nodes may be used with built-in graph commands: + +```rust +# use vk_graph::Graph; +# use vk_graph::cmd::ClearColorValue; +# use vk_graph::node::ImageNode; +# use std::sync::Arc; +# fn test( +# graph: &mut Graph, +# image: ImageNode, +# ) { +graph.clear_color_image(image, ClearColorValue::BLACK_ALPHA_ZERO); +# } +``` + +Graphs may contain many commands: + +```rust +# use vk_graph::Graph; +# use vk_graph::cmd::ClearColorValue; +# use vk_graph::node::{BufferNode, ImageNode}; +# use std::sync::Arc; +# fn test( +# graph: &mut Graph, +# buffer: BufferNode, +# image: ImageNode, +# ) { +graph + .fill_buffer(buffer, 0..320 * 200, 0) + .copy_buffer_to_image(buffer, image); +# } +``` + +Custom commands enable advanced Vulkan behavior: + +```rust +# use vk_graph::Graph; +# use vk_graph::cmd::ClearColorValue; +# use vk_graph::driver::{ash::vk, sync::AccessType}; +# use vk_graph::node::{BufferNode, ImageNode}; +# use std::sync::Arc; +# fn test( +# graph: &mut Graph, +# buffer: BufferNode, +# image: ImageNode, +# ) { +graph + .begin_cmd() + .resource_access(image, AccessType::TransferRead) + .resource_access(buffer, AccessType::TransferWrite) + .record_cmd(move |cmd| { + // Borrow resources from nodes we move into the closure + let buffer = cmd.resource(buffer); + let image = cmd.resource(image); + + // Run *any* Vulkan code using ash::Device + unsafe { + // Note: for example only, use safe versions! + cmd.device.cmd_copy_image_to_buffer2( + cmd.handle, + &vk::CopyImageToBufferInfo2::default() + .src_image(image.handle) + .dst_buffer(buffer.handle), + ); + } + }) + .end_cmd(); +# } +``` + +## Pipelines + +Pipelines allow shader code to execute as a graph command. A borrow of a pipeline may be bound to +record shader-stage specific commands: + +```glsl +// compute.glsl +#version 460 core +#pragma shader_stage(compute) + +layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; + +layout(binding = 0, rgba8) writeonly uniform image2D dstImage; + +void main() { + imageStore( + dstImage, + ivec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y), + vec4(0.0) + ); +} +``` + +```bash +# See: "Shader Compilation" +glslc compute.glsl -o compute.spv +``` + +```rust +# macro_rules! include_bytes { ($path:expr) => { [0u8] }; } +# use vk_graph::Graph; +# use vk_graph::driver::{DriverError, device::Device, sync::AccessType}; +# use vk_graph::driver::compute::{ComputePipeline, ComputePipelineInfo}; +# use vk_graph::node::ImageNode; +# fn test( +# graph: &mut Graph, +# device: &Device, +# image: ImageNode, +# ) -> Result<(), DriverError> { +let pipeline = ComputePipeline::create( + device, + ComputePipelineInfo::default(), + include_bytes!("compute.spv").as_slice(), +)?; + +graph + .begin_cmd() + .bind_pipeline(&pipeline) + .shader_resource_access(0, image, AccessType::ComputeShaderWrite) + .record_cmd(|cmd| { + cmd.dispatch(320, 200, 1); + }); +# Ok(()) } +``` + +## Queue Submission + +Completed graphs are queued for execution by a Vulkan implementation. + +> [!NOTE] +> While executing, resources used in a graph may be bound and used by other graphs. Graph commands +> access resources in the logical state defined by all prior commands and previously submitted +> graphs. + +Typical programs rely on a single `Graph` per frame and let their window implementation submit the +graph, but they may do so manually: + +```rust +# use vk_graph::Graph; +# use vk_graph::driver::{DriverError, device::Device}; +# use vk_graph::pool::lazy::LazyPool; +# fn test( +# graph: Graph, +# device: &Device, +# ) -> Result<(), DriverError> { +// NOTE: This will stall! Use the async functions to check periodically instead +graph + .into_submission() + .queue_submit(&mut LazyPool::new(device), 0, 0)? + .wait_until_executed()?; +# Ok(()) } +``` + +### Device Usage + +Buffers, images, and acceleration structure resources are created and used by a single `Device`. +All commands which use a resource must execute on the same `Device` which created the resource. diff --git a/guide/src/usage_debugging.md b/guide/src/usage_debugging.md new file mode 100644 index 00000000..136f5302 --- /dev/null +++ b/guide/src/usage_debugging.md @@ -0,0 +1,77 @@ +# Debugging + +Debug mode (setting the `debug` field of `DeviceInfo` or `InstanceInfo` to `true`) is supported only +when a compatible [_Vulkan SDK_](https://vulkan.lunarg.com/sdk/home) + is installed. + +> [!IMPORTANT] +> The installed Vulkan SDK version must be at least v{{ vulkan_sdk.version }}. + +While in debug mode `vk-graph` watches for errors, warnings, and certain performance warnings +emitted from any currently enabled Vulkan debug application layers. Emitted events will cause the +active thread to be parked and log a message indicating how to attach a debugger. + +## Logging + +`vk-graph` uses `log` v{{ log.version }} for low-overhead logging. + +To enable logging, set the `RUST_LOG` environment variable to `trace`, `debug`, `info`, `warn` or +`error` and initialize the logging provider of your choice. Examples use `pretty_env_logger`. + +_You may also filter messages, for example:_ + +```bash +RUST_LOG=vk_graph::driver=trace,vk_graph=warn cargo run --example ray_trace +``` + +``` +TRACE vk_graph::driver::instance > created a Vulkan instance +DEBUG vk_graph::driver::physical_device > physical device: NVIDIA GeForce RTX 3090 +DEBUG vk_graph::driver::physical_device > extension "VK_KHR_16bit_storage" v1 +DEBUG vk_graph::driver::physical_device > extension "VK_KHR_8bit_storage" v1 +DEBUG vk_graph::driver::physical_device > extension "VK_KHR_acceleration_structure" v13 +... +``` + +## Performance Profiling + +`vk-graph` uses `profiling` v{{ profiling.version }} and supports multiple profiling providers. When +not in use profiling has zero cost. + +To enable profiling, compile with one of the `profile-with-*` features enabled and initialize the +profiling provider of your choice. + +_Example using `puffin`:_ + +```bash +cargo run --features profile-with-puffin --release --example vsm_omni +``` + +Flamegraph of performance data + + +### Comparing Results + +Always profile code using a release-mode build. + +You may need to disable CPU thermal throttling in order to get consistent results on some platforms. +The inconsistent results are certainly valid, but they do not help in accurately measuring potential +changes. This may be done on Intel Linux machines by modifying the Intel P-State driver: + +```bash +echo 100 | sudo tee /sys/devices/system/cpu/intel_pstate/min_perf_pct +``` + +([_Source_](https://www.kernel.org/doc/Documentation/cpu-freq/intel-pstate.txt) +) + +## Helpful tools + +- [_VulkanSDK_](https://vulkan.lunarg.com/sdk/home) + +_(Required when setting `debug` to `true`)_ +- NVIDIA: [_nvidia-smi_](https://developer.nvidia.com/nvidia-system-management-interface) + +- AMD: [_RadeonTop_](https://github.com/clbr/radeontop) + +- [_RenderDoc_](https://renderdoc.org/) diff --git a/guide/src/usage_device.md b/guide/src/usage_device.md new file mode 100644 index 00000000..e609fb3f --- /dev/null +++ b/guide/src/usage_device.md @@ -0,0 +1,139 @@ +# Device Creation + +Most Vulkan operations occur within the context of a logical device, provided by +`Device` (_a smart pointer for `ash::Device`_). + +API docs: [`Device::create`](https://docs.rs/vk-graph/latest/vk_graph/driver/device/struct.Device.html#method.create), +[`Device::try_from_ash_device`](https://docs.rs/vk-graph/latest/vk_graph/driver/device/struct.Device.html#method.try_from_ash_device), +[`Device::try_from_display`](https://docs.rs/vk-graph/latest/vk_graph/driver/device/struct.Device.html#method.try_from_display). + +> [!WARNING] +> Vulkan has no global state and does not share resources between devices by default. +> +> Do not combine resources from multiple devices! The steps required to share resources across +> devices are not currently documented. + +## Headless Operation + +For any sort of server-based rendering or similar Vulkan usage without a display, the following is +production-ready code used to create a device: + +```rust +# use vk_graph::driver::DriverError; +# use vk_graph::driver::device::{Device, DeviceInfo}; +# fn test() -> Result<(), DriverError> { +let info = DeviceInfo::default(); +let device = Device::create(info)?; + +assert_eq!(device.physical_device.instance.info.debug, false); +# Ok(()) } +``` + +## Windowed Operation + +Prototype and demo code might use the built-in window handler, which creates a `Device` during +window creation: + +```toml +# Cargo.toml + +[dependencies] +vk-graph-window = "{{ vk-graph-window.version }}" +``` + +```rust +# use vk_graph::driver::device::Device; +# use vk_graph_window::WindowError; +# fn test() -> Result<(), WindowError> { +use vk_graph_window::WindowBuilder; + +let window = WindowBuilder::default().build()?; + +// Before run +let _: &Device = &window.device; + +window.run(|frame| { + // During any frame + let _: &Device = frame.device; +})?; +# Ok(()) } +``` + +## Advanced + +There are several scenarios that require advanced `Device` creation techniques: + +- Allowing user-selection of device +- Custom Window(s) handling +- FFI with OpenXR (_or similar_) +- Unsupported drivers/platforms + +### Device Selection + +The entrypoint is an `Instance` from which the available hardware is enumerated and inspected: + +```rust +# use vk_graph::driver::DriverError; +# use vk_graph::driver::device::Device; +# use vk_graph::driver::instance::{Instance, InstanceInfo}; +# fn test() -> Result<(), DriverError> { +let instance = Instance::new(InstanceInfo::default())?; +let physical_devices = Instance::physical_devices(&instance)?; + +for physical_device in physical_devices { + // We are looking for a device with support for these features + if !physical_device.swapchain_ext + || !physical_device.ray_trace_features.ray_tracing_pipeline { + continue; + } + + let _: Device = physical_device.try_into_device()?; +} +# Ok(()) } +``` + +### Native Device Usage + +Some scenarios require the Vulkan instance and/or device be created by other code and accepted for +use by `vk-graph`: + +```rust +# use vk_graph::Graph; +# use vk_graph::driver::DriverError; +# use vk_graph::driver::ash::{self, vk}; +# use vk_graph::driver::device::Device; +# use vk_graph::driver::instance::Instance; +# fn test() -> Result<(), DriverError> { +// Native ash types from somewhere else +let entry: ash::Entry = todo!(); +let instance: vk::Instance = todo!(); +let physical_device: vk::PhysicalDevice = todo!(); + +// vk-graph types +let instance = Instance::from_entry(entry, instance)?; +let physical_device = Instance::physical_device(&instance, physical_device)?; + +// Use our PhysicalDevice to create a native ash::Device (OpenXR requires this) +let device: ash::Device = unsafe { + physical_device + .create_ash_device(|create_info| { + // Somewhere else also provides the logical device! + let device: vk::Device = todo!(); + + let device: ash::Device = unsafe { + ash::Device::load(instance.fp_v1_0(), device) + }; + + Ok(device) + }) +}.unwrap(); + +// Create a Device from their native stuff +let device = Device::try_from_ash_device(device, physical_device)?; +# Ok(()) } +``` + +> [!TIP] +> See [_`examples/vr`_](https://github.com/attackgoat/vk-graph/tree/main/examples/vr) +> for an in-depth example of native device +> usage. diff --git a/guide/src/usage_moltenvk.md b/guide/src/usage_moltenvk.md new file mode 100644 index 00000000..7c93a98d --- /dev/null +++ b/guide/src/usage_moltenvk.md @@ -0,0 +1,16 @@ +# MoltenVK + +Vulkan is emulated on Apple platforms using MoltenVK. + +> [!WARNING] +> MoltenVK does not support all Vulkan features and has limited extension and format support. Pay +> particular attention to these areas: +> - Bindless descriptor count limit +> - Hardware queues provided for execution +> - Indirect drawing command support +> - Image format support + +Support for MoltenVK is best-effort and may not always be up to date. In the event that any +`vk-graph` workflow does not work using MoltenVK please +[_open an issue_](https://github.com/attackgoat/vk-graph/issues) +. diff --git a/guide/src/usage_shader.md b/guide/src/usage_shader.md new file mode 100644 index 00000000..e1a4afa3 --- /dev/null +++ b/guide/src/usage_shader.md @@ -0,0 +1,47 @@ +# Shader Compilation + +`vk-graph` does not provide any shader compiler or require any specific shading language. Users must +provide SPIR-V binary-format shaders. + +> [!TIP] +> See [Hot Reload](./pipeline_hot_reload.md) for details on a shader compiler provided as a separate +> crate. + +Examples using multiple shading languages and compilers are provided in the +[_`examples/`_](https://github.com/attackgoat/vk-graph/tree/main/examples) + directory. + +## Shader-stage `#pragma` + +This applies to GLSL and Shaderc generally but you might find similar functionality with other +languages and compilers. + +```glsl +// shader.glsl +#version 460 core +#pragma shader_stage(compute) + +void main() { + // Some code here +} +``` + +```bash +glslc shader.glsl -o shader.spv +``` + +```rust +# macro_rules! include_bytes { ($path:expr) => { [0u8] }; } +# use vk_graph::driver::shader::Shader; +let spirv = include_bytes!("shader.spv"); + +// #pragma allows for from_spirv syntax: +let shader = Shader::from_spirv( + spirv.as_slice(), +); + +// Without this #pragma we must specify stage: +let shader = Shader::new_compute( + spirv.as_slice(), +); +``` diff --git a/guide/src/usage_thread.md b/guide/src/usage_thread.md new file mode 100644 index 00000000..fdf89194 --- /dev/null +++ b/guide/src/usage_thread.md @@ -0,0 +1,70 @@ +# Threading Behavior + +`vk-graph` is intended to provide scalable performance when used on multiple host threads. +Resources are externally synchronized, and mutable graph-building APIs such as `Graph::begin_cmd` +require exclusive access to the `Graph` itself. + +API docs: [`Submission`](https://docs.rs/vk-graph/latest/vk_graph/struct.Submission.html), +[`Submission::queue_submit`](https://docs.rs/vk-graph/latest/vk_graph/struct.Submission.html#method.queue_submit), +[`Submission::queue_resource`](https://docs.rs/vk-graph/latest/vk_graph/struct.Submission.html#method.queue_resource), +[`Submission::queue_resource_dependencies`](https://docs.rs/vk-graph/latest/vk_graph/struct.Submission.html#method.queue_resource_dependencies), +[`CommandBuffer::has_executed`](https://docs.rs/vk-graph/latest/vk_graph/driver/cmd_buf/struct.CommandBuffer.html#method.has_executed). + +More precisely, `vk-graph` stores the most recent access type of each subresource of a resource. As +commands are submitted to the Vulkan implementation queue, the internal state of these resources is +updated. + +Resource state is updated during the following function calls: + +- `Submission::queue_submit` +- `Submission::queue_resource` +- `Submission::queue_resource_dependencies` + +> [!CAUTION] +> Do not call any `Submission` queue function that accesses buffers, images, or acceleration +> structures currently being submitted on other threads. + +## Execution + +The provided `Submission` queue functions are designed to support a typical swapchain-based +workflow: +1. Queue all commands the swapchain depends on +1. Acquire swapchain +1. Queue swapchain commands +1. Present swapchain +1. Submit any final unrelated commands + +## Safe Patterns + +Resources (buffers, images, or acceleration structures) are the only mutable types which require any +thread safety notes. All other types provided by `vk-graph` are immutable data structures or Vulkan +handle smart pointers. + +For example, there is no race condition or thread contention caused by using the same pipeline on +two threads.[^threads] In fact, there is no runtime overhead at all from this. + +Additionally, it is safe to build `Graph` instances, bind resources, record command buffers, and +call `Graph::into_submission` at *any* time on *any* thread, as long as each `Graph` instance is not +mutably shared across threads at the same time. + +These patterns are safe: +- Build `Graph` and `Send` to another thread for submission +- Build `Graph` and `Drop` it without submission +- `Send` resources to other threads _or_ share as `Arc` +- `Clone` devices or pipelines and `Send` them to other threads + +## Risky Patterns + +Host-mappable buffers require extra understanding to use properly. + +The contents of a buffer are undefined from the time of submission until that `Submission` has been +fully executed, as indicated by `CommandBuffer::has_executed`. This means that you should not call +`Buffer::mapped_slice` during any submission or execution accessing that memory. + +See: +[_`examples/cpu_readback.rs`_](https://github.com/attackgoat/vk-graph/blob/main/examples/cpu_readback.rs) + + + +[^threads]: The internal implementation of `GraphicPipeline` does do a bit of caching in order to +improve performance, however this behavior should not generate issues with any reasonable workload. diff --git a/guide/src/usage_window.md b/guide/src/usage_window.md new file mode 100644 index 00000000..02185b2b --- /dev/null +++ b/guide/src/usage_window.md @@ -0,0 +1,26 @@ +# Window Handling + +`vk-graph` does not directly provide any window implementation. Instead an accessory crate, +`vk-graph-window` is provided, based on `winit`. + +> [!TIP] +> [_`vk-graph-window`_ ](https://github.com/attackgoat/vk-graph/tree/main/crates/vk-graph-window) +> provides additional documentation and +> examples. + +## Swapchain + +The bifurcation of `vk-graph` along the window abstraction results in two `Swapchain` types, one in +each crate. + +Type | Usage +-- | -- +`vk_graph::driver::swapchain::Swapchain` | Vulkan swapchain smart pointer, contains "raw" functions +`vk_graph_window::swapchain::Swapchain` | High-level display interface for building window handlers + +### OpenXR + +Virtual reality support via OpenXR is provided as +[_an example_](https://github.com/attackgoat/vk-graph/tree/main/examples/vr) + which also implements a +swapchain. diff --git a/src/cmd/cmd_ref.rs b/src/cmd/cmd_ref.rs new file mode 100644 index 00000000..83db4efb --- /dev/null +++ b/src/cmd/cmd_ref.rs @@ -0,0 +1,889 @@ +use { + crate::{ + AnyAccelerationStructureNode, AnyResource, Node, + driver::{ + accel_struct::{ + AccelerationStructureGeometry, AccelerationStructureGeometryInfo, + DeviceOrHostAddress, + }, + device::Device, + }, + }, + ash::vk, + log::trace, + std::{cell::RefCell, ops::Deref}, +}; + +#[cfg(debug_assertions)] +use crate::Execution; + +/// Recording interface for general Vulkan commands. +/// +/// This structure provides a strongly-typed set of methods which allow acceleration structures to +/// be built and updated. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ```no_run +/// # use ash::vk; +/// # use vk_graph::Graph; +/// # fn main() { +/// # let mut my_graph = Graph::default(); +/// my_graph.begin_cmd() +/// .record_cmd(move |cmd| { +/// // Use provided command buffer functions or native calls +/// assert_ne!(cmd.handle, vk::CommandBuffer::null()); +/// }); +/// # } +/// ``` +#[derive(Clone, Copy)] +pub struct CommandRef<'a> { + cmd: &'a crate::driver::cmd_buf::CommandBuffer, + + #[cfg(debug_assertions)] + exec: &'a Execution, + + resources: &'a [AnyResource], +} + +impl<'a> CommandRef<'a> { + pub(crate) fn new( + cmd: &'a crate::driver::cmd_buf::CommandBuffer, + resources: &'a [AnyResource], + #[cfg(debug_assertions)] exec: &'a Execution, + ) -> Self { + Self { + cmd, + #[cfg(debug_assertions)] + exec, + resources, + } + } + + /// Build acceleration structures. + /// + /// There is no ordering or synchronization implied between any of the individual acceleration + /// structure builds. + /// + /// Requires a scratch buffer which was created with the following requirements: + /// + /// - Flags must include [`vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS`] + /// - Size must be equal to or greater than the `build_size` value returned by + /// `AccelerationStructure::size_of`, aligned to `min_accel_struct_scratch_offset_alignment` + /// of `PhysicalDevice::accel_struct_properties`. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// # use ash::vk; + /// # use vk_graph::cmd::BuildAccelerationStructureInfo; + /// # use vk_graph::driver::{sync::AccessType, DriverError}; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::accel_struct::{ + /// # AccelerationStructure, + /// # AccelerationStructureGeometry, + /// # AccelerationStructureGeometryData, + /// # AccelerationStructureGeometryInfo, + /// # AccelerationStructureInfo, + /// # DeviceOrHostAddress, + /// # }; + /// # use vk_graph::driver::buffer::{Buffer, BufferInfo}; + /// # use vk_graph::Graph; + /// # use vk_graph::driver::shader::Shader; + /// # fn main() -> Result<(), DriverError> { + /// # let device = Device::new(DeviceInfo::default())?; + /// # let mut my_graph = Graph::default(); + /// # let info = AccelerationStructureInfo::blas(1); + /// # let blas_accel_struct = AccelerationStructure::create(&device, info)?; + /// # let blas_node = my_graph.bind_resource(blas_accel_struct); + /// # let scratch_buf_info = + /// # BufferInfo::device_mem(8, vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS); + /// # let scratch_buf = Buffer::create(&device, scratch_buf_info)?; + /// # let scratch_buf = my_graph.bind_resource(scratch_buf); + /// # let buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::INDEX_BUFFER); + /// # let my_idx_buf = Buffer::create(&device, buf_info)?; + /// # let buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::VERTEX_BUFFER); + /// # let my_vtx_buf = Buffer::create(&device, buf_info)?; + /// # let index_buf = my_graph.bind_resource(my_idx_buf); + /// # let vertex_buf = my_graph.bind_resource(my_vtx_buf); + /// my_graph.begin_cmd() + /// .resource_access(index_buf, AccessType::IndexBuffer) + /// .resource_access(vertex_buf, AccessType::VertexBuffer) + /// .resource_access(scratch_buf, AccessType::AccelerationStructureBufferWrite) + /// .resource_access(blas_node, AccessType::AccelerationStructureBuildWrite) + /// .record_cmd(move |cmd| { + /// let scratch_addr = cmd.resource(scratch_buf).device_address(); + /// let geom = AccelerationStructureGeometry { + /// max_primitive_count: 64, + /// flags: vk::GeometryFlagsKHR::OPAQUE, + /// geometry: AccelerationStructureGeometryData::Triangles { + /// index_addr: DeviceOrHostAddress::DeviceAddress( + /// cmd.resource(index_buf).device_address() + /// ), + /// index_type: vk::IndexType::UINT32, + /// max_vertex: 42, + /// transform_addr: None, + /// vertex_addr: DeviceOrHostAddress::DeviceAddress( + /// cmd.resource(vertex_buf).device_address(), + /// ), + /// vertex_format: vk::Format::R32G32B32_SFLOAT, + /// vertex_stride: 12, + /// }, + /// }; + /// let build_range = vk::AccelerationStructureBuildRangeInfoKHR { + /// first_vertex: 0, + /// primitive_count: 1, + /// primitive_offset: 0, + /// transform_offset: 0, + /// }; + /// let info = AccelerationStructureGeometryInfo::blas([(geom, build_range)]); + /// + /// cmd.build_accel_struct(&[ + /// BuildAccelerationStructureInfo::new(blas_node, scratch_addr, info) + /// ]); + /// }); + /// # Ok(()) } + /// ``` + /// + /// See also: + /// + /// - [`examples/ray_omni.rs`](/examples/ray_omni.rs) + /// - [`examples/ray_trace.rs`](/examples/ray_trace.rs) + /// - [`examples/rt_triangle.rs`](/examples/rt_triangle.rs) + pub fn build_accel_struct(&self, infos: &[BuildAccelerationStructureInfo]) -> &Self { + #[derive(Default)] + struct Tls { + geometries: Vec>, + ranges: Vec, + } + + thread_local! { + static TLS: RefCell = Default::default(); + } + + TLS.with_borrow_mut(|tls| { + tls.geometries.clear(); + tls.geometries.extend(infos.iter().flat_map(|info| { + info.build_data.geometries.iter().map(|(geometry, _)| { + <&AccelerationStructureGeometry as Into< + vk::AccelerationStructureGeometryKHR, + >>::into(geometry) + }) + })); + + tls.ranges.clear(); + tls.ranges.extend( + infos + .iter() + .flat_map(|info| info.build_data.geometries.iter().map(|(_, range)| *range)), + ); + + let vk_ranges = { + let mut start = 0; + let mut vk_ranges = Vec::with_capacity(infos.len()); + for info in infos { + let end = start + info.build_data.geometries.len(); + vk_ranges.push(&tls.ranges[start..end]); + start = end; + } + + vk_ranges + }; + + let vk_infos = { + let mut start = 0; + let mut vk_infos = Vec::with_capacity(infos.len()); + for info in infos { + let end = start + info.build_data.geometries.len(); + vk_infos.push( + vk::AccelerationStructureBuildGeometryInfoKHR::default() + .ty(info.build_data.ty) + .flags(info.build_data.flags) + .mode(vk::BuildAccelerationStructureModeKHR::BUILD) + .dst_acceleration_structure(self.resource(info.accel_struct).handle) + .geometries(&tls.geometries[start..end]) + .scratch_data(info.scratch_addr.into()), + ); + start = end; + } + + vk_infos + }; + + unsafe { + Device::expect_accel_struct_ext(&self.cmd.device) + .cmd_build_acceleration_structures(self.cmd.handle, &vk_infos, &vk_ranges); + } + }); + + self + } + + /// Builds acceleration structures with some parameters provided on the device. + /// + /// There is no ordering or synchronization implied between any of the individual acceleration + /// structure builds. + /// + /// Each [`BuildAccelerationStructureIndirectInfo::range_base`] is a buffer device address which + /// points to an array of [`vk::AccelerationStructureBuildRangeInfoKHR`] structures defining + /// dynamic offsets to the addresses where geometry data is stored. + pub fn build_accel_struct_indirect( + &self, + infos: &[BuildAccelerationStructureIndirectInfo], + ) -> &Self { + #[derive(Default)] + struct Tls { + geometries: Vec>, + max_primitive_counts: Vec, + range_bases: Vec, + range_strides: Vec, + } + + thread_local! { + static TLS: RefCell = Default::default(); + } + + TLS.with_borrow_mut(|tls| { + tls.geometries.clear(); + tls.geometries.extend(infos.iter().flat_map(|info| { + info.build_data.geometries.iter().map( + <&AccelerationStructureGeometry as Into< + vk::AccelerationStructureGeometryKHR, + >>::into, + ) + })); + + tls.max_primitive_counts.clear(); + tls.max_primitive_counts + .extend(infos.iter().flat_map(|info| { + info.build_data + .geometries + .iter() + .map(|geometry| geometry.max_primitive_count) + })); + + tls.range_bases.clear(); + tls.range_strides.clear(); + let (vk_infos, vk_max_primitive_counts) = { + let mut start = 0; + let mut vk_infos = Vec::with_capacity(infos.len()); + let mut vk_max_primitive_counts = Vec::with_capacity(infos.len()); + for info in infos { + let end = start + info.build_data.geometries.len(); + vk_infos.push( + vk::AccelerationStructureBuildGeometryInfoKHR::default() + .ty(info.build_data.ty) + .flags(info.build_data.flags) + .mode(vk::BuildAccelerationStructureModeKHR::BUILD) + .dst_acceleration_structure(self.resource(info.accel_struct).handle) + .geometries(&tls.geometries[start..end]) + .scratch_data(info.scratch_data.into()), + ); + vk_max_primitive_counts.push(&tls.max_primitive_counts[start..end]); + start = end; + + tls.range_bases.push(info.range_base); + tls.range_strides.push(info.range_stride); + } + + (vk_infos, vk_max_primitive_counts) + }; + + unsafe { + Device::expect_accel_struct_ext(&self.cmd.device) + .cmd_build_acceleration_structures_indirect( + self.cmd.handle, + &vk_infos, + &tls.range_bases, + &tls.range_strides, + &vk_max_primitive_counts, + ); + } + }); + + self + } + + pub(crate) fn cmd_push_constants( + &self, + layout: vk::PipelineLayout, + push_consts: &[vk::PushConstantRange], + offset: u32, + data: &[u8], + ) { + for push_const in push_consts { + let push_const_end = push_const.offset + push_const.size; + let data_end = offset + data.len() as u32; + let end = data_end.min(push_const_end); + let start = offset.max(push_const.offset); + + if end > start { + trace!( + " push constants {:?} {}..{}", + push_const.stage_flags, start, end + ); + + unsafe { + self.device.cmd_push_constants( + self.handle, + layout, + push_const.stage_flags, + start, + &data[(start - offset) as usize..(end - offset) as usize], + ); + } + } + } + } + + /// Update acceleration structures. + /// + /// There is no ordering or synchronization implied between any of the individual acceleration + /// structure updates. + /// + /// Requires a scratch buffer which was created with the following requirements: + /// + /// - Flags must include [`vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS`] + /// - Size must be equal to or greater than the `update_size` value returned by + /// `AccelerationStructure::size_of`, aligned to `min_accel_struct_scratch_offset_alignment` + /// of `PhysicalDevice::accel_struct_properties`. + pub fn update_accel_struct(&self, infos: &[UpdateAccelerationStructureInfo]) -> &Self { + #[derive(Default)] + struct Tls { + geometries: Vec>, + ranges: Vec, + } + + thread_local! { + static TLS: RefCell = Default::default(); + } + + TLS.with_borrow_mut(|tls| { + tls.geometries.clear(); + tls.geometries.extend(infos.iter().flat_map(|info| { + info.update_data.geometries.iter().map(|(geometry, _)| { + <&AccelerationStructureGeometry as Into< + vk::AccelerationStructureGeometryKHR, + >>::into(geometry) + }) + })); + + tls.ranges.clear(); + tls.ranges.extend( + infos + .iter() + .flat_map(|info| info.update_data.geometries.iter().map(|(_, range)| *range)), + ); + + let vk_ranges = { + let mut start = 0; + let mut vk_ranges = Vec::with_capacity(infos.len()); + for info in infos { + let end = start + info.update_data.geometries.len(); + vk_ranges.push(&tls.ranges[start..end]); + start = end; + } + + vk_ranges + }; + + let vk_infos = { + let mut start = 0; + let mut vk_infos = Vec::with_capacity(infos.len()); + for info in infos { + let end = start + info.update_data.geometries.len(); + vk_infos.push( + vk::AccelerationStructureBuildGeometryInfoKHR::default() + .ty(info.update_data.ty) + .flags(info.update_data.flags) + .mode(vk::BuildAccelerationStructureModeKHR::UPDATE) + .dst_acceleration_structure(self.resource(info.dst_accel_struct).handle) + .src_acceleration_structure(self.resource(info.src_accel_struct).handle) + .geometries(&tls.geometries[start..end]) + .scratch_data(info.scratch_addr.into()), + ); + start = end; + } + + vk_infos + }; + + unsafe { + Device::expect_accel_struct_ext(&self.cmd.device) + .cmd_build_acceleration_structures(self.cmd.handle, &vk_infos, &vk_ranges); + } + }); + + self + } + + /// Updates acceleration structures with some parameters provided on the device. + /// + /// There is no ordering or synchronization implied between any of the individual acceleration + /// structure updates. + /// + /// Each [`UpdateAccelerationStructureIndirectInfo::range_base`] is a buffer device address + /// which points to an array of [`vk::AccelerationStructureBuildRangeInfoKHR`] structures + /// defining dynamic offsets to the addresses where geometry data is stored. + pub fn update_accel_struct_indirect( + &self, + infos: &[UpdateAccelerationStructureIndirectInfo], + ) -> &Self { + #[derive(Default)] + struct Tls { + geometries: Vec>, + max_primitive_counts: Vec, + range_bases: Vec, + range_strides: Vec, + } + + thread_local! { + static TLS: RefCell = Default::default(); + } + + TLS.with_borrow_mut(|tls| { + tls.geometries.clear(); + tls.geometries.extend(infos.iter().flat_map(|info| { + info.update_data.geometries.iter().map( + <&AccelerationStructureGeometry as Into< + vk::AccelerationStructureGeometryKHR, + >>::into, + ) + })); + + tls.max_primitive_counts.clear(); + tls.max_primitive_counts + .extend(infos.iter().flat_map(|info| { + info.update_data + .geometries + .iter() + .map(|geometry| geometry.max_primitive_count) + })); + + tls.range_bases.clear(); + tls.range_strides.clear(); + let (vk_infos, vk_max_primitive_counts) = { + let mut start = 0; + let mut vk_infos = Vec::with_capacity(infos.len()); + let mut vk_max_primitive_counts = Vec::with_capacity(infos.len()); + for info in infos { + let end = start + info.update_data.geometries.len(); + vk_infos.push( + vk::AccelerationStructureBuildGeometryInfoKHR::default() + .ty(info.update_data.ty) + .flags(info.update_data.flags) + .mode(vk::BuildAccelerationStructureModeKHR::UPDATE) + .src_acceleration_structure(self.resource(info.src_accel_struct).handle) + .dst_acceleration_structure(self.resource(info.dst_accel_struct).handle) + .geometries(&tls.geometries[start..end]) + .scratch_data(info.scratch_addr.into()), + ); + vk_max_primitive_counts.push(&tls.max_primitive_counts[start..end]); + start = end; + + tls.range_bases.push(info.range_base); + tls.range_strides.push(info.range_stride); + } + + (vk_infos, vk_max_primitive_counts) + }; + + unsafe { + Device::expect_accel_struct_ext(&self.cmd.device) + .cmd_build_acceleration_structures_indirect( + self.cmd.handle, + &vk_infos, + &tls.range_bases, + &tls.range_strides, + &vk_max_primitive_counts, + ); + } + }); + + self + } + + /// Returns a borrow of the original Vulkan resource (buffer, image or acceleration structure) + /// which the given bound resource node represents. + pub fn resource(&self, resource_node: N) -> &N::Resource + where + N: Node, + { + // You must have called an access function for this node on this execution before borrowing + // the resource! + // + // Why: Code that attempts to access this function is attempting to get access to the Vulkan + // resource (buffer, image, or acceleration structure). In order to access any resources the + // access type must first be specified so the correct barriers may be added. + // + // See: https://attackgoat.github.io/vk-graph/pipeline_sync.html + #[cfg(debug_assertions)] + assert!( + self.exec.accesses.contains_key(&resource_node.index()), + "unexpected node access: call an access function first" + ); + + resource_node.borrow(self.resources) + } +} + +impl<'a> Deref for CommandRef<'a> { + type Target = crate::driver::cmd_buf::CommandBuffer; + + fn deref(&self) -> &Self::Target { + self.cmd + } +} + +/// Specifies the information and data used to build an acceleration structure. +/// +/// See [`vkCmdBuildAccelerationStructuresKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdBuildAccelerationStructuresKHR.html). +#[derive(Clone, Debug)] +pub struct BuildAccelerationStructureInfo { + /// The acceleration structure to be written. + pub accel_struct: AnyAccelerationStructureNode, + + /// Specifies the geometry data to use when building the acceleration structure. + pub build_data: AccelerationStructureGeometryInfo<( + AccelerationStructureGeometry, + vk::AccelerationStructureBuildRangeInfoKHR, + )>, + + /// The temporary buffer or host address (with enough capacity per + /// `AccelerationStructure::size_of`). + pub scratch_addr: DeviceOrHostAddress, +} + +impl BuildAccelerationStructureInfo { + /// Constructs new acceleration structure build information. + pub fn new( + accel_struct: impl Into, + scratch_addr: impl Into, + build_data: AccelerationStructureGeometryInfo<( + AccelerationStructureGeometry, + vk::AccelerationStructureBuildRangeInfoKHR, + )>, + ) -> Self { + let accel_struct = accel_struct.into(); + let scratch_addr = scratch_addr.into(); + + Self { + accel_struct, + build_data, + scratch_addr, + } + } +} + +/// Specifies the information and data used to build an acceleration structure with some parameters +/// sourced on the device. +/// +/// See [`vkCmdBuildAccelerationStructuresKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdBuildAccelerationStructuresKHR.html). +#[derive(Clone, Debug)] +pub struct BuildAccelerationStructureIndirectInfo { + /// The acceleration structure to be written. + pub accel_struct: AnyAccelerationStructureNode, + + /// Specifies the geometry data to use when building the acceleration structure. + pub build_data: AccelerationStructureGeometryInfo, + + /// A buffer device addresses which points to `data.geometry.len()` + /// [vk::AccelerationStructureBuildRangeInfoKHR] structures defining dynamic offsets to the + /// addresses where geometry data is stored. + pub range_base: vk::DeviceAddress, + + /// Byte stride between elements of [`Self::range_base`]. + pub range_stride: u32, + + /// The temporary buffer or host address (with enough capacity per + /// `AccelerationStructure::size_of`). + pub scratch_data: DeviceOrHostAddress, +} + +impl BuildAccelerationStructureIndirectInfo { + /// Constructs new acceleration structure indirect build information. + pub fn new( + accel_struct: impl Into, + scratch_data: impl Into, + build_data: AccelerationStructureGeometryInfo, + range_base: vk::DeviceAddress, + range_stride: u32, + ) -> Self { + let accel_struct = accel_struct.into(); + let scratch_data = scratch_data.into(); + + Self { + accel_struct, + build_data, + range_base, + range_stride, + scratch_data, + } + } +} + +/// Specifies the information and data used to update an acceleration structure with some parameters +/// sourced on the device. +/// +/// See [`vkCmdBuildAccelerationStructuresKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdBuildAccelerationStructuresKHR.html). +#[derive(Clone, Debug)] +pub struct UpdateAccelerationStructureIndirectInfo { + /// The acceleration structure to be written. + pub dst_accel_struct: AnyAccelerationStructureNode, + + /// A buffer device addresses which points to `data.geometry.len()` + /// [vk::AccelerationStructureBuildRangeInfoKHR] structures defining dynamic offsets to the + /// addresses where geometry data is stored. + pub range_base: vk::DeviceAddress, + + /// Byte stride between elements of [`Self::range_base`]. + pub range_stride: u32, + + /// The temporary buffer or host address (with enough capacity per + /// `AccelerationStructure::size_of`). + pub scratch_addr: DeviceOrHostAddress, + + /// The source acceleration structure to be read. + pub src_accel_struct: AnyAccelerationStructureNode, + + /// Specifies the geometry data to use when building the acceleration structure. + pub update_data: AccelerationStructureGeometryInfo, +} + +impl UpdateAccelerationStructureIndirectInfo { + /// Constructs new acceleration structure indirect update information. + pub fn new( + src_accel_struct: impl Into, + dst_accel_struct: impl Into, + scratch_addr: impl Into, + update_data: AccelerationStructureGeometryInfo, + range_base: vk::DeviceAddress, + range_stride: u32, + ) -> Self { + let src_accel_struct = src_accel_struct.into(); + let dst_accel_struct = dst_accel_struct.into(); + let scratch_addr = scratch_addr.into(); + + Self { + dst_accel_struct, + range_base, + range_stride, + scratch_addr, + src_accel_struct, + update_data, + } + } +} + +/// Specifies the information and data used to update an acceleration structure. +#[derive(Clone, Debug)] +pub struct UpdateAccelerationStructureInfo { + /// The acceleration structure to be written. + pub dst_accel_struct: AnyAccelerationStructureNode, + + /// The temporary buffer or host address (with enough capacity per + /// `AccelerationStructure::size_of`). + pub scratch_addr: DeviceOrHostAddress, + + /// The source acceleration structure to be read. + pub src_accel_struct: AnyAccelerationStructureNode, + + /// Specifies the geometry data to use when updating the acceleration structure. + pub update_data: AccelerationStructureGeometryInfo<( + AccelerationStructureGeometry, + vk::AccelerationStructureBuildRangeInfoKHR, + )>, +} + +impl UpdateAccelerationStructureInfo { + /// Constructs new acceleration structure update information. + pub fn new( + src_accel_struct: impl Into, + dst_accel_struct: impl Into, + scratch_addr: impl Into, + update_data: AccelerationStructureGeometryInfo<( + AccelerationStructureGeometry, + vk::AccelerationStructureBuildRangeInfoKHR, + )>, + ) -> Self { + let src_accel_struct = src_accel_struct.into(); + let dst_accel_struct = dst_accel_struct.into(); + let scratch_addr = scratch_addr.into(); + + Self { + dst_accel_struct, + scratch_addr, + src_accel_struct, + update_data, + } + } +} + +#[allow(missing_docs)] +#[allow(deprecated)] +mod deprecated { + use ash::vk; + + use crate::{ + cmd::{ + BuildAccelerationStructureIndirectInfo, BuildAccelerationStructureInfo, CommandRef, + UpdateAccelerationStructureIndirectInfo, UpdateAccelerationStructureInfo, + }, + driver::accel_struct::{ + AccelerationStructureGeometry, AccelerationStructureGeometryInfo, DeviceOrHostAddress, + }, + graph::pass_ref::{ + AccelerationStructureBuildInfo, AccelerationStructureIndirectBuildInfo, + AccelerationStructureIndirectUpdateInfo, AccelerationStructureUpdateInfo, + }, + node::AnyAccelerationStructureNode, + }; + + impl<'a> CommandRef<'a> { + #[deprecated = "use build_accel_struct function"] + #[doc(hidden)] + pub fn build_structure( + &self, + info: &AccelerationStructureGeometryInfo<( + AccelerationStructureGeometry, + vk::AccelerationStructureBuildRangeInfoKHR, + )>, + accel_struct: impl Into, + scratch_addr: impl Into, + ) -> &Self { + self.build_accel_struct(&[BuildAccelerationStructureInfo { + accel_struct: accel_struct.into(), + build_data: info.clone(), + scratch_addr: scratch_addr.into(), + }]) + } + + #[deprecated = "use build_accel_struct_indirect function"] + #[doc(hidden)] + pub fn build_structure_indirect( + &self, + info: &AccelerationStructureGeometryInfo, + accel_struct: impl Into, + scratch_addr: impl Into, + range_base: vk::DeviceAddress, + range_stride: u32, + ) -> &Self { + self.build_accel_struct_indirect(&[BuildAccelerationStructureIndirectInfo { + accel_struct: accel_struct.into(), + build_data: info.clone(), + range_base, + range_stride, + scratch_data: scratch_addr.into(), + }]) + } + + #[deprecated = "use build_accel_struct function"] + #[doc(hidden)] + pub fn build_structures(&self, infos: &[AccelerationStructureBuildInfo]) -> &Self { + for info in infos { + self.build_structure(&info.build_data, info.accel_struct, info.scratch_addr); + } + + self + } + + #[deprecated = "use build_accel_struct_indirect function"] + #[doc(hidden)] + pub fn build_structures_indirect( + &self, + infos: &[AccelerationStructureIndirectBuildInfo], + ) -> &Self { + for info in infos { + self.build_structure_indirect( + &info.build_data, + info.accel_struct, + info.scratch_data, + info.range_base, + info.range_stride, + ); + } + + self + } + + #[deprecated = "use update_accel_struct function"] + #[doc(hidden)] + pub fn update_structure( + &self, + info: &AccelerationStructureGeometryInfo<( + AccelerationStructureGeometry, + vk::AccelerationStructureBuildRangeInfoKHR, + )>, + src_accel_struct: impl Into, + dst_accel_struct: impl Into, + scratch_addr: impl Into, + ) -> &Self { + self.update_accel_struct(&[UpdateAccelerationStructureInfo { + src_accel_struct: src_accel_struct.into(), + dst_accel_struct: dst_accel_struct.into(), + update_data: info.clone(), + scratch_addr: scratch_addr.into(), + }]) + } + + #[deprecated = "use update_accel_struct_indirect function"] + #[doc(hidden)] + pub fn update_structure_indirect( + &self, + info: &AccelerationStructureGeometryInfo, + src_accel_struct: impl Into, + dst_accel_struct: impl Into, + scratch_addr: impl Into, + range_base: vk::DeviceAddress, + range_stride: u32, + ) -> &Self { + self.update_accel_struct_indirect(&[UpdateAccelerationStructureIndirectInfo { + src_accel_struct: src_accel_struct.into(), + dst_accel_struct: dst_accel_struct.into(), + update_data: info.clone(), + range_base, + range_stride, + scratch_addr: scratch_addr.into(), + }]) + } + + #[deprecated = "use update_accel_struct function"] + #[doc(hidden)] + pub fn update_structures(&self, infos: &[AccelerationStructureUpdateInfo]) -> &Self { + for info in infos { + self.update_structure( + &info.update_data, + info.src_accel_struct, + info.dst_accel_struct, + info.scratch_addr, + ); + } + + self + } + + #[deprecated = "use update_accel_struct_indirect function"] + #[doc(hidden)] + pub fn update_structures_indirect( + &self, + infos: &[AccelerationStructureIndirectUpdateInfo], + ) -> &Self { + for info in infos { + self.update_structure_indirect( + &info.update_data, + info.src_accel_struct, + info.dst_accel_struct, + info.scratch_addr, + info.range_base, + info.range_stride, + ); + } + + self + } + } +} diff --git a/src/cmd/compute.rs b/src/cmd/compute.rs new file mode 100644 index 00000000..58da3730 --- /dev/null +++ b/src/cmd/compute.rs @@ -0,0 +1,430 @@ +use { + super::{cmd_ref::CommandRef, pipeline::PipelineCommand}, + crate::{driver::compute::ComputePipeline, node::AnyBufferNode}, + ash::vk, + std::ops::Deref, +}; + +impl PipelineCommand<'_, ComputePipeline> { + /// Begin recording a compute pipeline command buffer. + pub fn record_cmd(mut self, func: impl FnOnce(ComputeCommandRef<'_>) + Send + 'static) -> Self { + self.record_cmd_mut(func); + self + } + + /// Begin recording a compute pipeline command buffer. + pub fn record_cmd_mut(&mut self, func: impl FnOnce(ComputeCommandRef<'_>) + Send + 'static) { + let pipeline = self + .cmd + .cmd() + .expect_last_pipeline() + .expect_compute() + .clone(); + + self.cmd.push_exec(move |cmd| { + func(ComputeCommandRef { cmd, pipeline }); + }); + } +} + +/// Recording interface for computing commands. +/// +/// This structure provides a strongly-typed set of methods which allow compute shader code to be +/// executed. An instance is provided to the closure argument of +/// [`PipelineCommand::record_cmd`] which may be accessed by binding a [`ComputePipeline`] to a +/// command. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ```no_run +/// # use ash::vk; +/// # use vk_graph::driver::DriverError; +/// # use vk_graph::driver::device::{Device, DeviceInfo}; +/// # use vk_graph::driver::compute::{ComputePipeline, ComputePipelineInfo}; +/// # use vk_graph::driver::shader::{Shader}; +/// # use vk_graph::Graph; +/// # fn main() -> Result<(), DriverError> { +/// # let device = Device::new(DeviceInfo::default())?; +/// # let info = ComputePipelineInfo::default(); +/// # let shader = Shader::new_compute([0u8; 1].as_slice()); +/// # let my_compute_pipeline = ComputePipeline::create(&device, info, shader)?; +/// # let mut my_graph = Graph::default(); +/// my_graph +/// .begin_cmd() +/// .bind_pipeline(&my_compute_pipeline) +/// .record_cmd(move |cmd| { +/// // During this closure we have access to the compute functions! +/// cmd.dispatch(64, 1, 1); +/// }); +/// # Ok(()) } +/// ``` +pub struct ComputeCommandRef<'a> { + cmd: CommandRef<'a>, + pipeline: ComputePipeline, +} + +impl ComputeCommandRef<'_> { + /// [`Self::dispatch`] compute work items. + /// + /// When the command is executed, a global workgroup consisting of + /// `group_count_x × group_count_y × group_count_z` local workgroups is assembled. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// # vk_shader_macros::glsl!(r#" + /// #version 450 + /// #pragma shader_stage(compute) + /// + /// layout(set = 0, binding = 0, std430) restrict writeonly buffer MyBufer { + /// uint my_buf[]; + /// }; + /// + /// void main() { + /// // TODO + /// } + /// # "#); + /// ``` + /// + /// ```no_run + /// # use ash::vk; + /// # use vk_graph::driver::{AccessType, DriverError}; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::buffer::{Buffer, BufferInfo}; + /// # use vk_graph::driver::compute::{ComputePipeline, ComputePipelineInfo}; + /// # use vk_graph::driver::shader::{Shader}; + /// # use vk_graph::Graph; + /// # fn main() -> Result<(), DriverError> { + /// # let device = Device::new(DeviceInfo::default())?; + /// # let buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::STORAGE_BUFFER); + /// # let my_buf = Buffer::create(&device, buf_info)?; + /// # let info = ComputePipelineInfo::default(); + /// # let shader = Shader::new_compute([0u8; 1].as_slice()); + /// # let my_compute_pipeline = ComputePipeline::create(&device, info, shader)?; + /// # let mut my_graph = Graph::default(); + /// # let my_buf_node = my_graph.bind_resource(my_buf); + /// my_graph + /// .begin_cmd() + /// .debug_name("fill my_buf_node with data") + /// .bind_pipeline(&my_compute_pipeline) + /// .shader_resource_access(0, my_buf_node, AccessType::ComputeShaderWrite) + /// .record_cmd(move |cmd| { + /// cmd.dispatch(128, 64, 32); + /// }); + /// # Ok(()) } + /// ``` + /// + /// See [`vkCmdDispatch`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdDispatch.html). + #[profiling::function] + pub fn dispatch(&self, group_count_x: u32, group_count_y: u32, group_count_z: u32) -> &Self { + unsafe { + self.cmd.device.cmd_dispatch( + self.cmd.handle, + group_count_x, + group_count_y, + group_count_z, + ); + } + + self + } + + /// [`Self::dispatch_base`] compute work items with non-zero base values for the workgroup IDs. + /// + /// When the command is executed, a global workgroup consisting of + /// `group_count_x × group_count_y × group_count_z` local workgroups is assembled, with + /// WorkgroupId values ranging from `[base_group*, base_group* + group_count*)` in each + /// component. + /// + /// [`Self::dispatch`] is equivalent to + /// `dispatch_base(0, 0, 0, group_count_x, group_count_y, group_count_z)`. + /// + /// See [`vkCmdDispatchBase`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdDispatchBase.html). + #[profiling::function] + pub fn dispatch_base( + &self, + base_group_x: u32, + base_group_y: u32, + base_group_z: u32, + group_count_x: u32, + group_count_y: u32, + group_count_z: u32, + ) -> &Self { + unsafe { + self.cmd.device.cmd_dispatch_base( + self.cmd.handle, + base_group_x, + base_group_y, + base_group_z, + group_count_x, + group_count_y, + group_count_z, + ); + } + + self + } + + /// Dispatch compute work items with indirect parameters. + /// + /// `dispatch_indirect` behaves similarly to [`Self::dispatch`] except that the parameters + /// are read by the device from `args_buf` during execution. The parameters of the dispatch are + /// encoded in a [`vk::DispatchIndirectCommand`] structure taken from `args_buf` starting at + /// `args_offset`. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// # use ash::vk; + /// # use bytemuck::{bytes_of, Pod, Zeroable}; + /// # use vk_graph::driver::{AccessType, DriverError}; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::buffer::{Buffer, BufferInfo}; + /// # use vk_graph::driver::compute::{ComputePipeline, ComputePipelineInfo}; + /// # use vk_graph::driver::shader::{Shader}; + /// # use vk_graph::Graph; + /// # fn main() -> Result<(), DriverError> { + /// # let device = Device::new(DeviceInfo::default())?; + /// # let buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::STORAGE_BUFFER); + /// # let my_buf = Buffer::create(&device, buf_info)?; + /// # let info = ComputePipelineInfo::default(); + /// # let shader = Shader::new_compute([0u8; 1].as_slice()); + /// # let my_compute_pipeline = ComputePipeline::create(&device, info, shader)?; + /// # let mut my_graph = Graph::default(); + /// # let my_buf_node = my_graph.bind_resource(my_buf); + /// # #[repr(C)] + /// # #[derive(Clone, Copy, Pod, Zeroable)] + /// # struct DispatchIndirectCommand { x: u32, y: u32, z: u32, } + /// let args = DispatchIndirectCommand { + /// x: 1, + /// y: 2, + /// z: 3, + /// }; + /// let data = bytes_of(&args); + /// let usage = vk::BufferUsageFlags::INDIRECT_BUFFER | vk::BufferUsageFlags::STORAGE_BUFFER; + /// let args_buf = Buffer::create_from_slice(&device, usage, data)?; + /// let args_buf = my_graph.bind_resource(args_buf); + /// + /// my_graph + /// .begin_cmd() + /// .debug_name("fill my_buf_node with data") + /// .bind_pipeline(&my_compute_pipeline) + /// .resource_access(args_buf, AccessType::IndirectBuffer) + /// .shader_resource_access(0, my_buf_node, AccessType::ComputeShaderWrite) + /// .record_cmd(move |cmd| { + /// cmd.dispatch_indirect(args_buf, 0); + /// }); + /// # Ok(()) } + /// ``` + /// + /// See [`vkCmdDispatchIndirect`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdDispatchIndirect.html). + #[profiling::function] + pub fn dispatch_indirect( + &self, + args_buf: impl Into, + args_offset: vk::DeviceSize, + ) -> &Self { + let args_buf = args_buf.into(); + let args_buf = self.resource(args_buf); + + unsafe { + self.cmd + .device + .cmd_dispatch_indirect(self.cmd.handle, args_buf.handle, args_offset); + } + + self + } + + /// Updates push constants. + /// + /// Push constants represent a high speed path to modify constant data in pipelines that is + /// expected to outperform memory-backed resource updates. + /// + /// Push constant values can be updated incrementally, causing shader stages to read the new + /// data for push constants modified by this command, while still reading the previous data for + /// push constants not modified by this command. + /// + /// # Device limitations + /// + /// See + /// [`device.physical_device.props.limits.max_push_constants_size`](vk::PhysicalDeviceLimits) + /// for the limits of the current device. You may also check [gpuinfo.org] for a listing of + /// reported limits on other devices. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// # vk_shader_macros::glsl!(r#" + /// #version 450 + /// #pragma shader_stage(compute) + /// + /// layout(push_constant) uniform PushConstants { + /// layout(offset = 0) uint the_answer; + /// } push_constants; + /// + /// void main() + /// { + /// // TODO: Add bindings to read/write things! + /// } + /// # "#); + /// ``` + /// + /// ```no_run + /// # use ash::vk; + /// # use vk_graph::driver::DriverError; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::buffer::{Buffer, BufferInfo}; + /// # use vk_graph::driver::compute::{ComputePipeline, ComputePipelineInfo}; + /// # use vk_graph::driver::shader::{Shader}; + /// # use vk_graph::Graph; + /// # fn main() -> Result<(), DriverError> { + /// # let device = Device::new(DeviceInfo::default())?; + /// # let info = ComputePipelineInfo::default(); + /// # let shader = Shader::new_compute([0u8; 1].as_slice()); + /// # let my_compute_pipeline = ComputePipeline::create(&device, info, shader)?; + /// # let mut my_graph = Graph::default(); + /// my_graph + /// .begin_cmd() + /// .debug_name("compute the ultimate question") + /// .bind_pipeline(&my_compute_pipeline) + /// .record_cmd(move |cmd| { + /// cmd + /// .push_constants(0, &[42]) + /// .dispatch(1, 1, 1); + /// }); + /// # Ok(()) } + /// ``` + /// + /// See [`vkCmdPushConstants`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdPushConstants.html). + #[profiling::function] + pub fn push_constants(&self, offset: u32, data: &[u8]) -> &Self { + self.cmd_push_constants( + self.pipeline.inner.layout, + self.pipeline.inner.push_constants.as_slice(), + offset, + data, + ); + + self + } +} + +impl<'a> Deref for ComputeCommandRef<'a> { + type Target = CommandRef<'a>; + + fn deref(&self) -> &Self::Target { + &self.cmd + } +} + +#[allow(unused)] +mod deprecated { + use { + crate::{ + Node, + cmd::{ + Binding, PipelineCommand, Subresource, SubresourceRange, ViewInfo, + compute::ComputeCommandRef, + }, + driver::compute::ComputePipeline, + }, + std::any::Any, + vk_sync::AccessType, + }; + + impl ComputeCommandRef<'_> { + #[deprecated = "use push_constants function"] + #[doc(hidden)] + pub fn push_constants_offset(&self, offset: u32, data: &[u8]) -> &Self { + self.push_constants(offset, data) + } + } + + impl PipelineCommand<'_, ComputePipeline> { + #[deprecated = "use shader_resource_access with ComputeShaderReadOther"] + #[doc(hidden)] + pub fn read_descriptor(self, descriptor: impl Into, node: N) -> Self + where + N: Node + Subresource, + N::Info: Copy, + SubresourceRange: From, + ViewInfo: From, + { + self.shader_resource_access(descriptor, node, AccessType::ComputeShaderReadOther) + } + + #[deprecated = "use shader_subresource_access with ComputeShaderReadOther"] + #[doc(hidden)] + pub fn read_descriptor_as( + self, + descriptor: impl Into, + node: N, + node_view: impl Into, + ) -> Self + where + N: Node + Subresource, + N::Info: Copy, + SubresourceRange: From, + ViewInfo: From, + { + self.shader_subresource_access( + descriptor, + node, + node_view, + AccessType::ComputeShaderReadOther, + ) + } + + #[deprecated = "use record_cmd function"] + #[doc(hidden)] + pub fn record_compute( + self, + func: impl FnOnce(ComputeCommandRef<'_>, ()) + Send + 'static, + ) -> Self { + self.record_cmd(|cmd| func(cmd, ())) + } + + #[deprecated = "use shader_resource_access function with AccessType::ComputeShaderWrite"] + #[doc(hidden)] + pub fn write_descriptor(self, descriptor: impl Into, node: N) -> Self + where + N: Node + Subresource, + N::Info: Copy, + SubresourceRange: From, + ViewInfo: From, + { + self.shader_resource_access(descriptor, node, AccessType::ComputeShaderWrite) + } + + #[deprecated = "use shader_subresource_access function with AccessType::ComputeShaderWrite"] + #[doc(hidden)] + pub fn write_descriptor_as( + self, + descriptor: impl Into, + node: N, + node_view: impl Into, + ) -> Self + where + N: Node + Subresource, + N::Info: Copy, + SubresourceRange: From, + ViewInfo: From, + { + self.shader_subresource_access( + descriptor, + node, + node_view, + AccessType::ComputeShaderWrite, + ) + } + } +} diff --git a/src/cmd/graphic.rs b/src/cmd/graphic.rs new file mode 100644 index 00000000..2840ced3 --- /dev/null +++ b/src/cmd/graphic.rs @@ -0,0 +1,3044 @@ +use { + super::{AttachmentIndex, cmd_ref::CommandRef, pipeline::PipelineCommand}, + crate::{ + driver::{ + graphic::{DepthStencilInfo, GraphicPipeline}, + image::ImageViewInfo, + render_pass::ResolveMode, + }, + node::{AnyBufferNode, AnyImageNode}, + }, + ash::vk, + std::{cell::RefCell, ops::Deref, slice}, +}; + +impl PipelineCommand<'_, GraphicPipeline> { + /// Sets the `color_attachment` attachment index of the following render pass to the given + /// `image`. + /// + /// Note: To use multi-sampled (MSAA) rendering, use an image created with a sample count + /// greater than one. + /// + /// Note: The default view (the whole image) is used for `image`. + /// + /// See [_Render Pass_](https://docs.vulkan.org/spec/latest/chapters/renderpass.html) + pub fn color_attachment_image( + mut self, + color_attachment: AttachmentIndex, + image: impl Into, + load: LoadOp, + store: StoreOp, + ) -> Self { + self.set_color_attachment_image(color_attachment, image, load, store); + self + } + + /// Sets the `color_attachment` attachment index of the following render pass to the given + /// `image`, as interpreted by `image_view_info`. + /// + /// See [_Render Pass_](https://docs.vulkan.org/spec/latest/chapters/renderpass.html) + pub fn color_attachment_image_view( + mut self, + color_attachment: AttachmentIndex, + image: impl Into, + image_view_info: impl Into, + load: LoadOp, + store: StoreOp, + ) -> Self { + self.set_color_attachment_image_view(color_attachment, image, image_view_info, load, store); + self + } + + /// Resolves a multi-sampled (MSAA) color image attachment into a single-sampled attachment + /// using the given `image`. + /// + /// Note: The default view (the whole image) is used for `image`. + /// + /// See [_Render Pass_](https://docs.vulkan.org/spec/latest/chapters/renderpass.html) + pub fn color_attachment_resolve_image( + mut self, + msaa_attachment: AttachmentIndex, + color_attachment: AttachmentIndex, + image: impl Into, + ) -> Self { + self.set_color_attachment_resolve_image(msaa_attachment, color_attachment, image); + self + } + + /// Resolves a multi-sampled (MSAA) color image attachment into a single-sampled attachment + /// using the given `image`, as interpreted by `image_view_info`. + /// + /// See [_Render Pass_](https://docs.vulkan.org/spec/latest/chapters/renderpass.html) + pub fn color_attachment_resolve_image_view( + mut self, + msaa_attachment: AttachmentIndex, + color_attachment: AttachmentIndex, + image: impl Into, + image_view_info: impl Into, + ) -> Self { + self.set_color_attachment_resolve_image_view( + msaa_attachment, + color_attachment, + image, + image_view_info, + ); + self + } + + /// Sets the combined depth and stencil state used by any subsequent command buffer recordings + /// of the current graph command. + pub fn depth_stencil(mut self, depth_stencil: impl Into) -> Self { + self.set_depth_stencil(depth_stencil); + self + } + + /// Sets the combined depth and stencil attachment of the following render pass to the given + /// `image`. + /// + /// Note: To use multi-sampled (MSAA) rendering, use an image created with a sample count + /// greater than one. + /// + /// Note: The default view (the whole image) is used for `image`. + /// + /// See [_Render Pass_](https://docs.vulkan.org/spec/latest/chapters/renderpass.html) + pub fn depth_stencil_attachment_image( + mut self, + image: impl Into, + load: LoadOp, + store: StoreOp, + ) -> Self { + self.set_depth_stencil_attachment_image(image, load, store); + self + } + + /// Sets the combined depth and stencil attachment of the following render pass to the given + /// `image`, as interpreted by `image_view_info`. + /// + /// Note: To use multi-sampled (MSAA) rendering, use an image created with a sample count + /// greater than one. + /// + /// Note: The default view (the whole image) is used for `image`. + /// + /// See [_Render Pass_](https://docs.vulkan.org/spec/latest/chapters/renderpass.html) + pub fn depth_stencil_attachment_image_view( + mut self, + image: impl Into, + image_view_info: impl Into, + load: LoadOp, + store: StoreOp, + ) -> Self { + self.set_depth_stencil_attachment_image_view(image, image_view_info, load, store); + self + } + + /// Resolves a multi-sampled (MSAA) combined depth and stencil image attachment into a + /// single-sampled attachment using the given `image`. + /// + /// Note: The default view (the whole image) is used for `image`. + /// + /// See [_Render Pass_](https://docs.vulkan.org/spec/latest/chapters/renderpass.html) + pub fn depth_stencil_attachment_resolve_image( + mut self, + depth_stencil_attachment: AttachmentIndex, + image: impl Into, + depth_mode: Option, + stencil_mode: Option, + ) -> Self { + self.set_depth_stencil_attachment_resolve_image( + depth_stencil_attachment, + image, + depth_mode, + stencil_mode, + ); + self + } + + /// Resolves a multi-sampled (MSAA) combined depth and stencil image attachment into a + /// single-sampled attachment using the given `image`, as interpreted by `image_view_info`. + /// + /// Note: The default view (the whole image) is used for `image`. + /// + /// See [_Render Pass_](https://docs.vulkan.org/spec/latest/chapters/renderpass.html) + pub fn depth_stencil_attachment_resolve_image_view( + mut self, + depth_stencil_attachment: AttachmentIndex, + image: impl Into, + image_view_info: impl Into, + depth_mode: Option, + stencil_mode: Option, + ) -> Self { + self.set_depth_stencil_attachment_resolve_image_view( + depth_stencil_attachment, + image, + image_view_info, + depth_mode, + stencil_mode, + ); + self + } + + /// Sets multiview view and correlation masks used by any subsequent command buffer recordings + /// of the current graph command. + /// + /// See [`VkRenderPassMultiviewCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkRenderPassMultiviewCreateInfo.html). + pub fn multiview(mut self, view_mask: u32, correlated_view_mask: u32) -> Self { + self.set_multiview(view_mask, correlated_view_mask); + self + } + + /// Begin recording a graphics pipeline command buffer. + pub fn record_cmd(mut self, func: impl FnOnce(GraphicCommandRef<'_>) + Send + 'static) -> Self { + self.record_cmd_mut(func); + self + } + + /// Begin recording a graphics pipeline command buffer. + pub fn record_cmd_mut(&mut self, func: impl FnOnce(GraphicCommandRef<'_>) + Send + 'static) { + let pipeline = self + .cmd + .cmd() + .expect_last_pipeline() + .expect_graphic() + .clone(); + + self.cmd.push_exec(move |cmd| { + func(GraphicCommandRef { cmd, pipeline }); + }); + } + + /// See [`VkRenderPassBeginInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkRenderPassBeginInfo.html). + /// field when beginning a render pass used by any subsequent command buffer recordings + /// of the current graph command. + /// + /// _NOTE:_ Setting this value will cause the viewport and scissor to be unset, which is not the + /// default behavior. When this value is set you should call `set_viewport` and `set_scissor` on + /// the command buffer. + /// + /// If not set, this value defaults to the first loaded, resolved, or stored attachment + /// dimensions and sets the viewport and scissor to the same values, with a `0..1` depth if not + /// specified by `depth_stencil`. + pub fn render_area(mut self, area: vk::Rect2D) -> Self { + self.set_render_area(area); + self + } + + /// See [`Self::color_attachment_image`] + pub fn set_color_attachment_image( + &mut self, + color_attachment: AttachmentIndex, + image: impl Into, + load: LoadOp, + store: StoreOp, + ) -> &mut Self { + let image = image.into(); + let image_view = self.resource(image).info; + + self.set_color_attachment_image_view(color_attachment, image, image_view, load, store); + + self + } + + /// See [`Self::color_attachment_image_view`] + pub fn set_color_attachment_image_view( + &mut self, + color_attachment: AttachmentIndex, + image: impl Into, + image_view_info: impl Into, + load: LoadOp, + store: StoreOp, + ) -> &mut Self { + let image = image.into(); + let image_view_info = image_view_info.into(); + + #[allow(deprecated)] + { + match load { + LoadOp::Clear(color) => { + self.set_clear_color_value_as(color_attachment, image, color, image_view_info) + } + LoadOp::DontCare => { + self.set_attach_color_as(color_attachment, image, image_view_info) + } + LoadOp::Load => self.set_load_color_as(color_attachment, image, image_view_info), + }; + + if let StoreOp::Store = store { + self.set_store_color_as(color_attachment, image, image_view_info); + } + } + + self + } + + /// See [`Self::color_attachment_resolve_image`] + pub fn set_color_attachment_resolve_image( + &mut self, + msaa_attachment: AttachmentIndex, + color_attachment: AttachmentIndex, + image: impl Into, + ) -> &mut Self { + let image = image.into(); + let image_view = self.resource(image).info; + + self.set_color_attachment_resolve_image_view( + msaa_attachment, + color_attachment, + image, + image_view, + ); + + self + } + + /// See [`Self::color_attachment_resolve_image_view`] + pub fn set_color_attachment_resolve_image_view( + &mut self, + msaa_attachment: AttachmentIndex, + color_attachment: AttachmentIndex, + image: impl Into, + image_view_info: impl Into, + ) -> &mut Self { + let image = image.into(); + let image_view_info = image_view_info.into(); + + #[allow(deprecated)] + self.set_resolve_color_as(msaa_attachment, color_attachment, image, image_view_info); + + self + } + + /// See [`Self::depth_stencil`] + pub fn set_depth_stencil(&mut self, depth_stencil: impl Into) -> &mut Self { + let depth_stencil = depth_stencil.into(); + let cmd = self.cmd.cmd_mut(); + let exec = cmd.expect_last_exec_mut(); + + assert!(exec.depth_stencil.is_none()); + + exec.depth_stencil = Some(depth_stencil); + + self + } + + /// See [`Self::depth_stencil_attachment_image`] + pub fn set_depth_stencil_attachment_image( + &mut self, + image: impl Into, + load: LoadOp, + store: StoreOp, + ) -> &mut Self { + let image = image.into(); + let image_view_info = self.resource(image).info; + + self.set_depth_stencil_attachment_image_view(image, image_view_info, load, store); + + self + } + + /// See [`Self::depth_stencil_attachment_image_view`] + pub fn set_depth_stencil_attachment_image_view( + &mut self, + image: impl Into, + image_view_info: impl Into, + load: LoadOp, + store: StoreOp, + ) -> &mut Self { + let image = image.into(); + let image_view_info = image_view_info.into(); + + #[allow(deprecated)] + { + match load { + LoadOp::Clear(color) => self.set_clear_depth_stencil_value_as( + image, + color.depth, + color.stencil, + image_view_info, + ), + LoadOp::DontCare => self.set_attach_depth_stencil_as(image, image_view_info), + LoadOp::Load => self.set_load_depth_stencil_as(image, image_view_info), + }; + + if let StoreOp::Store = store { + self.set_store_depth_stencil_as(image, image_view_info); + } + } + + self + } + + /// See [`Self::depth_stencil_attachment_resolve_image`] + pub fn set_depth_stencil_attachment_resolve_image( + &mut self, + depth_stencil_attachment: AttachmentIndex, + image: impl Into, + depth_mode: Option, + stencil_mode: Option, + ) -> &mut Self { + let image = image.into(); + let image_view = self.resource(image).info; + + self.set_depth_stencil_attachment_resolve_image_view( + depth_stencil_attachment, + image, + image_view, + depth_mode, + stencil_mode, + ); + + self + } + + /// See [`Self::depth_stencil_attachment_resolve_image_view`] + pub fn set_depth_stencil_attachment_resolve_image_view( + &mut self, + depth_stencil_attachment: AttachmentIndex, + image: impl Into, + image_view_info: impl Into, + depth_mode: Option, + stencil_mode: Option, + ) -> &mut Self { + let image = image.into(); + let image_view_info = image_view_info.into(); + + #[allow(deprecated)] + self.set_resolve_depth_stencil_as( + depth_stencil_attachment, + image, + image_view_info, + depth_mode, + stencil_mode, + ); + + self + } + + /// See [`Self::multiview`] + pub fn set_multiview(&mut self, view_mask: u32, correlated_view_mask: u32) -> &mut Self { + let cmd = self.cmd.cmd_mut(); + let exec = cmd.expect_last_exec_mut(); + + exec.correlated_view_mask = correlated_view_mask; + exec.view_mask = view_mask; + + self + } + + /// See [`Self::render_area`] + pub fn set_render_area(&mut self, area: vk::Rect2D) -> &mut Self { + self.cmd.cmd_mut().expect_last_exec_mut().render_area = Some(area); + self + } +} + +/// Structure specifying a clear color value. +#[derive(Clone, Copy, Debug)] +pub enum ClearColorValue { + /// Value as [f32]. + /// + /// Use this member for color clear values when the format of the image or attachment is one of + /// the numeric formats with a numeric type that is floating-point. Floating-point values are + /// automatically converted to the format of the image, with the clear value being treated as + /// linear if the image is sRGB. + Float32([f32; 4]), + + /// Value as [i32]. + /// + /// Use this member for color clear values when the format of the image or attachment has a + /// numeric type that is signed integer. Signed integer values are converted to the format of + /// the image by casting to the smaller type (with negative 32-bit values mapping to negative + /// values in the smaller type). If the integer clear value is not representable in the target + /// type (e.g. would overflow in conversion to that type), the clear value is undefined. + Int32([i32; 4]), + + /// Value as [u32]. + /// + /// Use this member for color clear values when the format of the image or attachment has a + /// numeric type that is unsigned integer. Unsigned integer values are converted to the format + /// of the image by casting to the integer type with fewer bits. + Uint32([u32; 4]), +} + +impl ClearColorValue { + /// RGB zeros and alpha ones. + pub const BLACK_ALPHA_ONE: Self = Self::Float32([0.0, 0.0, 0.0, 1.0]); + + /// All zeros. + pub const BLACK_ALPHA_ZERO: Self = Self::Float32([0.0, 0.0, 0.0, 0.0]); + + /// RGB zeros and alpha ones. + pub const WHITE_ALPHA_ONE: Self = Self::Float32([1.0, 1.0, 1.0, 1.0]); + + /// RGB ones and alpha zeros. + pub const WHITE_ALPHA_ZERO: Self = Self::Float32([1.0, 1.0, 1.0, 0.0]); + + /// Convenience constructor for clear color values. + pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self { + Self::Float32([r, g, b, a]) + } + + /// Convert RGB+A values into a ClearColorValue. + pub const fn from_f32(r: f32, g: f32, b: f32, a: f32) -> Self { + Self::rgba(r, g, b, a) + } + + /// Convert RGB+A values into a ClearColorValue. + pub const fn from_u8(r: u8, g: u8, b: u8, a: u8) -> Self { + Self::from_f32( + r as f32 / u8::MAX as f32, + g as f32 / u8::MAX as f32, + b as f32 / u8::MAX as f32, + a as f32 / u8::MAX as f32, + ) + } +} + +impl Default for ClearColorValue { + fn default() -> Self { + Self::from_f32(0.0, 0.0, 0.0, 0.0) + } +} + +impl From<[f32; 4]> for ClearColorValue { + fn from(float32: [f32; 4]) -> Self { + Self::Float32(float32) + } +} + +impl From<[i32; 4]> for ClearColorValue { + fn from(int32: [i32; 4]) -> Self { + Self::Int32(int32) + } +} + +impl From<[u8; 4]> for ClearColorValue { + fn from(uint8: [u8; 4]) -> Self { + Self::from_u8(uint8[0], uint8[1], uint8[2], uint8[3]) + } +} + +impl From<[u32; 4]> for ClearColorValue { + fn from(uint32: [u32; 4]) -> Self { + Self::Uint32(uint32) + } +} + +impl From for vk::ClearColorValue { + fn from(value: ClearColorValue) -> Self { + match value { + ClearColorValue::Float32(float32) => Self { float32 }, + ClearColorValue::Int32(int32) => Self { int32 }, + ClearColorValue::Uint32(uint32) => Self { uint32 }, + } + } +} + +/// Recording interface for drawing commands. +/// +/// This structure provides a strongly-typed set of methods which allow raster graphics shader code +/// to be executed. An instance is provided to the closure argument of +/// [`PipelineCommand::record_cmd`] which may be accessed by binding a [`GraphicPipeline`] to a +/// command. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ```no_run +/// # use ash::vk; +/// # use vk_graph::cmd::{LoadOp, StoreOp}; +/// # use vk_graph::driver::DriverError; +/// # use vk_graph::driver::device::{Device, DeviceInfo}; +/// # use vk_graph::driver::graphic::{GraphicPipeline, GraphicPipelineInfo}; +/// # use vk_graph::driver::image::{Image, ImageInfo}; +/// # use vk_graph::Graph; +/// # use vk_graph::driver::shader::Shader; +/// # fn main() -> Result<(), DriverError> { +/// # let device = Device::new(DeviceInfo::default())?; +/// # let my_frag_code = [0u8; 1]; +/// # let my_vert_code = [0u8; 1]; +/// # let vert = Shader::new_vertex(my_vert_code.as_slice()); +/// # let frag = Shader::new_fragment(my_frag_code.as_slice()); +/// # let info = GraphicPipelineInfo::default(); +/// # let my_graphic_pipeline = GraphicPipeline::create(&device, info, [vert, frag])?; +/// # let mut my_graph = Graph::default(); +/// # let info = ImageInfo::image_2d( +/// # 32, +/// # 32, +/// # vk::Format::R8G8B8A8_UNORM, +/// # vk::ImageUsageFlags::SAMPLED, +/// # ); +/// # let swapchain_image = my_graph.bind_resource(Image::create(&device, info)?); +/// my_graph +/// .begin_cmd() +/// .debug_name("my draw command") +/// .bind_pipeline(&my_graphic_pipeline) +/// .color_attachment_image(0, swapchain_image, LoadOp::DontCare, StoreOp::Store) +/// .record_cmd(move |cmd| { +/// // During this closure we have access to the drawing functions! +/// cmd.draw(3, 1, 0, 0); +/// }); +/// # Ok(()) } +/// ``` +pub struct GraphicCommandRef<'a> { + cmd: CommandRef<'a>, + pipeline: GraphicPipeline, +} + +impl GraphicCommandRef<'_> { + /// Bind an index buffer to the current command. + /// + /// `offset` is the starting offset in bytes within `buffer` used in index buffer address + /// calculations. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// # use ash::vk; + /// # use vk_graph::cmd::{LoadOp, StoreOp}; + /// # use vk_graph::driver::{AccessType, DriverError}; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::buffer::{Buffer, BufferInfo}; + /// # use vk_graph::driver::graphic::{GraphicPipeline, GraphicPipelineInfo}; + /// # use vk_graph::driver::image::{Image, ImageInfo}; + /// # use vk_graph::driver::shader::Shader; + /// # use vk_graph::Graph; + /// # fn main() -> Result<(), DriverError> { + /// # let device = Device::new(DeviceInfo::default())?; + /// # let my_frag_code = [0u8; 1]; + /// # let my_vert_code = [0u8; 1]; + /// # let vert = Shader::new_vertex(my_vert_code.as_slice()); + /// # let frag = Shader::new_fragment(my_frag_code.as_slice()); + /// # let info = GraphicPipelineInfo::default(); + /// # let my_graphic_pipeline = GraphicPipeline::create(&device, info, [vert, frag])?; + /// # let mut my_graph = Graph::default(); + /// # let info = ImageInfo::image_2d( + /// # 32, + /// # 32, + /// # vk::Format::R8G8B8A8_UNORM, + /// # vk::ImageUsageFlags::SAMPLED, + /// # ); + /// # let swapchain_image = my_graph.bind_resource(Image::create(&device, info)?); + /// # let buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::INDEX_BUFFER); + /// # let my_idx_buf = Buffer::create(&device, buf_info)?; + /// # let buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::VERTEX_BUFFER); + /// # let my_vtx_buf = Buffer::create(&device, buf_info)?; + /// # let my_idx_buf = my_graph.bind_resource(my_idx_buf); + /// # let my_vtx_buf = my_graph.bind_resource(my_vtx_buf); + /// my_graph + /// .begin_cmd() + /// .debug_name("my indexed geometry draw pass") + /// .bind_pipeline(&my_graphic_pipeline) + /// .color_attachment_image(0, swapchain_image, LoadOp::DontCare, StoreOp::Store) + /// .resource_access(my_idx_buf, AccessType::IndexBuffer) + /// .resource_access(my_vtx_buf, AccessType::VertexBuffer) + /// .record_cmd(move |cmd| { + /// cmd + /// .bind_index_buffer(my_idx_buf, 0, vk::IndexType::UINT16) + /// .bind_vertex_buffer(0, my_vtx_buf, 0) + /// .draw_indexed(42, 1, 0, 0, 0); + /// }); + /// # Ok(()) } + /// ``` + #[profiling::function] + pub fn bind_index_buffer( + &self, + buffer: impl Into, + offset: vk::DeviceSize, + index_ty: vk::IndexType, + ) -> &Self { + let buffer = buffer.into(); + let buffer = self.resource(buffer); + + unsafe { + self.cmd + .device + .cmd_bind_index_buffer(self.cmd.handle, buffer.handle, offset, index_ty); + } + + self + } + + /// Bind a vertex buffer to the current pass. + /// + /// The vertex input binding is updated to start at `offset` from the start of `buffer`. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// # use ash::vk; + /// # use vk_graph::cmd::{LoadOp, StoreOp}; + /// # use vk_graph::driver::{sync::AccessType, DriverError}; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::buffer::{Buffer, BufferInfo}; + /// # use vk_graph::driver::graphic::{GraphicPipeline, GraphicPipelineInfo}; + /// # use vk_graph::driver::image::{Image, ImageInfo}; + /// # use vk_graph::driver::shader::Shader; + /// # use vk_graph::Graph; + /// # fn main() -> Result<(), DriverError> { + /// # let device = Device::new(DeviceInfo::default())?; + /// # let buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::VERTEX_BUFFER); + /// # let my_vtx_buf = Buffer::create(&device, buf_info)?; + /// # let my_frag_code = [0u8; 1]; + /// # let my_vert_code = [0u8; 1]; + /// # let vert = Shader::new_vertex(my_vert_code.as_slice()); + /// # let frag = Shader::new_fragment(my_frag_code.as_slice()); + /// # let info = GraphicPipelineInfo::default(); + /// # let my_graphic_pipeline = GraphicPipeline::create(&device, info, [vert, frag])?; + /// # let mut my_graph = Graph::default(); + /// # let info = ImageInfo::image_2d( + /// # 32, + /// # 32, + /// # vk::Format::R8G8B8A8_UNORM, + /// # vk::ImageUsageFlags::SAMPLED, + /// # ); + /// # let swapchain_image = my_graph.bind_resource(Image::create(&device, info)?); + /// # let my_vtx_buf = my_graph.bind_resource(my_vtx_buf); + /// my_graph + /// .begin_cmd() + /// .debug_name("my unindexed geometry draw pass") + /// .bind_pipeline(&my_graphic_pipeline) + /// .color_attachment_image(0, swapchain_image, LoadOp::DontCare, StoreOp::Store) + /// .resource_access(my_vtx_buf, AccessType::VertexBuffer) + /// .record_cmd(move |cmd| { + /// cmd + /// .bind_vertex_buffer(0, my_vtx_buf, 0) + /// .draw(42, 1, 0, 0); + /// }); + /// # Ok(()) } + /// ``` + #[profiling::function] + pub fn bind_vertex_buffer( + &self, + binding: u32, + buffer: impl Into, + offset: vk::DeviceSize, + ) -> &Self { + let buffer = buffer.into(); + let buffer = self.resource(buffer); + + unsafe { + self.cmd.device.cmd_bind_vertex_buffers( + self.cmd.handle, + binding, + slice::from_ref(&buffer.handle), + slice::from_ref(&offset), + ); + } + + self + } + + /// Binds multiple vertex buffers to the current pass, starting at the given `first_binding`. + /// + /// Each vertex input binding in `buffers` specifies an offset from the start of the + /// corresponding buffer. + /// + /// The vertex input attributes that use each of these bindings will use these updated addresses + /// in their address calculations for subsequent drawing commands. + #[profiling::function] + pub fn bind_vertex_buffers( + &self, + first_binding: u32, + buffer_offsets: impl IntoIterator, + ) -> &Self + where + N: Into, + { + #[derive(Default)] + struct Tls { + buffers: Vec, + offsets: Vec, + } + + thread_local! { + static TLS: RefCell = Default::default(); + } + + TLS.with_borrow_mut(|tls| { + tls.buffers.clear(); + tls.offsets.clear(); + + for (buffer, offset) in buffer_offsets { + let buffer = buffer.into(); + let buffer = self.resource(buffer); + + tls.buffers.push(buffer.handle); + tls.offsets.push(offset); + } + + unsafe { + self.cmd.device.cmd_bind_vertex_buffers( + self.cmd.handle, + first_binding, + tls.buffers.as_slice(), + tls.offsets.as_slice(), + ); + } + }); + + self + } + + /// Draw unindexed primitives. + /// + /// When the command is executed, primitives are assembled using the current primitive topology + /// and `vertex_count` consecutive vertex indices with the first `vertex_index` value equal to + /// `first_vertex`. The primitives are drawn `instance_count` times with `instance_index` + /// starting with `first_instance` and increasing sequentially for each instance. + #[profiling::function] + pub fn draw( + &self, + vertex_count: u32, + instance_count: u32, + first_vertex: u32, + first_instance: u32, + ) -> &Self { + unsafe { + self.cmd.device.cmd_draw( + self.cmd.handle, + vertex_count, + instance_count, + first_vertex, + first_instance, + ); + } + + self + } + + /// Draw indexed primitives. + /// + /// When the command is executed, primitives are assembled using the current primitive topology + /// and `index_count` vertices whose indices are retrieved from the index buffer. The index + /// buffer is treated as an array of tightly packed unsigned integers of size defined by the + /// `index_ty` parameter with which the buffer was bound. + #[profiling::function] + pub fn draw_indexed( + &self, + index_count: u32, + instance_count: u32, + first_index: u32, + vertex_offset: i32, + first_instance: u32, + ) -> &Self { + unsafe { + self.cmd.device.cmd_draw_indexed( + self.cmd.handle, + index_count, + instance_count, + first_index, + vertex_offset, + first_instance, + ); + } + + self + } + + /// Draw primitives with indirect parameters and indexed vertices. + /// + /// `draw_indexed_indirect` behaves similarly to `draw_indexed` except that the parameters are + /// read by the device from `buffer` during execution. `draw_count` draws are executed by the + /// command, with parameters taken from `buffer` starting at `offset` and increasing by `stride` + /// bytes for each successive draw. The parameters of each draw are encoded in an array of + /// [`vk::DrawIndexedIndirectCommand`] structures. + /// + /// If `draw_count` is less than or equal to one, `stride` is ignored. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// # use std::mem::size_of; + /// # use ash::vk; + /// # use vk_graph::cmd::{LoadOp, StoreOp}; + /// # use vk_graph::driver::{AccessType, DriverError}; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::buffer::{Buffer, BufferInfo}; + /// # use vk_graph::driver::graphic::{GraphicPipeline, GraphicPipelineInfo}; + /// # use vk_graph::driver::image::{Image, ImageInfo}; + /// # use vk_graph::driver::shader::Shader; + /// # use vk_graph::Graph; + /// # fn main() -> Result<(), DriverError> { + /// # let device = Device::new(DeviceInfo::default())?; + /// # let my_frag_code = [0u8; 1]; + /// # let my_vert_code = [0u8; 1]; + /// # let vert = Shader::new_vertex(my_vert_code.as_slice()); + /// # let frag = Shader::new_fragment(my_frag_code.as_slice()); + /// # let info = GraphicPipelineInfo::default(); + /// # let my_graphic_pipeline = GraphicPipeline::create(&device, info, [vert, frag])?; + /// # let mut my_graph = Graph::default(); + /// # let buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::INDEX_BUFFER); + /// # let my_idx_buf = Buffer::create(&device, buf_info)?; + /// # let buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::VERTEX_BUFFER); + /// # let my_vtx_buf = Buffer::create(&device, buf_info)?; + /// # let my_idx_buf = my_graph.bind_resource(my_idx_buf); + /// # let my_vtx_buf = my_graph.bind_resource(my_vtx_buf); + /// # let info = ImageInfo::image_2d( + /// # 32, + /// # 32, + /// # vk::Format::R8G8B8A8_UNORM, + /// # vk::ImageUsageFlags::SAMPLED, + /// # ); + /// # let swapchain_image = my_graph.bind_resource(Image::create(&device, info)?); + /// const CMD_SIZE: usize = size_of::(); + /// + /// let cmd = vk::DrawIndexedIndirectCommand { + /// index_count: 3, + /// instance_count: 1, + /// first_index: 0, + /// vertex_offset: 0, + /// first_instance: 0, + /// }; + /// let cmd_data = unsafe { + /// std::slice::from_raw_parts(&cmd as *const _ as *const _, CMD_SIZE) + /// }; + /// + /// let buf_flags = vk::BufferUsageFlags::STORAGE_BUFFER; + /// let buf = Buffer::create_from_slice(&device, buf_flags, cmd_data)?; + /// let buf_node = my_graph.bind_resource(buf); + /// + /// my_graph + /// .begin_cmd() + /// .debug_name("draw a single triangle") + /// .bind_pipeline(&my_graphic_pipeline) + /// .color_attachment_image(0, swapchain_image, LoadOp::DontCare, StoreOp::Store) + /// .resource_access(my_idx_buf, AccessType::IndexBuffer) + /// .resource_access(my_vtx_buf, AccessType::VertexBuffer) + /// .resource_access(buf_node, AccessType::IndirectBuffer) + /// .record_cmd(move |cmd| { + /// cmd + /// .bind_index_buffer(my_idx_buf, 0, vk::IndexType::UINT16) + /// .bind_vertex_buffer(0, my_vtx_buf, 0) + /// .draw_indexed_indirect(buf_node, 0, 1, 0); + /// }); + /// # Ok(()) } + /// ``` + #[profiling::function] + pub fn draw_indexed_indirect( + &self, + buffer: impl Into, + offset: vk::DeviceSize, + draw_count: u32, + stride: u32, + ) -> &Self { + let buffer = buffer.into(); + let buffer = self.resource(buffer); + + unsafe { + self.cmd.device.cmd_draw_indexed_indirect( + self.cmd.handle, + buffer.handle, + offset, + draw_count, + stride, + ); + } + + self + } + + /// Draw primitives with indirect parameters, indexed vertices, and draw count. + /// + /// `draw_indexed_indirect_count` behaves similarly to `draw_indexed_indirect` except that the + /// draw count is read by the device from `buffer` during execution. The command will read an + /// unsigned 32-bit integer from `count_buf` located at `count_buf_offset` and use this as the + /// draw count. + /// + /// `max_draw_count` specifies the maximum number of draws that will be executed. The actual + /// number of executed draw calls is the minimum of the count specified in `count_buf` and + /// `max_draw_count`. + /// + /// `stride` is the byte stride between successive sets of draw parameters. + #[profiling::function] + pub fn draw_indexed_indirect_count( + &self, + buffer: impl Into, + offset: vk::DeviceSize, + count_buf: impl Into, + count_buf_offset: vk::DeviceSize, + max_draw_count: u32, + stride: u32, + ) -> &Self { + let buffer = buffer.into(); + let buffer = self.resource(buffer); + + let count_buf = count_buf.into(); + let count_buf = self.resource(count_buf); + + unsafe { + self.cmd.device.cmd_draw_indexed_indirect_count( + self.cmd.handle, + buffer.handle, + offset, + count_buf.handle, + count_buf_offset, + max_draw_count, + stride, + ); + } + + self + } + + /// Draw primitives with indirect parameters and unindexed vertices. + /// + /// Behaves otherwise similar to [`Self::draw_indexed_indirect`]. + #[profiling::function] + pub fn draw_indirect( + &self, + buffer: impl Into, + offset: vk::DeviceSize, + draw_count: u32, + stride: u32, + ) -> &Self { + let buffer = buffer.into(); + let buffer = self.resource(buffer); + + unsafe { + self.cmd.device.cmd_draw_indirect( + self.cmd.handle, + buffer.handle, + offset, + draw_count, + stride, + ); + } + + self + } + + /// Draw primitives with indirect parameters, unindexed vertices, and draw count. + /// + /// Behaves otherwise similar to [`Self::draw_indexed_indirect_count`]. + #[profiling::function] + pub fn draw_indirect_count( + &self, + buffer: impl Into, + offset: vk::DeviceSize, + count_buf: impl Into, + count_buf_offset: vk::DeviceSize, + max_draw_count: u32, + stride: u32, + ) -> &Self { + let buffer = buffer.into(); + let buffer = self.resource(buffer); + + let count_buf = count_buf.into(); + let count_buf = self.resource(count_buf); + + unsafe { + self.cmd.device.cmd_draw_indirect_count( + self.cmd.handle, + buffer.handle, + offset, + count_buf.handle, + count_buf_offset, + max_draw_count, + stride, + ); + } + + self + } + + /// Updates push constants. + /// + /// Push constants represent a high speed path to modify constant data in pipelines that is + /// expected to outperform memory-backed resource updates. + /// + /// Push constant values can be updated incrementally, causing shader stages to read the new + /// data for push constants modified by this command, while still reading the previous data for + /// push constants not modified by this command. + /// + /// # Device limitations + /// + /// See + /// [`device.physical_device.props.limits.max_push_constants_size`](vk::PhysicalDeviceLimits) + /// for the limits of the current device. You may also check [gpuinfo.org] for a listing of + /// reported limits on other devices. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// # vk_shader_macros::glsl!(r#" + /// #version 450 + /// #pragma shader_stage(compute) + /// + /// layout(push_constant) uniform PushConstants { + /// layout(offset = 0) uint the_answer; + /// } push_constants; + /// + /// void main() { + /// // TODO: Add code! + /// } + /// # "#); + /// ``` + /// + /// ```no_run + /// # use ash::vk; + /// # use vk_graph::cmd::{LoadOp, StoreOp}; + /// # use vk_graph::driver::DriverError; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::graphic::{GraphicPipeline, GraphicPipelineInfo}; + /// # use vk_graph::driver::image::{Image, ImageInfo}; + /// # use vk_graph::Graph; + /// # use vk_graph::driver::shader::Shader; + /// # fn main() -> Result<(), DriverError> { + /// # let device = Device::new(DeviceInfo::default())?; + /// # let my_frag_code = [0u8; 1]; + /// # let my_vert_code = [0u8; 1]; + /// # let vert = Shader::new_vertex(my_vert_code.as_slice()); + /// # let frag = Shader::new_fragment(my_frag_code.as_slice()); + /// # let info = GraphicPipelineInfo::default(); + /// # let my_graphic_pipeline = GraphicPipeline::create(&device, info, [vert, frag])?; + /// # let info = ImageInfo::image_2d( + /// # 32, + /// # 32, + /// # vk::Format::R8G8B8A8_UNORM, + /// # vk::ImageUsageFlags::SAMPLED, + /// # ); + /// # let swapchain_image = Image::create(&device, info)?; + /// # let mut my_graph = Graph::default(); + /// # let swapchain_image = my_graph.bind_resource(swapchain_image); + /// my_graph + /// .begin_cmd() + /// .debug_name("draw a quad") + /// .bind_pipeline(&my_graphic_pipeline) + /// .color_attachment_image(0, swapchain_image, LoadOp::DontCare, StoreOp::Store) + /// .record_cmd(move |cmd| { + /// cmd + /// .push_constants(0, &[42]) + /// .draw(6, 1, 0, 0); + /// }); + /// # Ok(()) } + /// ``` + /// + /// See [`vkCmdPushConstants`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdPushConstants.html). + #[profiling::function] + pub fn push_constants(&self, offset: u32, data: &[u8]) -> &Self { + self.cmd_push_constants( + self.pipeline.inner.layout, + &self.pipeline.inner.push_constants, + offset, + data, + ); + + self + } + + /// Set scissor rectangle dynamically for the current command. + /// + /// The default scissor state is no-clip. + #[profiling::function] + pub fn set_scissor(&self, first_scissor: u32, scissors: &[vk::Rect2D]) -> &Self { + unsafe { + self.cmd + .device + .cmd_set_scissor(self.cmd.handle, first_scissor, scissors); + } + + self + } + + /// Set the viewport dynamically for the current command. + /// + /// The default viewport state is the entire render target as defined by all combined image + /// attachments. + #[profiling::function] + pub fn set_viewport(&self, first_viewport: u32, viewports: &[vk::Viewport]) -> &Self { + unsafe { + self.cmd + .device + .cmd_set_viewport(self.cmd.handle, first_viewport, viewports); + } + + self + } +} + +impl<'a> Deref for GraphicCommandRef<'a> { + type Target = CommandRef<'a>; + + fn deref(&self) -> &Self::Target { + &self.cmd + } +} + +/// Specifies the state of a color or combined depth and stencil attachment image during graphic +/// render pass framebuffer load operations. +/// +/// Use this to specify the desired contents of any image before use in a pipeline command buffer. +#[derive(Clone, Copy, Debug)] +pub enum LoadOp { + /// Clears the attachment. + /// + /// `T` will be [ClearColorValue] for color images or [vk::ClearDepthStencilValue] for + /// combined depth and stencil images. + Clear(T), + + /// The attachment will become undefined and reads will produce garbage data. + DontCare, + + /// The attachment will be preserved in memory. + Load, +} + +impl LoadOp { + /// A load operation which results in a color attachment filled with rgb zeros and alpha ones. + pub const CLEAR_BLACK_ALPHA_ONE: Self = Self::Clear(ClearColorValue::BLACK_ALPHA_ONE); + + /// A load operation which results in a color attachment filled with zeros. + pub const CLEAR_BLACK_ALPHA_ZERO: Self = Self::Clear(ClearColorValue::BLACK_ALPHA_ZERO); + + /// A load operation which results in a color attachment filled with rgb zeros and alpha ones. + pub const CLEAR_WHITE_ALPHA_ONE: Self = Self::Clear(ClearColorValue::WHITE_ALPHA_ONE); + + /// A load operation which results in a color attachment filled with rgb ones and alpha zeros. + pub const CLEAR_WHITE_ALPHA_ZERO: Self = Self::Clear(ClearColorValue::WHITE_ALPHA_ZERO); + + /// Convenience constructor for clear color values. + pub fn clear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self { + Self::Clear(ClearColorValue::rgba(r, g, b, a)) + } +} + +impl LoadOp { + /// A load operation which results in a depth attachment filled with ones and stencil filled + /// with zeros. + pub const CLEAR_ONE_STENCIL_ZERO: Self = Self::Clear(vk::ClearDepthStencilValue { + depth: 1.0, + stencil: 0, + }); + + /// A load operation which results in a depth and stencil attachment filled with zeros. + pub const CLEAR_ZERO_STENCIL_ZERO: Self = Self::Clear(vk::ClearDepthStencilValue { + depth: 0.0, + stencil: 0, + }); + + /// Convenience constructor for clear depth and stencil values. + pub fn clear_depth_stencil(depth: f32, stencil: u32) -> Self { + Self::Clear(vk::ClearDepthStencilValue { depth, stencil }) + } +} + +/// Specifies the state of a color or combined depth and stencil attachment image after graphic +/// render pass framebuffer store operations. +/// +/// Use this to specify the desired contents of any image after use in a pipeline command buffer. +#[derive(Clone, Copy, Debug)] +pub enum StoreOp { + /// The attachment will become undefined and reads will produce garbage data. + DontCare, + + /// The attachment will be preserved in memory. + Store, +} + +#[allow(unused)] +mod deprecated { + use { + crate::{ + Attachment, Node, SubresourceAccess, + cmd::{ + AttachmentIndex, Binding, ClearColorValue, PipelineCommand, Subresource, + SubresourceRange, ViewInfo, graphic::GraphicCommandRef, + }, + driver::{ + graphic::GraphicPipeline, + image::{ + ImageInfo, ImageViewInfo, image_subresource_range_contains, + image_subresource_range_intersects, + }, + render_pass::ResolveMode, + }, + node::AnyImageNode, + }, + ash::vk, + vk_sync::AccessType, + }; + + impl GraphicCommandRef<'_> { + #[deprecated = "use push_constants function"] + #[doc(hidden)] + pub fn push_constants_offset(&self, offset: u32, data: &[u8]) -> &Self { + self.push_constants(offset, data) + } + } + + // Attachment functions from previous version + impl PipelineCommand<'_, GraphicPipeline> { + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn attach_color( + self, + attachment_idx: AttachmentIndex, + image: impl Into, + ) -> Self { + let image = image.into(); + let image_info = self.resource(image).info; + let image_view_info: ImageViewInfo = image_info.into(); + + #[allow(deprecated)] + self.attach_color_as(attachment_idx, image, image_view_info) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn attach_color_as( + mut self, + attachment_idx: AttachmentIndex, + image: impl Into, + image_view_info: impl Into, + ) -> Self { + #[allow(deprecated)] + self.set_attach_color_as(attachment_idx, image, image_view_info); + + self + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn attach_depth_stencil(self, image: impl Into) -> Self { + let image = image.into(); + let image_view_info = self.resource(image).info; + + #[allow(deprecated)] + self.attach_depth_stencil_as(image, image_view_info) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn attach_depth_stencil_as( + mut self, + image: impl Into, + image_view_info: impl Into, + ) -> Self { + #[allow(deprecated)] + self.set_attach_depth_stencil_as(image, image_view_info); + + self + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn clear_color( + self, + attachment_idx: AttachmentIndex, + image: impl Into, + ) -> Self { + #[allow(deprecated)] + self.clear_color_value(attachment_idx, image, [0.0, 0.0, 0.0, 0.0]) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn clear_color_value( + self, + attachment_idx: AttachmentIndex, + image: impl Into, + color: impl Into, + ) -> Self { + let image = image.into(); + let image_info = self.resource(image).info; + let image_view_info: ImageViewInfo = image_info.into(); + + #[allow(deprecated)] + self.clear_color_value_as(attachment_idx, image, color, image_view_info) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn clear_color_value_as( + mut self, + attachment_idx: AttachmentIndex, + image: impl Into, + color: impl Into, + image_view_info: impl Into, + ) -> Self { + #[allow(deprecated)] + self.set_clear_color_value_as(attachment_idx, image, color, image_view_info); + + self + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn clear_depth_stencil(self, image: impl Into) -> Self { + #[allow(deprecated)] + self.clear_depth_stencil_value(image, 1.0, 0) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn clear_depth_stencil_value( + self, + image: impl Into, + depth: f32, + stencil: u32, + ) -> Self { + let image = image.into(); + let image_info = self.resource(image).info; + let image_view_info: ImageViewInfo = image_info.into(); + + #[allow(deprecated)] + self.clear_depth_stencil_value_as(image, depth, stencil, image_view_info) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn clear_depth_stencil_value_as( + mut self, + image: impl Into, + depth: f32, + stencil: u32, + image_view_info: impl Into, + ) -> Self { + #[allow(deprecated)] + self.set_clear_depth_stencil_value_as(image, depth, stencil, image_view_info); + + self + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn load_color( + self, + attachment_idx: AttachmentIndex, + image: impl Into, + ) -> Self { + let image = image.into(); + let image_info = self.resource(image).info; + + // Use the plain node information as the whole view of the node + let image_view_info = image_info; + + #[allow(deprecated)] + self.load_color_as(attachment_idx, image, image_view_info) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn load_color_as( + mut self, + attachment_idx: AttachmentIndex, + image: impl Into, + image_view_info: impl Into, + ) -> Self { + #[allow(deprecated)] + self.set_load_color_as(attachment_idx, image, image_view_info); + + self + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn load_depth_stencil(self, image: impl Into) -> Self { + let image = image.into(); + let image_view_info = self.resource(image).info; + + #[allow(deprecated)] + self.load_depth_stencil_as(image, image_view_info) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn load_depth_stencil_as( + mut self, + image: impl Into, + image_view_info: impl Into, + ) -> Self { + #[allow(deprecated)] + self.set_load_depth_stencil_as(image, image_view_info); + + self + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn store_color( + self, + attachment_idx: AttachmentIndex, + image: impl Into, + ) -> Self { + let image = image.into(); + let image_view_info = self.resource(image).info; + + #[allow(deprecated)] + self.store_color_as(attachment_idx, image, image_view_info) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn store_color_as( + mut self, + attachment_idx: AttachmentIndex, + image: impl Into, + image_view_info: impl Into, + ) -> Self { + #[allow(deprecated)] + self.set_store_color_as(attachment_idx, image, image_view_info); + + self + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn store_depth_stencil(self, image: impl Into) -> Self { + let image = image.into(); + let image_view_info = self.resource(image).info; + + #[allow(deprecated)] + self.store_depth_stencil_as(image, image_view_info) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn store_depth_stencil_as( + mut self, + image: impl Into, + image_view_info: impl Into, + ) -> Self { + #[allow(deprecated)] + self.set_store_depth_stencil_as(image, image_view_info); + + self + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn resolve_color( + self, + src_attachment_idx: AttachmentIndex, + dst_attachment_idx: AttachmentIndex, + image: impl Into, + ) -> Self { + let image = image.into(); + let image_view_info = self.resource(image).info; + + #[allow(deprecated)] + self.resolve_color_as( + src_attachment_idx, + dst_attachment_idx, + image, + image_view_info, + ) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn resolve_color_as( + mut self, + src_attachment_idx: AttachmentIndex, + dst_attachment_idx: AttachmentIndex, + image: impl Into, + image_view_info: impl Into, + ) -> Self { + #[allow(deprecated)] + self.set_resolve_color_as( + src_attachment_idx, + dst_attachment_idx, + image, + image_view_info, + ); + + self + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn resolve_depth_stencil( + self, + dst_attachment_idx: AttachmentIndex, + image: impl Into, + depth_mode: Option, + stencil_mode: Option, + ) -> Self { + let image = image.into(); + let image_view_info = self.resource(image).info; + + #[allow(deprecated)] + self.resolve_depth_stencil_as( + dst_attachment_idx, + image, + image_view_info, + depth_mode, + stencil_mode, + ) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn resolve_depth_stencil_as( + mut self, + dst_attachment_idx: AttachmentIndex, + image: impl Into, + image_view_info: impl Into, + depth_mode: Option, + stencil_mode: Option, + ) -> Self { + #[allow(deprecated)] + self.set_resolve_depth_stencil_as( + dst_attachment_idx, + image, + image_view_info, + depth_mode, + stencil_mode, + ); + + self + } + } + + // Attachment functions as setters + impl PipelineCommand<'_, GraphicPipeline> { + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_attach_color( + &mut self, + attachment_idx: AttachmentIndex, + image: impl Into, + ) -> &mut Self { + let image = image.into(); + let image_info = self.resource(image).info; + let image_view_info: ImageViewInfo = image_info.into(); + + #[allow(deprecated)] + self.set_attach_color_as(attachment_idx, image, image_view_info) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_attach_color_as( + &mut self, + attachment_idx: AttachmentIndex, + image: impl Into, + image_view_info: impl Into, + ) -> &mut Self { + let image = image.into(); + let image_view_info = image_view_info.into(); + let node_idx = image.index(); + let ImageInfo { sample_count, .. } = self.resource(image).info; + + debug_assert!( + !self + .cmd + .cmd() + .expect_last_exec() + .color_clears + .contains_key(&attachment_idx), + "color attachment {attachment_idx} already attached via clear" + ); + debug_assert!( + !self + .cmd + .cmd() + .expect_last_exec() + .color_loads + .contains_key(&attachment_idx), + "color attachment {attachment_idx} already attached via load" + ); + + self.cmd + .cmd_mut() + .expect_last_exec_mut() + .color_attachments + .insert( + attachment_idx, + Attachment::new(image_view_info, sample_count, node_idx), + ); + + debug_assert!( + Attachment::are_compatible( + self.cmd + .cmd() + .expect_last_exec() + .color_resolves + .get(&attachment_idx) + .map(|(attachment, _)| *attachment), + self.cmd + .cmd() + .expect_last_exec() + .color_attachments + .get(&attachment_idx) + .copied() + ), + "color attachment {attachment_idx} incompatible with existing resolve" + ); + debug_assert!( + Attachment::are_compatible( + self.cmd + .cmd() + .expect_last_exec() + .color_stores + .get(&attachment_idx) + .copied(), + self.cmd + .cmd() + .expect_last_exec() + .color_attachments + .get(&attachment_idx) + .copied() + ), + "color attachment {attachment_idx} incompatible with existing store" + ); + + self.cmd.push_subresource_access( + image, + SubresourceRange::Image(image_view_info.into()), + AccessType::ColorAttachmentWrite, + ); + + self + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_attach_depth_stencil(&mut self, image: impl Into) -> &mut Self { + let image = image.into(); + let image_view_info = self.resource(image).info; + + #[allow(deprecated)] + self.set_attach_depth_stencil_as(image, image_view_info) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_attach_depth_stencil_as( + &mut self, + image: impl Into, + image_view_info: impl Into, + ) -> &mut Self { + let image = image.into(); + let image_view_info = image_view_info.into(); + let node_idx = image.index(); + let ImageInfo { sample_count, .. } = self.resource(image).info; + + debug_assert!( + self.cmd + .cmd() + .expect_last_exec() + .depth_stencil_clear + .is_none(), + "depth/stencil attachment already attached via clear" + ); + debug_assert!( + self.cmd + .cmd() + .expect_last_exec() + .depth_stencil_load + .is_none(), + "depth/stencil attachment already attached via load" + ); + + self.cmd + .cmd_mut() + .expect_last_exec_mut() + .depth_stencil_attachment = + Some(Attachment::new(image_view_info, sample_count, node_idx)); + + debug_assert!( + Attachment::are_compatible( + self.cmd + .cmd() + .expect_last_exec() + .depth_stencil_resolve + .map(|(attachment, ..)| attachment), + self.cmd.cmd().expect_last_exec().depth_stencil_attachment + ), + "depth/stencil attachment incompatible with existing resolve" + ); + debug_assert!( + Attachment::are_compatible( + self.cmd.cmd().expect_last_exec().depth_stencil_store, + self.cmd.cmd().expect_last_exec().depth_stencil_attachment + ), + "depth/stencil attachment incompatible with existing store" + ); + + self.cmd.push_subresource_access( + image, + SubresourceRange::Image(image_view_info.into()), + if image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::DEPTH | vk::ImageAspectFlags::STENCIL) + { + AccessType::DepthStencilAttachmentWrite + } else if image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::DEPTH) + { + AccessType::DepthAttachmentWriteStencilReadOnly + } else { + AccessType::StencilAttachmentWriteDepthReadOnly + }, + ); + + self + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_clear_color( + &mut self, + attachment_idx: AttachmentIndex, + image: impl Into, + ) -> &mut Self { + #[allow(deprecated)] + self.set_clear_color_value(attachment_idx, image, [0.0, 0.0, 0.0, 0.0]) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_clear_color_value( + &mut self, + attachment_idx: AttachmentIndex, + image: impl Into, + color: impl Into, + ) -> &mut Self { + let image = image.into(); + let image_info = self.resource(image).info; + let image_view_info: ImageViewInfo = image_info.into(); + + #[allow(deprecated)] + self.set_clear_color_value_as(attachment_idx, image, color, image_view_info) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_clear_color_value_as( + &mut self, + attachment_idx: AttachmentIndex, + image: impl Into, + color: impl Into, + image_view_info: impl Into, + ) -> &mut Self { + let image = image.into(); + let image_view_info = image_view_info.into(); + let node_idx = image.index(); + let ImageInfo { sample_count, .. } = self.resource(image).info; + + let color = color.into(); + let color: vk::ClearColorValue = color.into(); + let color = unsafe { color.float32 }; + + debug_assert!( + !self + .cmd + .cmd() + .expect_last_exec() + .color_attachments + .contains_key(&attachment_idx), + "color attachment {attachment_idx} already attached" + ); + debug_assert!( + !self + .cmd + .cmd() + .expect_last_exec() + .color_loads + .contains_key(&attachment_idx), + "color attachment {attachment_idx} already attached via load" + ); + + self.cmd + .cmd_mut() + .expect_last_exec_mut() + .color_clears + .insert( + attachment_idx, + ( + Attachment::new(image_view_info, sample_count, node_idx), + color, + ), + ); + + debug_assert!( + Attachment::are_compatible( + self.cmd + .cmd() + .expect_last_exec() + .color_resolves + .get(&attachment_idx) + .map(|(attachment, _)| *attachment), + self.cmd + .cmd() + .expect_last_exec() + .color_clears + .get(&attachment_idx) + .map(|(attachment, _)| *attachment) + ), + "color attachment {attachment_idx} clear incompatible with existing resolve" + ); + debug_assert!( + Attachment::are_compatible( + self.cmd + .cmd() + .expect_last_exec() + .color_stores + .get(&attachment_idx) + .copied(), + self.cmd + .cmd() + .expect_last_exec() + .color_clears + .get(&attachment_idx) + .map(|(attachment, _)| *attachment) + ), + "color attachment {attachment_idx} clear incompatible with existing store" + ); + + let mut image_access = AccessType::ColorAttachmentWrite; + let image_range = image_view_info.into(); + + // Upgrade existing read access to read-write + if let Some(accesses) = self + .cmd + .cmd_mut() + .expect_last_exec_mut() + .accesses + .get_mut(&node_idx) + { + for SubresourceAccess { + access, + subresource, + } in accesses + { + let access_image_range = *subresource.expect_image(); + if !image_subresource_range_intersects(access_image_range, image_range) { + continue; + } + + image_access = match *access { + AccessType::ColorAttachmentRead | AccessType::ColorAttachmentReadWrite => { + AccessType::ColorAttachmentReadWrite + } + AccessType::ColorAttachmentWrite => AccessType::ColorAttachmentWrite, + _ => continue, + }; + + *access = image_access; + + // If the clear access is a subset of the existing access range there is no need + // to push a new access + if image_subresource_range_contains(access_image_range, image_range) { + return self; + } + } + } + + self.cmd.push_subresource_access( + image, + SubresourceRange::Image(image_range), + image_access, + ); + + self + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_clear_depth_stencil(&mut self, image: impl Into) -> &mut Self { + #[allow(deprecated)] + self.set_clear_depth_stencil_value(image, 1.0, 0) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_clear_depth_stencil_value( + &mut self, + image: impl Into, + depth: f32, + stencil: u32, + ) -> &mut Self { + let image = image.into(); + let image_info = self.resource(image).info; + let image_view_info: ImageViewInfo = image_info.into(); + + #[allow(deprecated)] + self.set_clear_depth_stencil_value_as(image, depth, stencil, image_view_info) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_clear_depth_stencil_value_as( + &mut self, + image: impl Into, + depth: f32, + stencil: u32, + image_view_info: impl Into, + ) -> &mut Self { + let image = image.into(); + let image_view_info = image_view_info.into(); + let node_idx = image.index(); + let ImageInfo { sample_count, .. } = self.resource(image).info; + + debug_assert!( + self.cmd + .cmd() + .expect_last_exec() + .depth_stencil_attachment + .is_none(), + "depth/stencil attachment already attached" + ); + debug_assert!( + self.cmd + .cmd() + .expect_last_exec() + .depth_stencil_load + .is_none(), + "depth/stencil attachment already attached via load" + ); + + self.cmd + .cmd_mut() + .expect_last_exec_mut() + .depth_stencil_clear = Some(( + Attachment::new(image_view_info, sample_count, node_idx), + vk::ClearDepthStencilValue { depth, stencil }, + )); + + debug_assert!( + Attachment::are_compatible( + self.cmd + .cmd() + .expect_last_exec() + .depth_stencil_resolve + .map(|(attachment, ..)| attachment), + self.cmd + .cmd() + .expect_last_exec() + .depth_stencil_clear + .map(|(attachment, _)| attachment) + ), + "depth/stencil attachment clear incompatible with existing resolve" + ); + debug_assert!( + Attachment::are_compatible( + self.cmd.cmd().expect_last_exec().depth_stencil_store, + self.cmd + .cmd() + .expect_last_exec() + .depth_stencil_clear + .map(|(attachment, _)| attachment) + ), + "depth/stencil attachment clear incompatible with existing store" + ); + + let mut image_access = if image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::DEPTH | vk::ImageAspectFlags::STENCIL) + { + AccessType::DepthStencilAttachmentWrite + } else if image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::DEPTH) + { + AccessType::DepthAttachmentWriteStencilReadOnly + } else { + debug_assert!( + image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::STENCIL) + ); + + AccessType::StencilAttachmentWriteDepthReadOnly + }; + let image_range = image_view_info.into(); + + // Upgrade existing read access to read-write + if let Some(accesses) = self + .cmd + .cmd_mut() + .expect_last_exec_mut() + .accesses + .get_mut(&node_idx) + { + for SubresourceAccess { + access, + subresource, + } in accesses + { + let access_image_range = *subresource.expect_image(); + if !image_subresource_range_intersects(access_image_range, image_range) { + continue; + } + + image_access = match *access { + AccessType::DepthAttachmentWriteStencilReadOnly => { + if image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::STENCIL) + { + AccessType::DepthStencilAttachmentReadWrite + } else { + AccessType::DepthAttachmentWriteStencilReadOnly + } + } + AccessType::DepthStencilAttachmentRead => { + if !image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::DEPTH) + { + AccessType::StencilAttachmentWriteDepthReadOnly + } else { + AccessType::DepthAttachmentWriteStencilReadOnly + } + } + AccessType::DepthStencilAttachmentWrite => { + AccessType::DepthStencilAttachmentWrite + } + AccessType::StencilAttachmentWriteDepthReadOnly => { + if image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::DEPTH) + { + AccessType::DepthStencilAttachmentReadWrite + } else { + AccessType::StencilAttachmentWriteDepthReadOnly + } + } + _ => continue, + }; + + *access = image_access; + + // If the clear access is a subset of the existing access range there is no need + // to push a new access + if image_subresource_range_contains(access_image_range, image_range) { + return self; + } + } + } + + self.cmd.push_subresource_access( + image, + SubresourceRange::Image(image_range), + image_access, + ); + + self + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_load_color( + &mut self, + attachment_idx: AttachmentIndex, + image: impl Into, + ) -> &mut Self { + let image = image.into(); + let image_info = self.resource(image).info; + + // Use the plain node information as the whole view of the node + let image_view_info = image_info; + + #[allow(deprecated)] + self.set_load_color_as(attachment_idx, image, image_view_info) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_load_color_as( + &mut self, + attachment_idx: AttachmentIndex, + image: impl Into, + image_view_info: impl Into, + ) -> &mut Self { + let image = image.into(); + let image_view_info = image_view_info.into(); + let node_idx = image.index(); + let ImageInfo { sample_count, .. } = self.resource(image).info; + + debug_assert!( + !self + .cmd + .cmd() + .expect_last_exec() + .color_attachments + .contains_key(&attachment_idx), + "color attachment {attachment_idx} already attached" + ); + debug_assert!( + !self + .cmd + .cmd() + .expect_last_exec() + .color_clears + .contains_key(&attachment_idx), + "color attachment {attachment_idx} already attached via clear" + ); + + self.cmd + .cmd_mut() + .expect_last_exec_mut() + .color_loads + .insert( + attachment_idx, + Attachment::new(image_view_info, sample_count, node_idx), + ); + + debug_assert!( + Attachment::are_compatible( + self.cmd + .cmd() + .expect_last_exec() + .color_resolves + .get(&attachment_idx) + .map(|(attachment, _)| *attachment), + self.cmd + .cmd() + .expect_last_exec() + .color_loads + .get(&attachment_idx) + .copied() + ), + "color attachment {attachment_idx} load incompatible with existing resolve" + ); + debug_assert!( + Attachment::are_compatible( + self.cmd + .cmd() + .expect_last_exec() + .color_stores + .get(&attachment_idx) + .copied(), + self.cmd + .cmd() + .expect_last_exec() + .color_loads + .get(&attachment_idx) + .copied() + ), + "color attachment {attachment_idx} load incompatible with existing store" + ); + + let mut image_access = AccessType::ColorAttachmentRead; + let image_range = image_view_info.into(); + + // Upgrade existing write access to read-write + if let Some(accesses) = self + .cmd + .cmd_mut() + .expect_last_exec_mut() + .accesses + .get_mut(&node_idx) + { + for SubresourceAccess { + access, + subresource, + } in accesses + { + let access_image_range = *subresource.expect_image(); + if !image_subresource_range_intersects(access_image_range, image_range) { + continue; + } + + image_access = match *access { + AccessType::ColorAttachmentRead => AccessType::ColorAttachmentRead, + AccessType::ColorAttachmentReadWrite | AccessType::ColorAttachmentWrite => { + AccessType::ColorAttachmentReadWrite + } + _ => continue, + }; + + *access = image_access; + + // If the load access is a subset of the existing access range there is no need + // to push a new access + if image_subresource_range_contains(access_image_range, image_range) { + return self; + } + } + } + + self.cmd.push_subresource_access( + image, + SubresourceRange::Image(image_range), + image_access, + ); + + self + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_load_depth_stencil(&mut self, image: impl Into) -> &mut Self { + let image = image.into(); + let image_view_info = self.resource(image).info; + + #[allow(deprecated)] + self.set_load_depth_stencil_as(image, image_view_info) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_load_depth_stencil_as( + &mut self, + image: impl Into, + image_view_info: impl Into, + ) -> &mut Self { + let image = image.into(); + let image_view_info = image_view_info.into(); + let node_idx = image.index(); + let ImageInfo { sample_count, .. } = self.resource(image).info; + + debug_assert!( + self.cmd + .cmd() + .expect_last_exec() + .depth_stencil_attachment + .is_none(), + "depth/stencil attachment already attached" + ); + debug_assert!( + self.cmd + .cmd() + .expect_last_exec() + .depth_stencil_clear + .is_none(), + "depth/stencil attachment already attached via clear" + ); + + self.cmd.cmd_mut().expect_last_exec_mut().depth_stencil_load = + Some(Attachment::new(image_view_info, sample_count, node_idx)); + + debug_assert!( + Attachment::are_compatible( + self.cmd + .cmd() + .expect_last_exec() + .depth_stencil_resolve + .map(|(attachment, ..)| attachment), + self.cmd.cmd().expect_last_exec().depth_stencil_load + ), + "depth/stencil attachment load incompatible with existing resolve" + ); + debug_assert!( + Attachment::are_compatible( + self.cmd.cmd().expect_last_exec().depth_stencil_store, + self.cmd.cmd().expect_last_exec().depth_stencil_load + ), + "depth/stencil attachment load incompatible with existing store" + ); + + let mut image_access = AccessType::DepthStencilAttachmentRead; + let image_range = image_view_info.into(); + + // Upgrade existing write access to read-write + if let Some(accesses) = self + .cmd + .cmd_mut() + .expect_last_exec_mut() + .accesses + .get_mut(&node_idx) + { + for SubresourceAccess { + access, + subresource, + } in accesses + { + let access_image_range = *subresource.expect_image(); + if !image_subresource_range_intersects(access_image_range, image_range) { + continue; + } + + image_access = match *access { + AccessType::DepthAttachmentWriteStencilReadOnly => { + AccessType::DepthAttachmentWriteStencilReadOnly + } + AccessType::DepthStencilAttachmentRead => { + AccessType::DepthStencilAttachmentRead + } + AccessType::DepthStencilAttachmentWrite => { + AccessType::DepthStencilAttachmentReadWrite + } + AccessType::StencilAttachmentWriteDepthReadOnly => { + AccessType::StencilAttachmentWriteDepthReadOnly + } + _ => continue, + }; + + *access = image_access; + + // If the load access is a subset of the existing access range there is no need + // to push a new access + if image_subresource_range_contains(access_image_range, image_range) { + return self; + } + } + } + + self.cmd.push_subresource_access( + image, + SubresourceRange::Image(image_range), + image_access, + ); + + self + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_store_color( + &mut self, + attachment_idx: AttachmentIndex, + image: impl Into, + ) -> &mut Self { + let image = image.into(); + let image_view_info = self.resource(image).info; + + #[allow(deprecated)] + self.set_store_color_as(attachment_idx, image, image_view_info) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_store_color_as( + &mut self, + attachment_idx: AttachmentIndex, + image: impl Into, + image_view_info: impl Into, + ) -> &mut Self { + let image = image.into(); + let image_view_info = image_view_info.into(); + let node_idx = image.index(); + let ImageInfo { sample_count, .. } = self.resource(image).info; + + self.cmd + .cmd_mut() + .expect_last_exec_mut() + .color_stores + .insert( + attachment_idx, + Attachment::new(image_view_info, sample_count, node_idx), + ); + + debug_assert!( + Attachment::are_compatible( + self.cmd + .cmd() + .expect_last_exec() + .color_attachments + .get(&attachment_idx) + .copied(), + self.cmd + .cmd() + .expect_last_exec() + .color_stores + .get(&attachment_idx) + .copied() + ), + "color attachment {attachment_idx} store incompatible with existing attachment" + ); + debug_assert!( + Attachment::are_compatible( + self.cmd + .cmd() + .expect_last_exec() + .color_clears + .get(&attachment_idx) + .map(|(attachment, _)| *attachment), + self.cmd + .cmd() + .expect_last_exec() + .color_stores + .get(&attachment_idx) + .copied() + ), + "color attachment {attachment_idx} store incompatible with existing clear" + ); + debug_assert!( + Attachment::are_compatible( + self.cmd + .cmd() + .expect_last_exec() + .color_loads + .get(&attachment_idx) + .copied(), + self.cmd + .cmd() + .expect_last_exec() + .color_stores + .get(&attachment_idx) + .copied() + ), + "color attachment {attachment_idx} store incompatible with existing load" + ); + + let mut image_access = AccessType::ColorAttachmentWrite; + let image_range = image_view_info.into(); + + // Upgrade existing read access to read-write + if let Some(accesses) = self + .cmd + .cmd_mut() + .expect_last_exec_mut() + .accesses + .get_mut(&node_idx) + { + for SubresourceAccess { + access, + subresource, + } in accesses + { + let access_image_range = *subresource.expect_image(); + if !image_subresource_range_intersects(access_image_range, image_range) { + continue; + } + + image_access = match *access { + AccessType::ColorAttachmentRead | AccessType::ColorAttachmentReadWrite => { + AccessType::ColorAttachmentReadWrite + } + AccessType::ColorAttachmentWrite => AccessType::ColorAttachmentWrite, + _ => continue, + }; + + *access = image_access; + + // If the store access is a subset of the existing access range there is no need + // to push a new access + if image_subresource_range_contains(access_image_range, image_range) { + return self; + } + } + } + + self.cmd.push_subresource_access( + image, + SubresourceRange::Image(image_range), + image_access, + ); + + self + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_store_depth_stencil(&mut self, image: impl Into) -> &mut Self { + let image = image.into(); + let image_view_info = self.resource(image).info; + + #[allow(deprecated)] + self.set_store_depth_stencil_as(image, image_view_info) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_store_depth_stencil_as( + &mut self, + image: impl Into, + image_view_info: impl Into, + ) -> &mut Self { + let image = image.into(); + let image_view_info = image_view_info.into(); + let node_idx = image.index(); + let ImageInfo { sample_count, .. } = self.resource(image).info; + + self.cmd + .cmd_mut() + .expect_last_exec_mut() + .depth_stencil_store = + Some(Attachment::new(image_view_info, sample_count, node_idx)); + + debug_assert!( + Attachment::are_compatible( + self.cmd.cmd().expect_last_exec().depth_stencil_attachment, + self.cmd.cmd().expect_last_exec().depth_stencil_store + ), + "depth/stencil attachment store incompatible with existing attachment" + ); + debug_assert!( + Attachment::are_compatible( + self.cmd + .cmd() + .expect_last_exec() + .depth_stencil_clear + .map(|(attachment, _)| attachment), + self.cmd.cmd().expect_last_exec().depth_stencil_store + ), + "depth/stencil attachment store incompatible with existing clear" + ); + debug_assert!( + Attachment::are_compatible( + self.cmd.cmd().expect_last_exec().depth_stencil_load, + self.cmd.cmd().expect_last_exec().depth_stencil_store + ), + "depth/stencil attachment store incompatible with existing load" + ); + + let mut image_access = if image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::DEPTH | vk::ImageAspectFlags::STENCIL) + { + AccessType::DepthStencilAttachmentWrite + } else if image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::DEPTH) + { + AccessType::DepthAttachmentWriteStencilReadOnly + } else { + debug_assert!( + image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::STENCIL) + ); + + AccessType::StencilAttachmentWriteDepthReadOnly + }; + let image_range = image_view_info.into(); + + // Upgrade existing read access to read-write + if let Some(accesses) = self + .cmd + .cmd_mut() + .expect_last_exec_mut() + .accesses + .get_mut(&node_idx) + { + for SubresourceAccess { + access, + subresource, + } in accesses + { + let access_image_range = *subresource.expect_image(); + if !image_subresource_range_intersects(access_image_range, image_range) { + continue; + } + + image_access = match *access { + AccessType::DepthAttachmentWriteStencilReadOnly => { + if image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::STENCIL) + { + AccessType::DepthStencilAttachmentReadWrite + } else { + AccessType::DepthAttachmentWriteStencilReadOnly + } + } + AccessType::DepthStencilAttachmentRead => { + if !image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::DEPTH) + { + AccessType::StencilAttachmentWriteDepthReadOnly + } else { + AccessType::DepthStencilAttachmentReadWrite + } + } + AccessType::DepthStencilAttachmentWrite => { + AccessType::DepthStencilAttachmentWrite + } + AccessType::StencilAttachmentWriteDepthReadOnly => { + if image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::DEPTH) + { + AccessType::DepthStencilAttachmentReadWrite + } else { + AccessType::StencilAttachmentWriteDepthReadOnly + } + } + _ => continue, + }; + + *access = image_access; + + // If the store access is a subset of the existing access range there is no need + // to push a new access + if image_subresource_range_contains(access_image_range, image_range) { + return self; + } + } + } + + self.cmd.push_subresource_access( + image, + SubresourceRange::Image(image_range), + image_access, + ); + + self + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_resolve_color( + &mut self, + src_attachment_idx: AttachmentIndex, + dst_attachment_idx: AttachmentIndex, + image: impl Into, + ) -> &mut Self { + let image = image.into(); + let image_view_info = self.resource(image).info; + + #[allow(deprecated)] + self.set_resolve_color_as( + src_attachment_idx, + dst_attachment_idx, + image, + image_view_info, + ) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_resolve_color_as( + &mut self, + src_attachment_idx: AttachmentIndex, + dst_attachment_idx: AttachmentIndex, + image: impl Into, + image_view_info: impl Into, + ) -> &mut Self { + let image = image.into(); + let image_view_info = image_view_info.into(); + let node_idx = image.index(); + let ImageInfo { sample_count, .. } = self.resource(image).info; + + self.cmd + .cmd_mut() + .expect_last_exec_mut() + .color_resolves + .insert( + dst_attachment_idx, + ( + Attachment::new(image_view_info, sample_count, node_idx), + src_attachment_idx, + ), + ); + + debug_assert!( + Attachment::are_compatible( + self.cmd + .cmd() + .expect_last_exec() + .color_attachments + .get(&dst_attachment_idx) + .copied(), + self.cmd + .cmd() + .expect_last_exec() + .color_resolves + .get(&dst_attachment_idx) + .map(|(attachment, _)| *attachment) + ), + "color attachment {dst_attachment_idx} resolve conflict", + ); + debug_assert!( + Attachment::are_compatible( + self.cmd + .cmd() + .expect_last_exec() + .color_clears + .get(&dst_attachment_idx) + .map(|(attachment, _)| *attachment), + self.cmd + .cmd() + .expect_last_exec() + .color_resolves + .get(&dst_attachment_idx) + .map(|(attachment, _)| *attachment) + ), + "color attachment {dst_attachment_idx} resolve incompatible with existing clear" + ); + debug_assert!( + Attachment::are_compatible( + self.cmd + .cmd() + .expect_last_exec() + .color_loads + .get(&dst_attachment_idx) + .copied(), + self.cmd + .cmd() + .expect_last_exec() + .color_resolves + .get(&dst_attachment_idx) + .map(|(attachment, _)| *attachment) + ), + "color attachment {dst_attachment_idx} resolve incompatible with existing load" + ); + + let mut image_access = AccessType::ColorAttachmentWrite; + let image_range = image_view_info.into(); + + // Upgrade existing read access to read-write + if let Some(accesses) = self + .cmd + .cmd_mut() + .expect_last_exec_mut() + .accesses + .get_mut(&node_idx) + { + for SubresourceAccess { + access, + subresource, + } in accesses + { + let access_image_range = *subresource.expect_image(); + if !image_subresource_range_intersects(access_image_range, image_range) { + continue; + } + + image_access = match *access { + AccessType::ColorAttachmentRead | AccessType::ColorAttachmentReadWrite => { + AccessType::ColorAttachmentReadWrite + } + AccessType::ColorAttachmentWrite => AccessType::ColorAttachmentWrite, + _ => continue, + }; + + *access = image_access; + + // If the resolve access is a subset of the existing access range there is no + // need to push a new access + if image_subresource_range_contains(access_image_range, image_range) { + return self; + } + } + } + + self.cmd.push_subresource_access( + image, + SubresourceRange::Image(image_range), + image_access, + ); + + self + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_resolve_depth_stencil( + &mut self, + dst_attachment_idx: AttachmentIndex, + image: impl Into, + depth_mode: Option, + stencil_mode: Option, + ) -> &mut Self { + let image = image.into(); + let image_view_info = self.resource(image).info; + + #[allow(deprecated)] + self.set_resolve_depth_stencil_as( + dst_attachment_idx, + image, + image_view_info, + depth_mode, + stencil_mode, + ) + } + + #[deprecated = "upgrade guide: https://github.com/attackgoat/vk-graph/pull/107"] + #[doc(hidden)] + pub fn set_resolve_depth_stencil_as( + &mut self, + dst_attachment_idx: AttachmentIndex, + image: impl Into, + image_view_info: impl Into, + depth_mode: Option, + stencil_mode: Option, + ) -> &mut Self { + let image = image.into(); + let image_view_info = image_view_info.into(); + let node_idx = image.index(); + let ImageInfo { sample_count, .. } = self.resource(image).info; + + self.cmd + .cmd_mut() + .expect_last_exec_mut() + .depth_stencil_resolve = Some(( + Attachment::new(image_view_info, sample_count, node_idx), + dst_attachment_idx, + depth_mode, + stencil_mode, + )); + + let mut image_access = if image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::DEPTH | vk::ImageAspectFlags::STENCIL) + { + AccessType::DepthStencilAttachmentWrite + } else if image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::DEPTH) + { + AccessType::DepthAttachmentWriteStencilReadOnly + } else { + debug_assert!( + image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::STENCIL) + ); + + AccessType::StencilAttachmentWriteDepthReadOnly + }; + let image_range = image_view_info.into(); + + // Upgrade existing read access to read-write + if let Some(accesses) = self + .cmd + .cmd_mut() + .expect_last_exec_mut() + .accesses + .get_mut(&node_idx) + { + for SubresourceAccess { + access, + subresource, + } in accesses + { + let access_image_range = *subresource.expect_image(); + if !image_subresource_range_intersects(access_image_range, image_range) { + continue; + } + + image_access = match *access { + AccessType::DepthAttachmentWriteStencilReadOnly => { + if image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::STENCIL) + { + AccessType::DepthStencilAttachmentReadWrite + } else { + AccessType::DepthAttachmentWriteStencilReadOnly + } + } + AccessType::DepthStencilAttachmentRead => { + if !image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::DEPTH) + { + AccessType::StencilAttachmentWriteDepthReadOnly + } else { + AccessType::DepthStencilAttachmentReadWrite + } + } + AccessType::DepthStencilAttachmentWrite => { + AccessType::DepthStencilAttachmentWrite + } + AccessType::StencilAttachmentWriteDepthReadOnly => { + if image_view_info + .aspect_mask + .contains(vk::ImageAspectFlags::DEPTH) + { + AccessType::DepthStencilAttachmentReadWrite + } else { + AccessType::StencilAttachmentWriteDepthReadOnly + } + } + _ => continue, + }; + + *access = image_access; + + // If the resolve access is a subset of the existing access range there is no + // need to push a new access + if image_subresource_range_contains(access_image_range, image_range) { + return self; + } + } + } + + self.cmd.push_subresource_access( + image, + SubresourceRange::Image(image_range), + image_access, + ); + + self + } + } + + // Resource functions + impl PipelineCommand<'_, GraphicPipeline> { + #[deprecated = "use shader_resource_access"] + #[doc(hidden)] + pub fn read_descriptor(self, binding: impl Into, node: N) -> Self + where + N: Node + Subresource, + N::Info: Copy, + SubresourceRange: From, + ViewInfo: From, + { + self.shader_resource_access( + binding, + node, + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + } + + #[deprecated = "use shader_subresource_access"] + #[doc(hidden)] + pub fn read_descriptor_as( + self, + descriptor: impl Into, + node: N, + node_view: impl Into, + ) -> Self + where + N: Node + Subresource, + N::Info: Copy, + SubresourceRange: From, + ViewInfo: From, + { + self.shader_subresource_access( + descriptor, + node, + node_view, + AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, + ) + } + + #[deprecated = "use record_cmd function"] + #[doc(hidden)] + pub fn record_subpass( + self, + func: impl FnOnce(GraphicCommandRef<'_>, ()) + Send + 'static, + ) -> Self { + self.record_cmd(|cmd| { + func(cmd, ()); + }) + } + + #[deprecated = "use shader_resource_access function with AccessType::AnyShaderWrite"] + #[doc(hidden)] + pub fn write_descriptor(self, descriptor: impl Into, node: N) -> Self + where + N: Node + Subresource, + N::Info: Copy, + SubresourceRange: From, + ViewInfo: From, + { + self.shader_resource_access(descriptor, node, AccessType::AnyShaderWrite) + } + + #[deprecated = "use shader_subresource_access function with AccessType::AnyShaderWrite"] + #[doc(hidden)] + pub fn write_descriptor_as( + self, + descriptor: impl Into, + node: N, + node_view: impl Into, + ) -> Self + where + N: Node + Subresource, + N::Info: Copy, + SubresourceRange: From, + ViewInfo: From, + { + self.shader_subresource_access(descriptor, node, node_view, AccessType::AnyShaderWrite) + } + } +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs new file mode 100644 index 00000000..80b88105 --- /dev/null +++ b/src/cmd/mod.rs @@ -0,0 +1,729 @@ +//! Strongly-typed [`Graph`] commands. + +mod cmd_ref; +mod compute; +mod graphic; +mod pipeline; +mod ray_trace; + +pub use self::{ + cmd_ref::{ + BuildAccelerationStructureIndirectInfo, BuildAccelerationStructureInfo, CommandRef, + UpdateAccelerationStructureIndirectInfo, UpdateAccelerationStructureInfo, + }, + compute::ComputeCommandRef, + graphic::{ClearColorValue, GraphicCommandRef, LoadOp, StoreOp}, + pipeline::{Pipeline, PipelineCommand}, + ray_trace::RayTraceCommandRef, +}; + +use { + super::{ + AccelerationStructureLeaseNode, AccelerationStructureNode, AnyAccelerationStructureNode, + AnyBufferNode, AnyImageNode, AnyResource, BufferLeaseNode, BufferNode, CommandData, + Execution, ExecutionFunction, Graph, ImageLeaseNode, ImageNode, Node, Resource, + SwapchainImageNode, + }, + crate::driver::{buffer::BufferSubresourceRange, image::ImageViewInfo}, + ash::vk, + std::ops::Range, + vk_sync::AccessType, +}; + +/// Alias for the index of a framebuffer attachment. +pub(crate) type AttachmentIndex = u32; + +/// Alias for the binding index of a shader descriptor. +pub(crate) type BindingIndex = u32; + +/// Alias for the binding offset of a shader descriptor array element. +pub(crate) type BindingOffset = u32; + +/// Alias for the descriptor set index of a shader descriptor. +pub(crate) type DescriptorSetIndex = u32; + +/// A general-purpose Vulkan command which may contain acceleration structure operations, transfers, +/// or shader pipelines. +/// +/// There are four main uses of a [`Command`]: +/// +/// 1. Bind resources ([`Self::bind_resource`]) +/// 1. Declare resource accesses ([`Self::resource_access`]) +/// 1. Record general-purpose command buffers or acceleration structure operations +/// ([`Self::record_cmd`]) +/// 1. Bind shader pipelines ([`Self::bind_pipeline`]) +/// +/// When bound, a shader pipeline consumes the `Command` and returns a [`PipelineCommand`] which +/// provides command recording functions specific to each pipeline type. +pub struct Command<'a> { + pub(super) cmd_idx: usize, + pub(super) exec_idx: usize, + pub(super) graph: &'a mut Graph, +} + +impl<'a> Command<'a> { + pub(super) fn new(graph: &'a mut Graph) -> Self { + let cmd_idx = graph.cmds.len(); + graph.cmds.push(CommandData { + execs: vec![Default::default()], // We start off with a default execution! + name: None, + }); + + Self { + cmd_idx, + exec_idx: 0, + graph, + } + } + + fn cmd(&self) -> &CommandData { + &self.graph.cmds[self.cmd_idx] + } + + fn cmd_mut(&mut self) -> &mut CommandData { + &mut self.graph.cmds[self.cmd_idx] + } + + /// Binds a Vulkan buffer, image, or acceleration structure resource to the graph associated + /// with this command. + /// + /// Bound nodes may be used in commands for pipeline and shader operations. + pub fn bind_resource(&mut self, resource: R) -> R::Node + where + R: Resource, + { + self.graph.bind_resource(resource) + } + + /// Binds a shader pipeline to the current command, allowing for strongly typed access to the + /// related functions. + /// + /// `P`|`P::Command` + /// -|- + /// [`ComputePipeline`](crate::driver::compute::ComputePipeline)|[`PipelineCommand<'_, + /// ComputePipeline>`] + /// [`GraphicPipeline`](crate::driver::graphic::GraphicPipeline)|[`PipelineCommand<'_, + /// GraphicPipeline>`] + /// [`RayTracePipeline`](crate::driver::ray_trace::RayTracePipeline)|[`PipelineCommand<'_, + /// RayTracePipeline>`] + pub fn bind_pipeline

(self, pipeline: P) -> P::Command + where + P: Pipeline<'a>, + { + pipeline.bind_cmd(self) + } + + /// Sets a debugging name, but only in debug builds + pub fn debug_name(mut self, name: impl Into) -> Self { + self.set_debug_name(name); + self + } + + /// Finalize the recording of this command and return to the `Graph` where you may record + /// additional commands. + pub fn end_cmd(self) -> &'a mut Graph { + // If nothing was done in this pass we can just ignore it + if self.exec_idx == 0 { + self.graph.cmds.pop(); + } + + self.graph + } + + fn push_exec(&mut self, func: impl FnOnce(CommandRef) + Send + 'static) { + let cmd = self.cmd_mut(); + let exec = { + let last_exec = cmd.expect_last_exec_mut(); + last_exec.func = Some(ExecutionFunction(Box::new(func))); + + Execution { + pipeline: last_exec.pipeline.clone(), + ..Default::default() + } + }; + + cmd.execs.push(exec); + self.exec_idx += 1; + } + + fn push_subresource_access( + &mut self, + resource_node: impl Node, + subresource: SubresourceRange, + access: AccessType, + ) { + let node_idx = resource_node.index(); + + debug_assert!(self.graph.resources.get(node_idx).is_some()); + + let access = SubresourceAccess { + access, + subresource, + }; + self.cmd_mut() + .expect_last_exec_mut() + .accesses + .entry(node_idx) + .and_modify(|accesses| accesses.push(access)) + .or_insert(vec![access]); + } + + /// Begin recording a general-purpose command buffer. + /// + /// This is the entry point for building and updating an + /// [`AccelerationStructure`](crate::driver::accel_struct::AccelerationStructure) instance. + /// + /// The provided closure allows you to run any Vulkan code, or interoperate with other Vulkan + /// code and interfaces. + pub fn record_cmd(mut self, func: impl FnOnce(CommandRef<'_>) + Send + 'static) -> Self { + self.record_cmd_mut(func); + self + } + + /// Begin recording a general-purpose command buffer. + /// + /// This is the entry point for building and updating an + /// [`AccelerationStructure`](crate::driver::accel_struct::AccelerationStructure) instance. + /// + /// The provided closure allows you to run any Vulkan code, or interoperate with other Vulkan + /// code and interfaces. + pub fn record_cmd_mut(&mut self, func: impl FnOnce(CommandRef<'_>) + Send + 'static) { + self.push_exec(move |cmd| { + func(cmd); + }); + } + + /// Returns a borrow of the original Vulkan resource (buffer, image or acceleration structure) + /// which the given bound resource node represents. + pub fn resource(&self, resource_node: N) -> &N::Resource + where + N: Node, + { + self.graph.resource(resource_node) + } + + /// Informs the command that the next recorded command buffer will read or write `resource_node` + /// using `access`. + /// + /// An access function must be called for `resource_node` before it is used within a + /// `record_`-function. + pub fn resource_access(mut self, resource_node: N, access: AccessType) -> Self + where + N: Node + Subresource, + SubresourceRange: From, + { + self.set_resource_access(resource_node, access); + self + } + + /// Sets a debugging name, but only in debug builds. + pub fn set_debug_name(&mut self, _name: impl Into) -> &mut Self { + #[cfg(debug_assertions)] + { + self.cmd_mut().name = Some(_name.into()); + } + + self + } + + /// Informs the command that the next recorded command buffer will read or write `resource_node` + /// using `access`. + /// + /// An access function must be called for `resource_node` before it is used within a + /// `record_`-function. + pub fn set_resource_access(&mut self, resource_node: N, access: AccessType) + where + N: Node + Subresource, + SubresourceRange: From, + { + let whole_resource = resource_node.range(&self.graph.resources); + let subresource = SubresourceRange::from(whole_resource); + + self.push_subresource_access(resource_node, subresource, access); + } + + /// Informs the command that the next recorded command buffer will read or write the + /// `subresource` of `resource_node` using `access`. + /// + /// An access function must be called for `resource_node` before it is used within a + /// `record_`-function. + pub fn set_subresource_access( + &mut self, + resource_node: N, + subresource: impl Into, + access: AccessType, + ) where + N: Node + Subresource, + SubresourceRange: From, + { + let subresource = subresource.into(); + let subresource = SubresourceRange::from(subresource); + + self.push_subresource_access(resource_node, subresource, access); + } + + /// Informs the command that the next recorded command buffer will read or write the + /// `subresource` of `resource` using `access`. + /// + /// An access function must be called for `resource` before it is used within a + /// `record_`-function. + pub fn subresource_access( + mut self, + resource_node: N, + subresource: impl Into, + access: AccessType, + ) -> Self + where + N: Node + Subresource, + SubresourceRange: From, + { + self.set_subresource_access(resource_node, subresource, access); + self + } +} + +/// Describes the SPIR-V binding index, and optionally a specific descriptor set +/// and array index. +/// +/// Generally you might pass a function a descriptor using a simple integer: +/// +/// ```rust +/// # fn my_func(_: usize, _: ()) {} +/// # let image = (); +/// let descriptor = 42; +/// my_func(descriptor, image); +/// ``` +/// +/// But also: +/// +/// - `(0, 42)` for descriptor set `0` and binding index `42` +/// - `(42, [8])` for the same binding, but the 8th element +/// - `(0, 42, [8])` same as the previous example +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct Binding { + /// The value of the descriptor binding decoration applied to the variable. + pub binding: u32, + + /// An array-element offset applied to this descriptor. + pub offset: u32, + + /// An optional descriptor set index value. + pub set: u32, +} + +impl Binding { + pub(super) fn into_tuple(self) -> (DescriptorSetIndex, BindingIndex, BindingOffset) { + (self.set, self.binding, self.offset) + } + + pub(super) fn set(self) -> DescriptorSetIndex { + let (res, _, _) = self.into_tuple(); + res + } +} + +impl From for Binding { + fn from(binding: BindingIndex) -> Self { + Self { + binding, + offset: 0, + set: 0, + } + } +} + +impl From<(DescriptorSetIndex, BindingIndex)> for Binding { + fn from((set, binding): (DescriptorSetIndex, BindingIndex)) -> Self { + Self { + binding, + offset: 0, + set, + } + } +} + +impl From<(BindingIndex, [BindingOffset; 1])> for Binding { + fn from((binding, [offset]): (BindingIndex, [BindingOffset; 1])) -> Self { + Self { + binding, + offset, + set: 0, + } + } +} + +impl From<(DescriptorSetIndex, BindingIndex, [BindingOffset; 1])> for Binding { + fn from( + (set, binding, [offset]): (DescriptorSetIndex, BindingIndex, [BindingOffset; 1]), + ) -> Self { + Self { + binding, + offset, + set, + } + } +} + +/// Allows for a resource to be reinterpreted as differently formatted data. +pub trait Subresource { + /// The information about the subresource when bound directly to shader descriptors. + type Info; + + /// The information about the subresource when used indirectly by any part of a graph. + type Range; + + #[doc(hidden)] + fn info(&self, _: &[AnyResource]) -> Self::Info + where + Self: Node; + + #[doc(hidden)] + fn range(&self, _: &[AnyResource]) -> Self::Range + where + Self: Node; +} + +macro_rules! view_accel_struct { + ($name:ident) => { + impl Subresource for $name { + type Info = Self::Range; + type Range = (); + + fn info(&self, _: &[AnyResource]) -> Self::Info + where + Self: Node, + { + } + + fn range(&self, _: &[AnyResource]) -> Self::Range + where + Self: Node, + { + } + } + }; +} + +view_accel_struct!(AnyAccelerationStructureNode); +view_accel_struct!(AccelerationStructureLeaseNode); +view_accel_struct!(AccelerationStructureNode); + +macro_rules! view_buffer { + ($name:ident) => { + impl Subresource for $name { + type Info = Self::Range; + type Range = BufferSubresourceRange; + + fn info(&self, resources: &[AnyResource]) -> Self::Info + where + Self: Node, + { + self.range(resources) + } + + fn range(&self, resources: &[AnyResource]) -> Self::Range + where + Self: Node, + { + let idx = self.index(); + + resources[idx].expect_buffer().info.into() + } + } + }; +} + +view_buffer!(AnyBufferNode); +view_buffer!(BufferLeaseNode); +view_buffer!(BufferNode); + +macro_rules! view_image { + ($name:ident) => { + impl Subresource for $name { + type Info = ImageViewInfo; + type Range = vk::ImageSubresourceRange; + + fn info(&self, resources: &[AnyResource]) -> Self::Info + where + Self: Node, + { + let idx = self.index(); + + resources[idx].expect_image().info.into() + } + + fn range(&self, resources: &[AnyResource]) -> Self::Range + where + Self: Node, + { + self.info(resources).into() + } + } + }; +} + +view_image!(AnyImageNode); +view_image!(ImageLeaseNode); +view_image!(ImageNode); +view_image!(SwapchainImageNode); + +#[derive(Clone, Copy, Debug)] +#[doc(hidden)] +pub enum SubresourceRange { + /// Acceleration structures are bound whole. + AccelerationStructure, + + /// Images may be partially bound. + Image(vk::ImageSubresourceRange), + + /// Buffers may be partially bound. + Buffer(BufferSubresourceRange), +} + +impl SubresourceRange { + pub(super) fn as_image(&self) -> Option<&vk::ImageSubresourceRange> { + if let Self::Image(subresource) = self { + Some(subresource) + } else { + None + } + } + + pub(super) fn expect_image(&self) -> &vk::ImageSubresourceRange { + self.as_image().expect("missing image subresource") + } +} + +impl From for SubresourceRange { + fn from(subresource: BufferSubresourceRange) -> Self { + Self::Buffer(subresource) + } +} + +impl From<()> for SubresourceRange { + fn from(_: ()) -> Self { + Self::AccelerationStructure + } +} + +impl From for SubresourceRange { + fn from(subresource: ImageViewInfo) -> Self { + Self::Image(subresource.into()) + } +} + +impl From for SubresourceRange { + fn from(subresource: vk::ImageSubresourceRange) -> Self { + Self::Image(subresource) + } +} + +#[derive(Clone, Copy, Debug)] +pub(super) struct SubresourceAccess { + pub access: AccessType, + pub subresource: SubresourceRange, +} + +/// Describes the interpretation of a resource. +#[derive(Debug)] +#[doc(hidden)] +pub enum ViewInfo { + /// Acceleration structures are always whole resources. + AccelerationStructure, + + /// Images may be interpreted as differently formatted images. + Image(ImageViewInfo), + + /// Buffers may be interpreted as subregions of the same buffer. + Buffer(BufferSubresourceRange), +} + +impl ViewInfo { + pub(crate) fn as_buffer(&self) -> Option<&BufferSubresourceRange> { + match self { + Self::Buffer(info) => Some(info), + _ => None, + } + } + + pub(crate) fn as_image(&self) -> Option<&ImageViewInfo> { + match self { + Self::Image(info) => Some(info), + _ => None, + } + } + + pub(crate) fn expect_buffer(&self) -> &BufferSubresourceRange { + self.as_buffer().expect("missing buffer view info") + } + + pub(crate) fn expect_image(&self) -> &ImageViewInfo { + self.as_image().expect("missing image view info") + } +} + +impl From<()> for ViewInfo { + fn from(_: ()) -> Self { + Self::AccelerationStructure + } +} + +impl From for ViewInfo { + fn from(info: BufferSubresourceRange) -> Self { + Self::Buffer(info) + } +} + +impl From for ViewInfo { + fn from(info: ImageViewInfo) -> Self { + Self::Image(info) + } +} + +impl From> for ViewInfo { + fn from(range: Range) -> Self { + Self::Buffer(BufferSubresourceRange { + start: range.start, + end: range.end, + }) + } +} + +#[allow(deprecated)] +#[allow(unused)] +mod deprecated { + use { + crate::{ + Graph, Node, Resource, + cmd::{Command, CommandRef, Subresource, SubresourceRange}, + deprecated::Info, + }, + ash::vk, + vk_sync::AccessType, + }; + + impl<'a> Command<'a> { + #[deprecated = "use resource_access function"] + #[doc(hidden)] + pub fn access_node(mut self, node: N, access: AccessType) -> Self + where + N: Node + Subresource, + SubresourceRange: From, + { + self.resource_access(node, access) + } + + #[deprecated = "use set_resource_access function"] + #[doc(hidden)] + pub fn access_node_mut(&mut self, node: N, access: AccessType) -> &mut Self + where + N: Node + Subresource, + SubresourceRange: From, + { + self.set_resource_access(node, access); + self + } + + #[deprecated = "use subresource_access function"] + #[doc(hidden)] + pub fn access_node_subrange( + mut self, + node: N, + access: AccessType, + subresource: impl Into, + ) -> Self + where + N: Node + Subresource, + SubresourceRange: From, + { + self.access_node_subrange_mut(node, access, subresource); + self + } + + #[deprecated = "use set_subresource_access function"] + #[doc(hidden)] + pub fn access_node_subrange_mut( + &mut self, + node: N, + access: AccessType, + subresource: impl Into, + ) -> &mut Self + where + N: Node + Subresource, + SubresourceRange: From, + { + self.set_subresource_access(node, subresource, access); + self + } + + #[deprecated = "use resource_access function"] + #[doc(hidden)] + pub fn access_resource(mut self, node: N, access: AccessType) -> Self + where + N: Node + Subresource, + SubresourceRange: From, + { + self.resource_access(node, access) + } + + #[deprecated = "use subresource_access function"] + #[doc(hidden)] + pub fn access_subresource( + mut self, + node: N, + subresource: impl Into, + access: AccessType, + ) -> Self + where + N: Node + Subresource, + SubresourceRange: From, + { + self.subresource_access(node, subresource, access) + } + + #[deprecated = "use bind_resource function"] + #[doc(hidden)] + pub fn bind_node(&mut self, resource: R) -> R::Node + where + R: Resource, + { + self.bind_resource(resource) + } + + #[deprecated = "use device_address function of resource function result"] + #[doc(hidden)] + pub fn node_device_address(&self, node: impl Node) -> vk::DeviceAddress { + let idx = node.index(); + + self.graph.resources[idx].expect_buffer().device_address() + } + + #[deprecated = "dereference info field of resource function result"] + #[doc(hidden)] + pub fn node_info(&self, node: N) -> N::Type + where + N: Node + Info, + { + node.info(&self.graph.resources) + } + + #[deprecated = "use record_cmd function"] + #[doc(hidden)] + pub fn record_acceleration( + mut self, + func: impl FnOnce(CommandRef<'_>, ()) + Send + 'static, + ) -> Self { + self.push_exec(|cmd| { + func(cmd, ()); + }); + + self + } + + #[deprecated = "use end_cmd function"] + #[doc(hidden)] + pub fn submit_pass(self) -> &'a mut Graph { + self.end_cmd() + } + } +} diff --git a/src/cmd/pipeline.rs b/src/cmd/pipeline.rs new file mode 100644 index 00000000..dc0eedc6 --- /dev/null +++ b/src/cmd/pipeline.rs @@ -0,0 +1,493 @@ +use { + super::{ + AccessType, Binding, Command, Graph, Node, Resource, Subresource, SubresourceRange, + ViewInfo, + }, + crate::{ + ExecutionPipeline, + driver::{compute::ComputePipeline, graphic::GraphicPipeline, ray_trace::RayTracePipeline}, + }, + std::marker::PhantomData, +}; + +/// A trait for pipelines which may be bound to a `Command`. +/// +/// See [`Command::bind_pipeline`](crate::cmd::Command::bind_pipeline) for details. +pub trait Pipeline<'a> { + /// The resource reference type. + type Command; + + /// Binds the resource to a command. + /// + /// Returns a reference type. + fn bind_cmd(self, _: Command<'a>) -> Self::Command; +} + +macro_rules! pipeline { + ($name:ident) => { + paste::paste! { + impl<'a> Pipeline<'a> for [<$name Pipeline>] { + type Command = PipelineCommand<'a, [<$name Pipeline>]>; + + fn bind_cmd(self, mut cmd: Command<'a>) -> Self::Command { + { + let cmd = cmd.cmd_mut(); + if cmd.expect_last_exec().pipeline.is_some() { + cmd.execs.push(Default::default()); + } + + cmd.expect_last_exec_mut().pipeline + = Some(ExecutionPipeline::$name(self)); + } + + Self::Command { + __: PhantomData, + cmd, + } + } + } + + impl<'a> Pipeline<'a> for &'a [<$name Pipeline>] { + type Command = PipelineCommand<'a, [<$name Pipeline>]>; + + fn bind_cmd(self, mut cmd: Command<'a>) -> Self::Command { + { + let cmd = cmd.cmd_mut(); + if cmd.expect_last_exec().pipeline.is_some() { + cmd.execs.push(Default::default()); + } + + cmd.expect_last_exec_mut().pipeline + = Some(ExecutionPipeline::$name(self.clone())); + } + + Self::Command { + __: PhantomData, + cmd, + } + } + + } + + impl ExecutionPipeline { + #[allow(unused)] + pub(crate) fn [](&self) -> bool { + matches!(self, Self::$name(_)) + } + + #[allow(unused)] + pub(crate) fn [](&self) -> &[<$name Pipeline>] { + if let Self::$name(binding) = self { + &binding + } else { + panic!(); + } + } + } + } + }; +} + +// Pipelines you can bind to a command ref +pipeline!(Compute); +pipeline!(Graphic); +pipeline!(RayTrace); + +/// A [`Command`] which has been bound to a particular compute, graphic, or ray-trace pipeline. +pub struct PipelineCommand<'c, T> { + pub(super) __: PhantomData, + pub(super) cmd: Command<'c>, +} + +// NOTE: There are specific implementations of T in the compute, graphic, and ray trace modules +impl<'c, T> PipelineCommand<'c, T> { + /// Binds a shader pipeline to the current command, allowing for strongly typed access to the + /// related functions. + /// + /// `P`|`P::Command` + /// -|- + /// [`ComputePipeline`](crate::driver::compute::ComputePipeline)|[`PipelineCommand<'_, + /// ComputePipeline>`] + /// [`GraphicPipeline`](crate::driver::graphic::GraphicPipeline)|[`PipelineCommand<'_, + /// GraphicPipeline>`] + /// [`RayTracePipeline`](crate::driver::ray_trace::RayTracePipeline)|[`PipelineCommand<'_, + /// RayTracePipeline>`] + pub fn bind_pipeline

(self, pipeline: P) -> P::Command + where + P: Pipeline<'c>, + { + pipeline.bind_cmd(self.cmd) + } + + /// Binds a Vulkan buffer, image, or acceleration structure resource to the graph associated + /// with this command. + /// + /// Bound nodes may be used in passes for pipeline and shader operations. + pub fn bind_resource(&mut self, resource: R) -> R::Node + where + R: Resource, + { + self.cmd.bind_resource(resource) + } + + /// Finalizes a command and returns the graph so that additional commands may be added. + pub fn end_cmd(self) -> &'c mut Graph { + self.cmd.end_cmd() + } + + /// Returns a borrow of the original Vulkan resource (buffer, image or acceleration structure) + /// which the given bound resource node represents. + pub fn resource(&self, resource_node: N) -> &N::Resource + where + N: Node, + { + self.cmd.resource(resource_node) + } + + /// Informs the command that the next recorded command buffer will read or write `resource_node` + /// using `access`. + /// + /// An access function must be called for `resource_node` before it is used within a + /// `record_`-function. + pub fn resource_access(mut self, resource_node: N, access: AccessType) -> Self + where + N: Node + Subresource, + SubresourceRange: From, + { + self.cmd.set_resource_access(resource_node, access); + self + } + + /// Informs the command that the next recorded command buffer will read or write `resource_node` + /// using `access`. + /// + /// An access function must be called for `resource_node` before it is used within a + /// `record_`-function. + pub fn set_resource_access(&mut self, resource_node: N, access: AccessType) -> &mut Self + where + N: Node + Subresource, + SubresourceRange: From, + { + self.cmd.set_resource_access(resource_node, access); + self + } + + /// Informs the command that the next recorded command buffer will read or write the + /// `resource_node` at the specified shader `binding` using `access`. + /// + /// An access function must be called for `resource_node` before it is used within a + /// `record_`-function. + pub fn set_shader_resource_access( + &mut self, + binding: impl Into, + resource_node: N, + access: AccessType, + ) -> &mut Self + where + N: Node + Subresource, + N::Info: Copy, + SubresourceRange: From, + ViewInfo: From, + { + let subresource = resource_node.info(&self.cmd.graph.resources); + + self.set_shader_subresource_access(binding, resource_node, subresource, access) + } + + /// Informs the command that the next recorded command buffer will read or write the + /// `resource_node` at the specified shader `binding` using `access`. The resource will be + /// interpreted using `view_info`. + /// + /// An access function must be called for `resource_node` before it is used within a + /// `record_`-function. + pub fn set_shader_subresource_access( + &mut self, + binding: impl Into, + resource_node: N, + subresource: impl Into, + access: AccessType, + ) -> &mut Self + where + N: Node + Subresource, + N::Info: Copy, + SubresourceRange: From, + ViewInfo: From, + { + let binding = binding.into(); + let subresource = subresource.into(); + let node_idx = resource_node.index(); + + self.cmd.push_subresource_access( + resource_node, + SubresourceRange::from(subresource), + access, + ); + + assert!( + self.cmd + .cmd_mut() + .expect_last_exec_mut() + .bindings + .insert(binding, (node_idx, subresource.into())) + .is_none(), + "binding {binding:?} has already been bound" + ); + + self + } + + /// Informs the command that the next recorded command buffer will read or write the + /// `subresource` of `resource_node` using `access`. + /// + /// An access function must be called for `resource_node` before it is used within a + /// `record_`-function. + pub fn set_subresource_access( + &mut self, + resource_node: N, + subresource: impl Into, + access: AccessType, + ) -> &mut Self + where + N: Node + Subresource, + SubresourceRange: From, + { + self.cmd + .set_subresource_access(resource_node, subresource, access); + self + } + + /// Informs the command that the next recorded command buffer will read or write the + /// `resource_node` at the specified shader `binding` using `access`. + /// + /// An access function must be called for `resource_node` before it is used within a + /// `record_`-function. + pub fn shader_resource_access( + mut self, + binding: impl Into, + resource_node: N, + access: AccessType, + ) -> Self + where + N: Node + Subresource, + N::Info: Copy, + SubresourceRange: From, + ViewInfo: From, + { + self.set_shader_resource_access(binding, resource_node, access); + self + } + + /// Informs the command that the next recorded command buffer will read or write the + /// `resource_node` at the specified shader `binding` using `access`. The resource will be + /// interpreted using `view_info`. + /// + /// An access function must be called for `resource_node` before it is used within a + /// `record_`-function. + pub fn shader_subresource_access( + mut self, + binding: impl Into, + resource_node: N, + subresource: impl Into, + access: AccessType, + ) -> Self + where + N: Node + Subresource, + N::Info: Copy, + SubresourceRange: From, + ViewInfo: From, + { + self.set_shader_subresource_access(binding, resource_node, subresource, access); + self + } + + /// Informs the command that the next recorded command buffer will read or write the + /// `subresource` of `resource_node` using `access`. + /// + /// An access function must be called for `resource_node` before it is used within a + /// `record_`-function. + pub fn subresource_access( + mut self, + resource_node: N, + subresource: impl Into, + access: AccessType, + ) -> Self + where + N: Node + Subresource, + SubresourceRange: From, + { + self.cmd + .set_subresource_access(resource_node, subresource, access); + self + } +} + +#[allow(deprecated)] +#[allow(unused)] +mod deprecated { + use { + crate::{ + Graph, Node, Resource, + cmd::{Binding, PipelineCommand, Subresource, SubresourceRange, ViewInfo}, + deprecated::Info, + graph::pass_ref::ViewType, + }, + ash::vk, + vk_sync::AccessType, + }; + + impl<'a, T> PipelineCommand<'a, T> { + #[deprecated = "use shader_resource_access function"] + #[doc(hidden)] + pub fn access_descriptor( + self, + descriptor: impl Into, + node: N, + access: AccessType, + ) -> Self + where + N: Node + Info + Subresource, + ViewType: From<::Info>, + ::Info: Copy + From<::Type>, + ::Range: From<::Info>, + SubresourceRange: From, + { + let view_info = Subresource::info(&node, &self.cmd.graph.resources); + + self.access_descriptor_as(descriptor, node, access, view_info) + } + + #[deprecated = "use shader_subresource_access function"] + #[doc(hidden)] + pub fn access_descriptor_as( + self, + descriptor: impl Into, + node: N, + access: AccessType, + view_info: impl Into, + ) -> Self + where + N: Node + Subresource, + ::Info: Copy + Into, + ::Range: From<::Info>, + SubresourceRange: From, + { + let view_info = view_info.into(); + let subresource = ::Range::from(view_info); + + self.access_descriptor_subrange(descriptor, node, access, view_info, subresource) + } + + #[deprecated = "use shader_subresource_access function"] + #[doc(hidden)] + pub fn access_descriptor_subrange( + mut self, + descriptor: impl Into, + node: N, + access: AccessType, + view_info: impl Into, + subresource: impl Into, + ) -> Self + where + N: Node + Subresource, + ::Info: Into, + SubresourceRange: From, + { + let descriptor = descriptor.into(); + let view_info = view_info.into(); + let node_idx = node.index(); + + self.cmd.push_subresource_access( + node, + SubresourceRange::from(subresource.into()), + access, + ); + + assert!( + self.cmd + .cmd_mut() + .expect_last_exec_mut() + .bindings + .insert(descriptor, (node_idx, view_info.into())) + .is_none(), + "descriptor {descriptor:?} has already been bound" + ); + + self + } + + #[deprecated = "use resource_access function"] + #[doc(hidden)] + pub fn access_node(mut self, node: N, access: AccessType) -> Self + where + N: Node + Subresource, + SubresourceRange: From, + { + self.resource_access(node, access) + } + + #[deprecated = "use subresource_access function"] + #[doc(hidden)] + pub fn access_node_subrange( + mut self, + node: N, + access: AccessType, + subresource: impl Into, + ) -> Self + where + N: Node + Subresource, + SubresourceRange: From, + { + self.access_node_subrange_mut(node, access, subresource); + self + } + + #[deprecated = "use set_subresource_access function"] + #[doc(hidden)] + pub fn access_node_subrange_mut( + &mut self, + node: N, + access: AccessType, + subresource: impl Into, + ) -> &mut Self + where + N: Node + Subresource, + SubresourceRange: From, + { + self.set_subresource_access(node, subresource, access) + } + + #[deprecated = "use bind_resource function"] + #[doc(hidden)] + pub fn bind_node(&mut self, resource: R) -> R::Node + where + R: Resource, + { + self.bind_resource(resource) + } + + #[deprecated = "use device_address function of resource function result"] + #[doc(hidden)] + pub fn node_device_address(&self, node: impl Node) -> vk::DeviceAddress { + let idx = node.index(); + + self.cmd.graph.resources[idx] + .expect_buffer() + .device_address() + } + + #[deprecated = "dereference info field of resource function result"] + #[doc(hidden)] + pub fn node_info(&self, node: N) -> N::Type + where + N: Node + Info, + { + node.info(&self.cmd.graph.resources) + } + + #[deprecated = "use end_cmd function"] + #[doc(hidden)] + pub fn submit_pass(self) -> &'a mut Graph { + self.end_cmd() + } + } +} diff --git a/src/cmd/ray_trace.rs b/src/cmd/ray_trace.rs new file mode 100644 index 00000000..e99533b3 --- /dev/null +++ b/src/cmd/ray_trace.rs @@ -0,0 +1,450 @@ +use { + super::{PipelineCommand, cmd_ref::CommandRef}, + crate::driver::{device::Device, ray_trace::RayTracePipeline}, + ash::vk, + std::ops::Deref, +}; + +impl PipelineCommand<'_, RayTracePipeline> { + /// Begin recording a ray trace pipeline command buffer. + pub fn record_cmd( + mut self, + func: impl FnOnce(RayTraceCommandRef<'_>) + Send + 'static, + ) -> Self { + self.record_cmd_mut(func); + self + } + + /// Begin recording a ray trace pipeline command buffer. + pub fn record_cmd_mut(&mut self, func: impl FnOnce(RayTraceCommandRef<'_>) + Send + 'static) { + let pipeline = self + .cmd + .cmd() + .expect_last_pipeline() + .expect_ray_trace() + .clone(); + + #[cfg(debug_assertions)] + let dynamic_stack_size = pipeline.inner.info.dynamic_stack_size; + + self.cmd.push_exec(move |cmd| { + func(RayTraceCommandRef { + cmd, + + #[cfg(debug_assertions)] + dynamic_stack_size, + + pipeline, + }); + }); + } +} + +/// Recording interface for ray tracing commands. +/// +/// This structure provides a strongly-typed set of methods which allow ray trace shader code to be +/// executed. An instance is provided to the closure argument of +/// [`PipelineCommand::record_cmd`] which may be accessed by binding a [`RayTracePipeline`] to +/// a command. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ```no_run +/// # use ash::vk; +/// # use vk_graph::driver::DriverError; +/// # use vk_graph::driver::device::{Device, DeviceInfo}; +/// # use vk_graph::driver::ray_trace::{ +/// # RayTracePipeline, +/// # RayTracePipelineInfo, +/// # RayTraceShaderGroup, +/// # }; +/// # use vk_graph::driver::shader::Shader; +/// # use vk_graph::Graph; +/// # fn main() -> Result<(), DriverError> { +/// # let device = Device::new(DeviceInfo::default())?; +/// # let info = RayTracePipelineInfo::default(); +/// # let my_miss_code = [0u8; 1]; +/// # let my_ray_trace_pipeline = RayTracePipeline::create(&device, info, +/// # [Shader::new_miss(my_miss_code.as_slice())], +/// # [RayTraceShaderGroup::new_general(0)], +/// # )?; +/// # let mut my_graph = Graph::default(); +/// my_graph.begin_cmd() +/// .debug_name("my ray trace command") +/// .bind_pipeline(&my_ray_trace_pipeline) +/// .record_cmd(move |cmd| { +/// // During this closure we have access to the ray trace functions! +/// }); +/// # Ok(()) } +/// ``` +pub struct RayTraceCommandRef<'a> { + cmd: CommandRef<'a>, + + #[cfg(debug_assertions)] + dynamic_stack_size: bool, + + pipeline: RayTracePipeline, +} + +impl RayTraceCommandRef<'_> { + /// Updates push constants. + /// + /// Push constants represent a high speed path to modify constant data in pipelines that is + /// expected to outperform memory-backed resource updates. + /// + /// Push constant values can be updated incrementally, causing shader stages to read the new + /// data for push constants modified by this command, while still reading the previous data for + /// push constants not modified by this command. + /// + /// # Device limitations + /// + /// See + /// [`device.physical_device.props.limits.max_push_constants_size`](vk::PhysicalDeviceLimits) + /// for the limits of the current device. You may also check [gpuinfo.org] for a listing of + /// reported limits on other devices. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// # vk_shader_macros::glsl!(target: vulkan1_2, r#" + /// #version 460 + /// #pragma shader_stage(closest) + /// + /// layout(push_constant) uniform PushConstants { + /// layout(offset = 0) uint some_val; + /// } push_constants; + /// + /// void main() { + /// // TODO: Add bindings to write things! + /// } + /// # "#); + /// ``` + /// + /// ```no_run + /// # use ash::vk; + /// # use vk_graph::driver::DriverError; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::buffer::{Buffer, BufferInfo}; + /// # use vk_graph::driver::ray_trace::{ + /// # RayTracePipeline, + /// # RayTracePipelineInfo, + /// # RayTraceShaderGroup, + /// # }; + /// # use vk_graph::driver::shader::Shader; + /// # use vk_graph::Graph; + /// # fn main() -> Result<(), DriverError> { + /// # let device = Device::new(DeviceInfo::default())?; + /// # let shader = [0u8; 1]; + /// # let info = RayTracePipelineInfo::default(); + /// # let my_miss_code = [0u8; 1]; + /// # let my_ray_trace_pipeline = RayTracePipeline::create(&device, info, + /// # [Shader::new_miss(my_miss_code.as_slice())], + /// # [RayTraceShaderGroup::new_general(0)], + /// # )?; + /// # let rgen_sbt = vk::StridedDeviceAddressRegionKHR { + /// # device_address: 0, + /// # stride: 0, + /// # size: 0, + /// # }; + /// # let hit_sbt = vk::StridedDeviceAddressRegionKHR { + /// # device_address: 0, + /// # stride: 0, + /// # size: 0, + /// # }; + /// # let miss_sbt = vk::StridedDeviceAddressRegionKHR { + /// # device_address: 0, + /// # stride: 0, + /// # size: 0, + /// # }; + /// # let call_sbt = vk::StridedDeviceAddressRegionKHR { + /// # device_address: 0, + /// # stride: 0, + /// # size: 0, + /// # }; + /// # let mut my_graph = Graph::default(); + /// my_graph.begin_cmd() + /// .debug_name("draw a cornell box") + /// .bind_pipeline(&my_ray_trace_pipeline) + /// .record_cmd(move |cmd| { + /// cmd.push_constants(0, &[0xcb]) + /// .trace_rays(&rgen_sbt, &hit_sbt, &miss_sbt, &call_sbt, 320, 200, 1); + /// }); + /// # Ok(()) } + /// ``` + /// + /// See [`vkCmdPushConstants`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdPushConstants.html). + #[profiling::function] + pub fn push_constants(&self, offset: u32, data: &[u8]) -> &Self { + self.cmd_push_constants( + self.pipeline.inner.layout, + &self.pipeline.inner.push_constants, + offset, + data, + ); + + self + } + + /// Set the stack size dynamically for a ray trace pipeline. + /// + /// See + /// `RayTracePipelineInfo::dynamic_stack_size` and see the Vulkan spec. + #[profiling::function] + pub fn set_stack_size(&self, pipeline_stack_size: u32) -> &Self { + #[cfg(debug_assertions)] + assert!(self.dynamic_stack_size); + + let ray_trace_ext = Device::expect_ray_trace_ext(&self.cmd.device); + + unsafe { + ray_trace_ext + .cmd_set_ray_tracing_pipeline_stack_size(self.cmd.handle, pipeline_stack_size); + } + + self + } + + // TODO: If the rayTraversalPrimitiveCulling or rayQuery features are enabled, the + // SkipTrianglesKHR and SkipAABBsKHR ray flags can be specified when tracing a ray. + // SkipTrianglesKHR and SkipAABBsKHR are mutually exclusive. + + /// Ray traces using the currently-bound [`RayTracePipeline`] and the given shader binding + /// tables. + /// + /// Shader binding tables must be constructed according to this [example]. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// # use ash::vk; + /// # use vk_graph::driver::DriverError; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::buffer::{Buffer, BufferInfo}; + /// # use vk_graph::driver::ray_trace::{ + /// # RayTracePipeline, + /// # RayTracePipelineInfo, + /// # RayTraceShaderGroup, + /// # }; + /// # use vk_graph::driver::shader::Shader; + /// # use vk_graph::Graph; + /// # fn main() -> Result<(), DriverError> { + /// # let device = Device::new(DeviceInfo::default())?; + /// # let shader = [0u8; 1]; + /// # let info = RayTracePipelineInfo::default(); + /// # let my_miss_code = [0u8; 1]; + /// # let my_ray_trace_pipeline = RayTracePipeline::create(&device, info, + /// # [Shader::new_miss(my_miss_code.as_slice())], + /// # [RayTraceShaderGroup::new_general(0)], + /// # )?; + /// # let rgen_sbt = vk::StridedDeviceAddressRegionKHR { + /// # device_address: 0, + /// # stride: 0, + /// # size: 0, + /// # }; + /// # let hit_sbt = vk::StridedDeviceAddressRegionKHR { + /// # device_address: 0, + /// # stride: 0, + /// # size: 0, + /// # }; + /// # let miss_sbt = vk::StridedDeviceAddressRegionKHR { + /// # device_address: 0, + /// # stride: 0, + /// # size: 0, + /// # }; + /// # let call_sbt = vk::StridedDeviceAddressRegionKHR { + /// # device_address: 0, + /// # stride: 0, + /// # size: 0, + /// # }; + /// # let mut my_graph = Graph::default(); + /// my_graph.begin_cmd() + /// .debug_name("draw a cornell box") + /// .bind_pipeline(&my_ray_trace_pipeline) + /// .record_cmd(move |cmd| { + /// cmd.trace_rays(&rgen_sbt, &hit_sbt, &miss_sbt, &call_sbt, 320, 200, 1); + /// }); + /// # Ok(()) } + /// ``` + /// + /// [example]: https://github.com/attackgoat/vk-graph/blob/master/examples/ray_trace.rs + #[allow(clippy::too_many_arguments)] + #[profiling::function] + pub fn trace_rays( + &self, + raygen_shader_binding_table: &vk::StridedDeviceAddressRegionKHR, + miss_shader_binding_table: &vk::StridedDeviceAddressRegionKHR, + hit_shader_binding_table: &vk::StridedDeviceAddressRegionKHR, + callable_shader_binding_table: &vk::StridedDeviceAddressRegionKHR, + width: u32, + height: u32, + depth: u32, + ) -> &Self { + let ray_trace_ext = Device::expect_ray_trace_ext(&self.cmd.device); + + unsafe { + ray_trace_ext.cmd_trace_rays( + self.cmd.handle, + raygen_shader_binding_table, + miss_shader_binding_table, + hit_shader_binding_table, + callable_shader_binding_table, + width, + height, + depth, + ); + } + + self + } + + /// Ray traces using the currently-bound [`RayTracePipeline`] and the given shader binding + /// tables. + /// + /// `indirect_device_address` is a [buffer device address] which is a pointer to a + /// [`vk::TraceRaysIndirectCommandKHR`] structure containing the trace ray parameters. + /// + /// See [`vkCmdTraceRaysIndirectKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdTraceRaysIndirectKHR.html). + /// + /// [buffer device address]: crate::driver::buffer::Buffer::device_address + #[profiling::function] + pub fn trace_rays_indirect( + &self, + raygen_shader_binding_table: &vk::StridedDeviceAddressRegionKHR, + miss_shader_binding_table: &vk::StridedDeviceAddressRegionKHR, + hit_shader_binding_table: &vk::StridedDeviceAddressRegionKHR, + callable_shader_binding_table: &vk::StridedDeviceAddressRegionKHR, + indirect_device_address: vk::DeviceAddress, + ) -> &Self { + let ray_trace_ext = Device::expect_ray_trace_ext(&self.cmd.device); + + unsafe { + ray_trace_ext.cmd_trace_rays_indirect( + self.cmd.handle, + raygen_shader_binding_table, + miss_shader_binding_table, + hit_shader_binding_table, + callable_shader_binding_table, + indirect_device_address, + ) + } + + self + } +} + +impl<'a> Deref for RayTraceCommandRef<'a> { + type Target = CommandRef<'a>; + + fn deref(&self) -> &Self::Target { + &self.cmd + } +} + +#[allow(unused)] +mod deprecated { + use { + crate::{ + Node, + cmd::{ + Binding, PipelineCommand, Subresource, SubresourceRange, ViewInfo, + ray_trace::RayTraceCommandRef, + }, + driver::ray_trace::RayTracePipeline, + }, + vk_sync::AccessType, + }; + + impl RayTraceCommandRef<'_> { + #[deprecated = "use push_constants function"] + #[doc(hidden)] + pub fn push_constants_offset(&self, offset: u32, data: &[u8]) -> &Self { + self.push_constants(offset, data) + } + } + + impl PipelineCommand<'_, RayTracePipeline> { + #[deprecated = "use shader_resource_access"] + #[doc(hidden)] + pub fn read_descriptor(self, descriptor: impl Into, node: N) -> Self + where + N: Node + Subresource, + N::Info: Copy, + SubresourceRange: From, + ViewInfo: From, + { + self.shader_resource_access( + descriptor, + node, + AccessType::RayTracingShaderReadSampledImageOrUniformTexelBuffer, + ) + } + + #[deprecated = "use shader_subresource_access"] + #[doc(hidden)] + pub fn read_descriptor_as( + self, + descriptor: impl Into, + node: N, + node_view: impl Into, + ) -> Self + where + N: Node + Subresource, + N::Info: Copy, + SubresourceRange: From, + ViewInfo: From, + { + self.shader_subresource_access( + descriptor, + node, + node_view, + AccessType::RayTracingShaderReadSampledImageOrUniformTexelBuffer, + ) + } + + #[deprecated = "use record_cmd function"] + #[doc(hidden)] + pub fn record_ray_trace( + self, + func: impl FnOnce(RayTraceCommandRef<'_>, ()) + Send + 'static, + ) -> Self { + self.record_cmd(|cmd| { + func(cmd, ()); + }) + } + + #[deprecated = "use shader_resource_access function with AccessType::AnyShaderWrite"] + #[doc(hidden)] + pub fn write_descriptor(self, descriptor: impl Into, node: N) -> Self + where + N: Node + Subresource, + N::Info: Copy, + SubresourceRange: From, + ViewInfo: From, + { + self.shader_resource_access(descriptor, node, AccessType::AnyShaderWrite) + } + + #[deprecated = "use shader_subresource_access function with AccessType::AnyShaderWrite"] + #[doc(hidden)] + pub fn write_descriptor_as( + self, + descriptor: impl Into, + node: N, + node_view: impl Into, + ) -> Self + where + N: Node + Subresource, + N::Info: Copy, + SubresourceRange: From, + ViewInfo: From, + { + self.shader_subresource_access(descriptor, node, node_view, AccessType::AnyShaderWrite) + } + } +} diff --git a/src/display.rs b/src/display.rs deleted file mode 100644 index 22f7ad44..00000000 --- a/src/display.rs +++ /dev/null @@ -1,499 +0,0 @@ -use { - super::{ - driver::{ - CommandBuffer, CommandBufferInfo, DescriptorPool, DescriptorPoolInfo, DriverError, - RenderPass, RenderPassInfo, - device::Device, - image::Image, - image_access_layout, - swapchain::{Swapchain, SwapchainImage, SwapchainInfo}, - }, - graph::{RenderGraph, node::SwapchainImageNode}, - pool::Pool, - }, - crate::prelude::SwapchainError, - ash::vk, - derive_builder::{Builder, UninitializedFieldError}, - log::{trace, warn}, - std::{ - error::Error, - fmt::{Debug, Formatter}, - slice, - sync::Arc, - thread::panicking, - time::Instant, - }, - vk_sync::{AccessType, ImageBarrier, cmd::pipeline_barrier}, -}; - -/// A physical display interface. -pub struct Display { - exec_idx: usize, - execs: Box<[Execution]>, - queue_family_idx: u32, - swapchain: Swapchain, -} - -impl Display { - /// Constructs a new `Display` object. - pub fn new( - device: &Arc, - swapchain: Swapchain, - info: impl Into, - ) -> Result { - let info: DisplayInfo = info.into(); - - assert_ne!(info.command_buffer_count, 0); - - let mut execs = Vec::with_capacity(info.command_buffer_count as _); - for _ in 0..info.command_buffer_count { - let cmd_buf = - CommandBuffer::create(device, CommandBufferInfo::new(info.queue_family_index))?; - let swapchain_acquired = Device::create_semaphore(device)?; - let swapchain_rendered = Device::create_semaphore(device)?; - - execs.push(Execution { - cmd_buf, - queue: None, - swapchain_acquired, - swapchain_rendered, - }); - } - let execs = execs.into_boxed_slice(); - - Ok(Self { - exec_idx: info.command_buffer_count, - execs, - queue_family_idx: info.queue_family_index, - swapchain, - }) - } - - /// Gets the next available swapchain image which should be rendered to and then presented using - /// [`present_image`][Self::present_image]. - pub fn acquire_next_image(&mut self) -> Result, DisplayError> { - self.exec_idx += 1; - self.exec_idx %= self.execs.len(); - let exec = &mut self.execs[self.exec_idx]; - - if exec.queue.is_some() { - CommandBuffer::wait_until_executed(&mut exec.cmd_buf).inspect_err(|err| { - warn!("unable to wait for display fence: {err}"); - })?; - - exec.queue = None; - } - - CommandBuffer::drop_fenced(&mut exec.cmd_buf); - - unsafe { - exec.cmd_buf - .device - .reset_fences(slice::from_ref(&exec.cmd_buf.fence)) - .map_err(|err| { - warn!("unable to reset display fence: {err}"); - - DriverError::InvalidData - })?; - } - - let acquire_next_image = self.swapchain.acquire_next_image(exec.swapchain_acquired); - - if let Err(err) = acquire_next_image { - warn!("unable to acquire next swapchain image: {err:?}"); - } - - let mut swapchain_image = match acquire_next_image { - Err(SwapchainError::DeviceLost) => Err(DisplayError::DeviceLost), - Err(SwapchainError::Suboptimal) => return Ok(None), - Err(SwapchainError::SurfaceLost) => Err(DisplayError::Driver(DriverError::InvalidData)), - Ok(swapchain_image) => Ok(swapchain_image), - }?; - swapchain_image.exec_idx = self.exec_idx; - - Ok(Some(swapchain_image)) - } - - /// Displays the given swapchain image using passes specified in `render_graph`, if possible. - #[profiling::function] - pub fn present_image( - &mut self, - pool: &mut impl ResolverPool, - render_graph: RenderGraph, - swapchain_image: SwapchainImageNode, - queue_index: u32, - ) -> Result<(), DisplayError> { - trace!("present_image"); - - let mut resolver = render_graph.resolve(); - let wait_dst_stage_mask = resolver.node_pipeline_stages(swapchain_image); - - // The swapchain should have been written to, otherwise it would be noise and that's a panic - assert!( - !wait_dst_stage_mask.is_empty(), - "uninitialized swapchain image: write something each frame!", - ); - - let exec_idx = resolver.swapchain_image(swapchain_image).exec_idx; - let exec = &mut self.execs[exec_idx]; - - debug_assert!(exec.queue.is_none()); - - let started = Instant::now(); - - unsafe { - exec.cmd_buf - .device - .begin_command_buffer( - *exec.cmd_buf, - &vk::CommandBufferBeginInfo::default() - .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT), - ) - .map_err(|_| ())?; - } - - // resolver.record_node_dependencies(&mut *self.pool, cmd_buf, swapchain_image)?; - resolver.record_node(pool, &mut exec.cmd_buf, swapchain_image)?; - - { - let swapchain_image = resolver.swapchain_image(swapchain_image); - for (access, range) in Image::access( - swapchain_image, - AccessType::Present, - vk::ImageSubresourceRange { - aspect_mask: vk::ImageAspectFlags::COLOR, - base_array_layer: 0, - base_mip_level: 0, - layer_count: 1, - level_count: 1, - }, - ) { - trace!( - "image {:?} {:?}->{:?}", - **swapchain_image, - access, - AccessType::Present, - ); - - // Force a presentation layout transition - pipeline_barrier( - &exec.cmd_buf.device, - *exec.cmd_buf, - None, - &[], - slice::from_ref(&ImageBarrier { - previous_accesses: slice::from_ref(&access), - previous_layout: image_access_layout(access), - next_accesses: slice::from_ref(&AccessType::Present), - next_layout: image_access_layout(AccessType::Present), - discard_contents: false, - src_queue_family_index: vk::QUEUE_FAMILY_IGNORED, - dst_queue_family_index: vk::QUEUE_FAMILY_IGNORED, - image: ***swapchain_image, - range, - }), - ); - } - } - - // We may have unresolved nodes; things like copies that happen after present or operations - // before present which use nodes that are unused in the remainder of the graph. - // These operations are still important, but they don't need to wait for any of the above - // things so we do them last - resolver.record_unscheduled_passes(pool, &mut exec.cmd_buf)?; - - let queue = - exec.cmd_buf.device.queues[self.queue_family_idx as usize][queue_index as usize]; - - unsafe { - exec.cmd_buf - .device - .end_command_buffer(*exec.cmd_buf) - .map_err(|err| { - warn!("unable to end display command buffer: {err}"); - - DriverError::InvalidData - })?; - exec.cmd_buf - .device - .queue_submit( - queue, - slice::from_ref( - &vk::SubmitInfo::default() - .command_buffers(slice::from_ref(&exec.cmd_buf)) - .wait_semaphores(slice::from_ref(&exec.swapchain_acquired)) - .wait_dst_stage_mask(slice::from_ref(&wait_dst_stage_mask)) - .signal_semaphores(slice::from_ref(&exec.swapchain_rendered)), - ), - exec.cmd_buf.fence, - ) - .map_err(|err| { - warn!("unable to submit display command buffer: {err}"); - - DriverError::InvalidData - })? - } - - exec.cmd_buf.waiting = true; - exec.queue = Some(queue); - - let elapsed = Instant::now() - started; - trace!("🔜🔜🔜 vkQueueSubmit took {} μs", elapsed.as_micros(),); - - let swapchain_image = - SwapchainImage::clone_swapchain(resolver.swapchain_image(swapchain_image)); - - self.swapchain.present_image( - swapchain_image, - slice::from_ref(&exec.swapchain_rendered), - self.queue_family_idx, - queue_index, - ); - - // Store the resolved graph because it contains bindings, leases, and other shared resources - // that need to be kept alive until the fence is waited upon. - CommandBuffer::push_fenced_drop(&mut exec.cmd_buf, resolver); - - Ok(()) - } - - /// Sets information about the swapchain. - /// - /// Previously acquired swapchain images should be discarded after calling this function. - pub fn set_swapchain_info(&mut self, info: impl Into) { - self.swapchain.set_info(info); - } - - /// Gets information about the swapchain. - pub fn swapchain_info(&self) -> SwapchainInfo { - self.swapchain.info() - } -} - -impl Debug for Display { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str("Display") - } -} - -impl Drop for Display { - fn drop(&mut self) { - if panicking() { - return; - } - - let idle = unsafe { self.execs[0].cmd_buf.device.device_wait_idle() }; - if idle.is_err() { - warn!("unable to wait for device"); - - return; - } - - for batch in &mut self.execs { - if let Some(queue) = batch.queue { - // Wait for presentation to stop - let present = unsafe { batch.cmd_buf.device.queue_wait_idle(queue) }; - if present.is_err() { - warn!("unable to wait for queue"); - - continue; - } - } - - unsafe { - batch - .cmd_buf - .device - .destroy_semaphore(batch.swapchain_acquired, None); - batch - .cmd_buf - .device - .destroy_semaphore(batch.swapchain_rendered, None); - } - } - } -} - -/// Describes error conditions relating to physical displays. -#[derive(Debug)] -pub enum DisplayError { - /// Unrecoverable device error; must destroy this device and display and start a new one - DeviceLost, - - /// Recoverable driver error - Driver(DriverError), -} - -impl Error for DisplayError {} - -impl From<()> for DisplayError { - fn from(_: ()) -> Self { - Self::DeviceLost - } -} - -impl From for DisplayError { - fn from(err: DriverError) -> Self { - Self::Driver(err) - } -} - -impl std::fmt::Display for DisplayError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } -} - -/// Information used to create a [`Display`] instance. -#[derive(Builder, Clone, Copy, Debug, Eq, Hash, PartialEq)] -#[builder( - build_fn(private, name = "fallible_build", error = "DisplayInfoBuilderError"), - derive(Clone, Copy, Debug), - pattern = "owned" -)] -#[non_exhaustive] -pub struct DisplayInfo { - /// The number of command buffers to use for image submissions. - /// - /// Generally one more than the swapchain image count is best. - #[builder(default = "4")] - command_buffer_count: usize, - - /// The device queue family which will be used to submit and present images. - #[builder(default = "0")] - queue_family_index: u32, -} - -impl DisplayInfo { - /// Converts a `DisplayInfo` into a `DisplayInfoBuilder`. - #[inline(always)] - pub fn to_builder(self) -> DisplayInfoBuilder { - DisplayInfoBuilder { - command_buffer_count: Some(self.command_buffer_count), - queue_family_index: Some(self.queue_family_index), - } - } -} - -impl Default for DisplayInfo { - fn default() -> Self { - Self { - command_buffer_count: 4, - queue_family_index: 0, - } - } -} - -impl From for DisplayInfo { - fn from(info: DisplayInfoBuilder) -> Self { - info.build() - } -} - -impl DisplayInfoBuilder { - /// Builds a new `DisplayInfo`. - /// - /// # Panics - /// - /// If any of the following values have not been set this function will panic: - /// - /// * `command_buffer_count` - #[inline(always)] - pub fn build(self) -> DisplayInfo { - let info = match self.fallible_build() { - Err(DisplayInfoBuilderError(err)) => panic!("{err}"), - Ok(info) => info, - }; - - assert_ne!( - info.command_buffer_count, 0, - "Field value invalid: command_buffer_count" - ); - - info - } -} - -#[derive(Debug)] -struct DisplayInfoBuilderError(UninitializedFieldError); - -impl From for DisplayInfoBuilderError { - fn from(err: UninitializedFieldError) -> Self { - Self(err) - } -} - -struct Execution { - cmd_buf: CommandBuffer, - queue: Option, - swapchain_acquired: vk::Semaphore, - swapchain_rendered: vk::Semaphore, -} - -/// Combination trait which groups together all [`Pool`] traits required for a [`Resolver`] -/// instance. -/// -/// [`Resolver`]: crate::graph::Resolver -#[allow(private_bounds)] -pub trait ResolverPool: - Pool - + Pool - + Pool - + Send -{ -} - -impl ResolverPool for T where - T: Pool - + Pool - + Pool - + Send -{ -} - -#[cfg(test)] -mod tests { - use super::*; - - type Info = DisplayInfo; - type Builder = DisplayInfoBuilder; - - #[test] - pub fn display_info() { - let info = Info { - command_buffer_count: 42, - queue_family_index: 16, - }; - let builder = info.to_builder().build(); - - assert_eq!(info, builder); - } - - #[test] - pub fn display_info_builder() { - let info = Info { - command_buffer_count: 42, - queue_family_index: 16, - }; - let builder = Builder::default() - .command_buffer_count(42) - .queue_family_index(16) - .build(); - - assert_eq!(info, builder); - } - - #[test] - pub fn display_info_default() { - let info = Info::default(); - let builder = Builder::default().build(); - - assert_eq!(info, builder); - } - - #[test] - #[should_panic(expected = "Field value invalid: command_buffer_count")] - pub fn display_info_builder_uninit_command_buffer_count() { - Builder::default().command_buffer_count(0).build(); - } -} diff --git a/src/driver/accel_struct.rs b/src/driver/accel_struct.rs index d2624fee..5345ae96 100644 --- a/src/driver/accel_struct.rs +++ b/src/driver/accel_struct.rs @@ -8,8 +8,6 @@ use { std::{ ffi::c_void, mem::{replace, size_of_val}, - ops::Deref, - sync::Arc, thread::panicking, }, vk_sync::AccessType, @@ -25,40 +23,48 @@ use std::sync::Mutex; /// /// Also contains the backing buffer and information about the object. /// -/// ## `Deref` behavior -/// -/// `AccelerationStructure` automatically dereferences to [`vk::AccelerationStructureKHR`] (via the -/// [`Deref`] trait), so you can call `vk::AccelerationStructureKHR`'s methods on a value of -/// type `AccelerationStructure`. To avoid name clashes with `vk::AccelerationStructureKHR`'s -/// methods, the methods of `AccelerationStructure` itself are associated functions, called using -/// [fully qualified syntax]: -/// /// ```no_run -/// # use std::sync::Arc; /// # use ash::vk; -/// # use screen_13::driver::{AccessType, DriverError}; -/// # use screen_13::driver::device::{Device, DeviceInfo}; -/// # use screen_13::driver::accel_struct::{AccelerationStructure, AccelerationStructureInfo}; +/// # use vk_graph::driver::DriverError; +/// # use vk_graph::driver::device::{Device, DeviceInfo}; +/// # use vk_graph::driver::accel_struct::{AccelerationStructure, AccelerationStructureInfo}; /// # fn main() -> Result<(), DriverError> { -/// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); -/// # const SIZE: vk::DeviceSize = 1024; -/// # let info = AccelerationStructureInfo::blas(SIZE); -/// # let my_accel_struct = AccelerationStructure::create(&device, info)?; -/// let addr = AccelerationStructure::device_address(&my_accel_struct); +/// # let device = Device::new(DeviceInfo::default())?; +/// let info = AccelerationStructureInfo::blas(0); +/// let accel_struct = AccelerationStructure::create(&device, info)?; +/// let addr = accel_struct.device_address(); +/// +/// assert_eq!(accel_struct.info, info); +/// assert_ne!(accel_struct.handle, vk::AccelerationStructureKHR::null()); /// # Ok(()) } /// ``` /// -/// [acceleration structure]: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkAccelerationStructureKHR.html -/// [deref]: core::ops::Deref -/// [fully qualified syntax]: https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#fully-qualified-syntax-for-disambiguation-calling-methods-with-the-same-name +/// See [`VkAccelerationStructureKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkAccelerationStructureKHR.html). #[derive(Debug)] +#[read_only::cast] pub struct AccelerationStructure { access: Mutex, - accel_struct: (vk::AccelerationStructureKHR, Buffer), - device: Arc, + + /// The native Vulkan resource handle of the buffer which supports this acceleration structure. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub buffer: Buffer, + + /// The native Vulkan resource handle of this acceleration structure. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub handle: vk::AccelerationStructureKHR, /// Information used to create this object. + /// + /// _Note:_ This field is read-only. + #[readonly] pub info: AccelerationStructureInfo, + + /// A name for debugging purposes. + pub name: Option, } impl AccelerationStructure { @@ -71,22 +77,22 @@ impl AccelerationStructure { /// ```no_run /// # use std::sync::Arc; /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::accel_struct::{AccelerationStructure, AccelerationStructureInfo}; + /// # use vk_graph::driver::DriverError; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::accel_struct::{AccelerationStructure, AccelerationStructureInfo}; /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); + /// # let device = Device::new(DeviceInfo::default())?; /// const SIZE: vk::DeviceSize = 1024; /// let info = AccelerationStructureInfo::blas(SIZE); /// let accel_struct = AccelerationStructure::create(&device, info)?; /// - /// assert_ne!(*accel_struct, vk::AccelerationStructureKHR::null()); + /// assert_ne!(accel_struct.handle, vk::AccelerationStructureKHR::null()); /// assert_eq!(accel_struct.info.size, SIZE); /// # Ok(()) } /// ``` #[profiling::function] pub fn create( - device: &Arc, + device: &Device, info: impl Into, ) -> Result { debug_assert!(device.physical_device.accel_struct_properties.is_some()); @@ -102,36 +108,39 @@ impl AccelerationStructure { ), )?; - let accel_struct = { + let handle = { let create_info = vk::AccelerationStructureCreateInfoKHR::default() .ty(info.ty) - .buffer(*buffer) + .buffer(buffer.handle) .size(info.size); let accel_struct_ext = Device::expect_accel_struct_ext(device); unsafe { accel_struct_ext.create_acceleration_structure(&create_info, None) }.map_err( |err| { - warn!("{err}"); + warn!("unable to create acceleration structure: {err}"); match err { vk::Result::ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS => { + warn!("invalid acceleration structure opaque capture address: {err}"); DriverError::InvalidData } vk::Result::ERROR_OUT_OF_HOST_MEMORY => DriverError::OutOfMemory, - _ => DriverError::Unsupported, + _ => { + warn!("unsupported acceleration structure creation: {err}"); + DriverError::Unsupported + } } }, )? }; - let device = Arc::clone(device); - - Ok(AccelerationStructure { + Ok(Self { access: Mutex::new(AccessType::Nothing), - accel_struct: (accel_struct, buffer), - device, + buffer, + handle, info, + name: None, }) } @@ -142,7 +151,7 @@ impl AccelerationStructure { /// /// # Note /// - /// Used to maintain object state when passing a _Screen 13_-created + /// Used to maintain object state when passing a _vk-graph_-created /// `vk::AccelerationStructureKHR` handle to external code such as [_Ash_] or [_Erupt_] /// bindings. /// @@ -153,11 +162,11 @@ impl AccelerationStructure { /// ```no_run /// # use std::sync::Arc; /// # use ash::vk; - /// # use screen_13::driver::{AccessType, DriverError}; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::accel_struct::{AccelerationStructure, AccelerationStructureInfo}; + /// # use vk_graph::driver::{AccessType, DriverError}; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::accel_struct::{AccelerationStructure, AccelerationStructureInfo}; /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); + /// # let device = Device::new(DeviceInfo::default())?; /// # const SIZE: vk::DeviceSize = 1024; /// # let info = AccelerationStructureInfo::blas(SIZE); /// # let my_accel_struct = AccelerationStructure::create(&device, info)?; @@ -180,14 +189,15 @@ impl AccelerationStructure { /// [_Ash_]: https://crates.io/crates/ash /// [_Erupt_]: https://crates.io/crates/erupt #[profiling::function] - pub fn access(this: &Self, access: AccessType) -> AccessType { - #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] - let mut access_guard = this.access.lock(); + pub fn access(&self, next_access: AccessType) -> AccessType { + self.with_access(|prev_access| replace(prev_access, next_access)) + } - #[cfg(not(feature = "parking_lot"))] - let mut access_guard = access_guard.unwrap(); + /// Sets the debugging name assigned to this acceleration structure. + pub fn debug_name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); - replace(&mut access_guard, access) + self } /// Returns the device address of this object. @@ -199,11 +209,11 @@ impl AccelerationStructure { /// ```no_run /// # use std::sync::Arc; /// # use ash::vk; - /// # use screen_13::driver::{AccessType, DriverError}; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::accel_struct::{AccelerationStructure, AccelerationStructureInfo}; + /// # use vk_graph::driver::{AccessType, DriverError}; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::accel_struct::{AccelerationStructure, AccelerationStructureInfo}; /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); + /// # let device = Device::new(DeviceInfo::default())?; /// # const SIZE: vk::DeviceSize = 1024; /// # let info = AccelerationStructureInfo::blas(SIZE); /// # let my_accel_struct = AccelerationStructure::create(&device, info)?; @@ -213,13 +223,13 @@ impl AccelerationStructure { /// # Ok(()) } /// ``` #[profiling::function] - pub fn device_address(this: &Self) -> vk::DeviceAddress { - let accel_struct_ext = Device::expect_accel_struct_ext(&this.device); + pub fn device_address(&self) -> vk::DeviceAddress { + let accel_struct_ext = Device::expect_accel_struct_ext(&self.buffer.device); unsafe { accel_struct_ext.get_acceleration_structure_device_address( &vk::AccelerationStructureDeviceAddressInfoKHR::default() - .acceleration_structure(this.accel_struct.0), + .acceleration_structure(self.handle), ) } } @@ -241,11 +251,17 @@ impl AccelerationStructure { /// ```no_run /// # use std::sync::Arc; /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::accel_struct::{AccelerationStructure, AccelerationStructureGeometry, AccelerationStructureGeometryData, AccelerationStructureGeometryInfo, DeviceOrHostAddress}; + /// # use vk_graph::driver::DriverError; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::accel_struct::{ + /// # AccelerationStructure, + /// # AccelerationStructureGeometry, + /// # AccelerationStructureGeometryData, + /// # AccelerationStructureGeometryInfo, + /// # DeviceOrHostAddress, + /// # }; /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); + /// # let device = Device::new(DeviceInfo::default())?; /// # let my_geom_triangles = AccelerationStructureGeometryData::Triangles { /// # index_addr: DeviceOrHostAddress::DeviceAddress(0), /// # index_type: vk::IndexType::UINT32, @@ -323,13 +339,16 @@ impl AccelerationStructure { } }) } -} -impl Deref for AccelerationStructure { - type Target = vk::AccelerationStructureKHR; + fn with_access(&self, f: impl FnOnce(&mut AccessType) -> R) -> R { + let access = self.access.lock(); + + #[cfg(not(feature = "parking_lot"))] + let access = access.expect("poisoned acceleration structure access lock"); - fn deref(&self) -> &Self::Target { - &self.accel_struct.0 + let mut access = access; + + f(&mut access) } } @@ -340,19 +359,25 @@ impl Drop for AccelerationStructure { return; } - let accel_struct_ext = Device::expect_accel_struct_ext(&self.device); + let accel_struct_ext = Device::expect_accel_struct_ext(&self.buffer.device); unsafe { - accel_struct_ext.destroy_acceleration_structure(self.accel_struct.0, None); + accel_struct_ext.destroy_acceleration_structure(self.handle, None); } } } +impl Eq for AccelerationStructure {} + +impl PartialEq for AccelerationStructure { + fn eq(&self, other: &Self) -> bool { + self.handle == other.handle + } +} + /// Structure specifying geometries to be built into an acceleration structure. /// -/// See -/// [VkAccelerationStructureGeometryKHR](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkAccelerationStructureGeometryKHR.html) -/// for more information. +/// See [`VkAccelerationStructureGeometryKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkAccelerationStructureGeometryKHR.html). #[derive(Clone, Copy, Debug)] pub struct AccelerationStructureGeometry { /// The number of primitives built into each geometry. @@ -414,16 +439,13 @@ impl From for vk::AccelerationStructureGeometryKH /// Specifies acceleration structure geometry data. /// -/// See -/// [VkAccelerationStructureGeometryDataKHR](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkAccelerationStructureGeometryDataKHR.html) -/// for more information. +/// See [`VkAccelerationStructureGeometryKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkAccelerationStructureGeometryKHR.html). #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum AccelerationStructureGeometryData { /// Axis-aligned bounding box geometry in a bottom-level acceleration structure. /// /// See - /// [VkAccelerationStructureGeometryAabbsDataKHR](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkAccelerationStructureGeometryAabbsDataKHR.html) - /// for more information. + /// See [`VkAccelerationStructureGeometryAabbsDataKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkAccelerationStructureGeometryAabbsDataKHR.html). AABBs { /// A device or host address to memory containing [vk::AabbPositionsKHR] structures /// containing position data for each axis-aligned bounding box in the geometry. @@ -437,13 +459,12 @@ pub enum AccelerationStructureGeometryData { /// Geometry consisting of instances of other acceleration structures. /// - /// See [VkAccelerationStructureGeometryInstancesDataKHR](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkAccelerationStructureGeometryInstancesDataKHR.html) - /// for more information. + /// See [`VkAccelerationStructureGeometryInstancesDataKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkAccelerationStructureGeometryInstancesDataKHR.html). Instances { /// Either the address of an array of device referencing individual /// VkAccelerationStructureInstanceKHR structures or packed motion instance information as /// described in - /// [motion instances](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#acceleration-structure-motion-instances) + /// See [`VkAccelerationStructureInstanceKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkAccelerationStructureInstanceKHR.html). /// if `array_of_pointers` is `true`, or the address of an array of /// VkAccelerationStructureInstanceKHR structures. /// @@ -456,23 +477,22 @@ pub enum AccelerationStructureGeometryData { /// A triangle geometry in a bottom-level acceleration structure. /// - /// See - /// [VkAccelerationStructureGeometryTrianglesDataKHR](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkAccelerationStructureGeometryTrianglesDataKHR.html) - /// for more information. + /// See [`VkAccelerationStructureGeometryTrianglesDataKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkAccelerationStructureGeometryTrianglesDataKHR.html). Triangles { /// A device or host address to memory containing index data for this geometry. index_addr: DeviceOrHostAddress, /// The - /// [VkIndexType](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkIndexType.html) + /// See [`VkIndexType`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkIndexType.html). /// of each index element. index_type: vk::IndexType, - /// The highest index of a vertex that will be addressed by a build command using this structure. + /// The highest index of a vertex that will be addressed by a build command using this + /// structure. max_vertex: u32, /// A device or host address to memory containing an optional reference to a - /// [VkTransformMatrixKHR](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkTransformMatrixKHR.html) + /// See [`VkTransformMatrixKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkTransformMatrixKHR.html). /// structure describing a transformation from the space in which the vertices in this /// geometry are described to the space in which the acceleration structure is defined. transform_addr: Option, @@ -481,7 +501,7 @@ pub enum AccelerationStructureGeometryData { vertex_addr: DeviceOrHostAddress, /// The - /// [VkFormat](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkFormat.html) + /// See [`VkFormat`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkFormat.html). /// of each vertex element. vertex_format: vk::Format, @@ -647,7 +667,6 @@ impl AccelerationStructureGeometryInfo { derive(Clone, Copy, Debug), pattern = "owned" )] -#[non_exhaustive] pub struct AccelerationStructureInfo { /// Type of acceleration structure. #[builder(default = "vk::AccelerationStructureTypeKHR::GENERIC")] @@ -671,7 +690,6 @@ impl AccelerationStructureInfo { } /// Creates a default `AccelerationStructureInfoBuilder`. - #[allow(clippy::new_ret_no_self)] pub fn builder() -> AccelerationStructureInfoBuilder { Default::default() } @@ -687,13 +705,24 @@ impl AccelerationStructureInfo { } /// Converts an `AccelerationStructureInfo` into an `AccelerationStructureInfoBuilder`. - #[inline(always)] - pub fn to_builder(self) -> AccelerationStructureInfoBuilder { + pub fn into_builder(self) -> AccelerationStructureInfoBuilder { AccelerationStructureInfoBuilder { ty: Some(self.ty), size: Some(self.size), } } + + #[deprecated = "use into_builder function"] + #[doc(hidden)] + pub fn to_builder(self) -> AccelerationStructureInfoBuilder { + self.into_builder() + } +} + +impl From for AccelerationStructureInfo { + fn from(info: AccelerationStructureInfoBuilder) -> Self { + info.build() + } } impl From for () { @@ -730,8 +759,7 @@ impl From for AccelerationStructureInfoBuilderError { #[derive(Clone, Copy, Debug)] pub struct AccelerationStructureSize { /// The size of the scratch buffer required when building an acceleration structure using the - /// [`Acceleration::build_structure`](super::super::graph::pass_ref::Acceleration::build_structure) - /// function. + /// `Acceleration::build_structure` function. pub build_size: vk::DeviceSize, /// The value of `size` parameter needed by [`AccelerationStructureInfo`] for use with the @@ -739,16 +767,13 @@ pub struct AccelerationStructureSize { pub create_size: vk::DeviceSize, /// The size of the scratch buffer required when updating an acceleration structure using the - /// [`Acceleration::update_structure`](super::super::graph::pass_ref::Acceleration::update_structure) - /// function. + /// `Acceleration::update_structure` function. pub update_size: vk::DeviceSize, } /// Specifies a constant device or host address. /// -/// See -/// [VkDeviceOrHostAddressKHR](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkDeviceOrHostAddressKHR.html) -/// for more information. +/// See [`VkDeviceOrHostAddressKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkDeviceOrHostAddressKHR.html). #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum DeviceOrHostAddress { /// An address value returned from [`AccelerationStructure::device_address`]. @@ -793,7 +818,7 @@ impl From for vk::DeviceOrHostAddressKHR { } #[cfg(test)] -mod tests { +mod test { use super::*; type Info = AccelerationStructureInfo; @@ -802,7 +827,7 @@ mod tests { #[test] pub fn accel_struct_info() { let info = Info::blas(32); - let builder = info.to_builder().build(); + let builder = info.into_builder().build(); assert_eq!(info, builder); } diff --git a/src/driver/buffer.rs b/src/driver/buffer.rs index d8ba332a..26018956 100644 --- a/src/driver/buffer.rs +++ b/src/driver/buffer.rs @@ -13,54 +13,59 @@ use { std::{ fmt::{Debug, Formatter}, mem::ManuallyDrop, - ops::{Deref, DerefMut, Range}, - sync::Arc, + ops::{DerefMut, Range}, thread::panicking, }, vk_sync::AccessType, }; #[cfg(feature = "parking_lot")] -use parking_lot::Mutex; +use parking_lot::{Mutex, MutexGuard}; #[cfg(not(feature = "parking_lot"))] -use std::sync::Mutex; +use std::sync::{Mutex, MutexGuard}; /// Smart pointer handle to a [buffer] object. /// /// Also contains information about the object. /// -/// ## `Deref` behavior -/// -/// `Buffer` automatically dereferences to [`vk::Buffer`] (via the [`Deref`] trait), so you -/// can call `vk::Buffer`'s methods on a value of type `Buffer`. To avoid name clashes with -/// `vk::Buffer`'s methods, the methods of `Buffer` itself are associated functions, called using -/// [fully qualified syntax]: -/// /// ```no_run -/// # use std::sync::Arc; /// # use ash::vk; -/// # use screen_13::driver::{AccessType, DriverError}; -/// # use screen_13::driver::device::{Device, DeviceInfo}; -/// # use screen_13::driver::buffer::{Buffer, BufferInfo}; +/// # use vk_graph::driver::DriverError; +/// # use vk_graph::driver::device::{Device, DeviceInfo}; +/// # use vk_graph::driver::buffer::{Buffer, BufferInfo}; /// # fn main() -> Result<(), DriverError> { -/// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); -/// # let info = BufferInfo::device_mem(8, vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS); -/// # let my_buf = Buffer::create(&device, info)?; -/// let addr = Buffer::device_address(&my_buf); +/// # let device = Device::new(DeviceInfo::default())?; +/// let info = BufferInfo::device_mem(1_024, vk::BufferUsageFlags::STORAGE_BUFFER); +/// let my_buf = Buffer::create(&device, info)?; +/// +/// assert_eq!(my_buf.info, info); +/// assert_ne!(my_buf.handle, vk::Buffer::null()); /// # Ok(()) } /// ``` /// /// [buffer]: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkBuffer.html -/// [deref]: core::ops::Deref -/// [fully qualified syntax]: https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#fully-qualified-syntax-for-disambiguation-calling-methods-with-the-same-name +#[read_only::cast] pub struct Buffer { accesses: Mutex, allocation: ManuallyDrop, - buffer: vk::Buffer, - device: Arc, - /// Information used to create this object. + /// The device which owns this buffer resource. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub device: Device, + + /// The native Vulkan resource handle of this buffer. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub handle: vk::Buffer, + + /// Information used to create this resource. + /// + /// _Note:_ This field is read-only. + #[readonly] pub info: BufferInfo, /// A name for debugging purposes. @@ -77,102 +82,109 @@ impl Buffer { /// ```no_run /// # use std::sync::Arc; /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; + /// # use vk_graph::driver::DriverError; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::buffer::{Buffer, BufferInfo}; /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); + /// # let device = Device::new(DeviceInfo::default())?; /// const SIZE: vk::DeviceSize = 1024; /// let info = BufferInfo::host_mem(SIZE, vk::BufferUsageFlags::UNIFORM_BUFFER); /// let buf = Buffer::create(&device, info)?; /// - /// assert_ne!(*buf, vk::Buffer::null()); + /// assert_ne!(buf.handle, vk::Buffer::null()); /// assert_eq!(buf.info.size, SIZE); /// # Ok(()) } /// ``` #[profiling::function] - pub fn create(device: &Arc, info: impl Into) -> Result { + pub fn create(device: &Device, info: impl Into) -> Result { let info = info.into(); trace!("create: {:?}", info); debug_assert_ne!(info.size, 0, "Size must be non-zero"); - let device = Arc::clone(device); + let device = device.clone(); let buffer_info = vk::BufferCreateInfo::default() .size(info.size) .usage(info.usage) .sharing_mode(vk::SharingMode::CONCURRENT) .queue_family_indices(&device.physical_device.queue_family_indices); - let buffer = unsafe { + let handle = unsafe { device.create_buffer(&buffer_info, None).map_err(|err| { warn!("unable to create buffer: {err}"); DriverError::Unsupported })? }; - let mut requirements = unsafe { device.get_buffer_memory_requirements(buffer) }; + let mut requirements = unsafe { device.get_buffer_memory_requirements(handle) }; requirements.alignment = requirements.alignment.max(info.alignment); - let memory_location = if info.mappable { + let allocation_scheme = if info.dedicated { + AllocationScheme::DedicatedBuffer(handle) + } else { + AllocationScheme::GpuAllocatorManaged + }; + let location = if info.host_write { MemoryLocation::CpuToGpu + } else if info.host_read { + MemoryLocation::GpuToCpu } else { MemoryLocation::GpuOnly }; let allocation = { profiling::scope!("allocate"); - #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] - let mut allocator = device.allocator.lock(); - - #[cfg(not(feature = "parking_lot"))] - let mut allocator = allocator.unwrap(); - - allocator - .allocate(&AllocationCreateDesc { - name: "buffer", - requirements, - location: memory_location, - linear: true, // Buffers are always linear - allocation_scheme: AllocationScheme::GpuAllocatorManaged, - }) - .map_err(|err| { - warn!("unable to allocate buffer memory: {err}"); - - unsafe { - device.destroy_buffer(buffer, None); - } - - DriverError::from_alloc_err(err) - }) - .and_then(|allocation| { - if let Err(err) = unsafe { - device.bind_buffer_memory(buffer, allocation.memory(), allocation.offset()) - } { - warn!("unable to bind buffer memory: {err}"); - - if let Err(err) = allocator.free(allocation) { - warn!("unable to free buffer allocation: {err}") - } + Device::with_allocator(&device, |allocator| { + allocator + .allocate(&AllocationCreateDesc { + name: "buffer", + requirements, + location, + linear: true, // Buffers are always linear + allocation_scheme, + }) + .map_err(|err| { + warn!("unable to allocate buffer memory: {err}"); unsafe { - device.destroy_buffer(buffer, None); + device.destroy_buffer(handle, None); } - Err(DriverError::OutOfMemory) - } else { - Ok(allocation) - } - }) + DriverError::from_alloc_err(err) + }) + .and_then(|allocation| { + if let Err(err) = unsafe { + device.bind_buffer_memory( + handle, + allocation.memory(), + allocation.offset(), + ) + } { + warn!("unable to bind buffer memory: {err}"); + + if let Err(err) = allocator.free(allocation) { + warn!("unable to free buffer allocation: {err}") + } + + unsafe { + device.destroy_buffer(handle, None); + } + + Err(DriverError::OutOfMemory) + } else { + Ok(allocation) + } + }) + }) }?; - debug_assert_ne!(buffer, vk::Buffer::null()); + debug_assert_ne!(handle, vk::Buffer::null()); Ok(Self { accesses: Mutex::new(BufferAccess::new(info.size)), allocation: ManuallyDrop::new(allocation), - buffer, device, + handle, info, name: None, }) @@ -187,30 +199,29 @@ impl Buffer { /// ```no_run /// # use std::sync::Arc; /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; + /// # use vk_graph::driver::DriverError; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::buffer::{Buffer, BufferInfo}; /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); + /// # let device = Device::new(DeviceInfo::default())?; /// const DATA: [u8; 4] = [0xfe, 0xed, 0xbe, 0xef]; /// let buf = Buffer::create_from_slice(&device, vk::BufferUsageFlags::UNIFORM_BUFFER, &DATA)?; /// - /// assert_ne!(*buf, vk::Buffer::null()); + /// assert_ne!(buf.handle, vk::Buffer::null()); /// assert_eq!(buf.info.size, 4); /// assert_eq!(Buffer::mapped_slice(&buf), &DATA); /// # Ok(()) } /// ``` #[profiling::function] pub fn create_from_slice( - device: &Arc, + device: &Device, usage: vk::BufferUsageFlags, - slice: impl AsRef<[u8]>, + data: &[u8], ) -> Result { - let slice = slice.as_ref(); - let info = BufferInfo::host_mem(slice.len() as _, usage); + let info = BufferInfo::host_mem(data.len() as _, usage); let mut buffer = Self::create(device, info)?; - Self::copy_from_slice(&mut buffer, 0, slice); + Self::copy_from_slice(&mut buffer, 0, data); Ok(buffer) } @@ -222,7 +233,7 @@ impl Buffer { /// /// # Note /// - /// Used to maintain object state when passing a _Screen 13_-created `vk::Buffer` handle to + /// Used to maintain object state when passing a _vk-graph_-created `vk::Buffer` handle to /// external code such as [_Ash_] or [_Erupt_] bindings. /// /// # Examples @@ -232,11 +243,11 @@ impl Buffer { /// ```no_run /// # use std::sync::Arc; /// # use ash::vk; - /// # use screen_13::driver::{AccessType, DriverError}; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::buffer::{Buffer, BufferInfo, BufferSubresourceRange}; + /// # use vk_graph::driver::{AccessType, DriverError}; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::buffer::{Buffer, BufferInfo, BufferSubresourceRange}; /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); + /// # let device = Device::new(DeviceInfo::default())?; /// # const SIZE: vk::DeviceSize = 1024; /// # let info = BufferInfo::device_mem(SIZE, vk::BufferUsageFlags::STORAGE_BUFFER); /// # let my_buf = Buffer::create(&device, info)?; @@ -266,22 +277,17 @@ impl Buffer { /// [_Erupt_]: https://crates.io/crates/erupt #[profiling::function] pub fn access( - this: &Self, + &self, access: AccessType, access_range: impl Into, ) -> impl Iterator + '_ { let mut access_range: BufferSubresourceRange = access_range.into(); if access_range.end == vk::WHOLE_SIZE { - access_range.end = this.info.size; + access_range.end = self.info.size; } - let accesses = this.accesses.lock(); - - #[cfg(not(feature = "parking_lot"))] - let accesses = accesses.unwrap(); - - BufferAccessIter::new(accesses, access, access_range) + BufferAccessIter::new(self.lock_accesses(), access, access_range) } /// Updates a mappable buffer starting at `offset` with the data in `slice`. @@ -297,11 +303,11 @@ impl Buffer { /// ```no_run /// # use std::sync::Arc; /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; + /// # use vk_graph::driver::DriverError; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::buffer::{Buffer, BufferInfo}; /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); + /// # let device = Device::new(DeviceInfo::default())?; /// # let info = BufferInfo::host_mem(4, vk::BufferUsageFlags::empty()); /// # let mut my_buf = Buffer::create(&device, info)?; /// const DATA: [u8; 4] = [0xde, 0xad, 0xc0, 0xde]; @@ -311,10 +317,11 @@ impl Buffer { /// # Ok(()) } /// ``` #[profiling::function] - pub fn copy_from_slice(this: &mut Self, offset: vk::DeviceSize, slice: impl AsRef<[u8]>) { - let slice = slice.as_ref(); - Self::mapped_slice_mut(this)[offset as _..offset as usize + slice.len()] - .copy_from_slice(slice); + pub fn copy_from_slice(&mut self, offset: vk::DeviceSize, data: &[u8]) { + let range = offset as _..offset as usize + data.len(); + let mapped_data = self.mapped_slice_mut(); + + mapped_data[range].copy_from_slice(data); } /// Returns the device address of this object. @@ -330,33 +337,42 @@ impl Buffer { /// ```no_run /// # use std::sync::Arc; /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; + /// # use vk_graph::driver::DriverError; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::buffer::{Buffer, BufferInfo}; /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); + /// # let device = Device::new(DeviceInfo::default())?; /// # let info = BufferInfo::host_mem(4, vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS); /// # let my_buf = Buffer::create(&device, info)?; - /// let addr = Buffer::device_address(&my_buf); + /// let addr = my_buf.device_address(); /// /// assert_ne!(addr, 0); /// # Ok(()) } /// ``` #[profiling::function] - pub fn device_address(this: &Self) -> vk::DeviceAddress { + pub fn device_address(&self) -> vk::DeviceAddress { debug_assert!( - this.info + self.info .usage .contains(vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS) ); unsafe { - this.device.get_buffer_device_address( - &vk::BufferDeviceAddressInfo::default().buffer(this.buffer), + self.device.get_buffer_device_address( + &vk::BufferDeviceAddressInfo::default().buffer(self.handle), ) } } + fn lock_accesses(&self) -> MutexGuard<'_, BufferAccess> { + let accesses = self.accesses.lock(); + + #[cfg(not(feature = "parking_lot"))] + let accesses = accesses.expect("poisoned buffer access lock"); + + accesses + } + /// Returns a mapped slice. /// /// # Panics @@ -370,11 +386,11 @@ impl Buffer { /// ```no_run /// # use std::sync::Arc; /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; + /// # use vk_graph::driver::DriverError; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::buffer::{Buffer, BufferInfo}; /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); + /// # let device = Device::new(DeviceInfo::default())?; /// # const DATA: [u8; 4] = [0; 4]; /// # let my_buf = Buffer::create_from_slice(&device, vk::BufferUsageFlags::empty(), &DATA)?; /// // my_buf is mappable and filled with four zeroes @@ -385,13 +401,16 @@ impl Buffer { /// # Ok(()) } /// ``` #[profiling::function] - pub fn mapped_slice(this: &Self) -> &[u8] { + pub fn mapped_slice(&self) -> &[u8] { debug_assert!( - this.info.mappable, - "Buffer is not mappable - create using mappable flag" + self.info.host_read, + "Buffer is not readable - create using host_read flag" ); - &this.allocation.mapped_slice().unwrap()[0..this.info.size as usize] + &self + .allocation + .mapped_slice() + .expect("missing mapped buffer memory")[0..self.info.size as usize] } /// Returns a mapped mutable slice. @@ -408,13 +427,17 @@ impl Buffer { /// # use std::sync::Arc; /// # use ash::vk; /// # use glam::Mat4; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; + /// # use vk_graph::driver::DriverError; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::buffer::{Buffer, BufferInfo}; /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); + /// # let device = Device::new(DeviceInfo::default())?; /// # const DATA: [u8; 4] = [0; 4]; - /// # let mut my_buf = Buffer::create_from_slice(&device, vk::BufferUsageFlags::empty(), &DATA)?; + /// # let mut my_buf = Buffer::create_from_slice( + /// # &device, + /// # vk::BufferUsageFlags::empty(), + /// # &DATA, + /// # )?; /// let mut data = Buffer::mapped_slice_mut(&mut my_buf); /// data.copy_from_slice(&42f32.to_be_bytes()); /// @@ -423,34 +446,36 @@ impl Buffer { /// # Ok(()) } /// ``` #[profiling::function] - pub fn mapped_slice_mut(this: &mut Self) -> &mut [u8] { + pub fn mapped_slice_mut(&mut self) -> &mut [u8] { debug_assert!( - this.info.mappable, - "Buffer is not mappable - create using mappable flag" + self.info.host_write, + "Buffer is not writable - create using host_write flag" ); - &mut this.allocation.mapped_slice_mut().unwrap()[0..this.info.size as usize] + &mut self + .allocation + .mapped_slice_mut() + .expect("missing mapped buffer memory")[0..self.info.size as usize] + } + + /// Sets the debugging name assigned to this buffer. + pub fn debug_name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + + self } } impl Debug for Buffer { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if let Some(name) = &self.name { - write!(f, "{} ({:?})", name, self.buffer) + write!(f, "{} ({:?})", name, self.handle) } else { - write!(f, "{:?}", self.buffer) + write!(f, "{:?}", self.handle) } } } -impl Deref for Buffer { - type Target = vk::Buffer; - - fn deref(&self) -> &Self::Target { - &self.buffer - } -} - impl Drop for Buffer { #[profiling::function] fn drop(&mut self) { @@ -461,22 +486,26 @@ impl Drop for Buffer { { profiling::scope!("deallocate"); - #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] - let mut allocator = self.device.allocator.lock(); - - #[cfg(not(feature = "parking_lot"))] - let mut allocator = allocator.unwrap(); - - allocator.free(unsafe { ManuallyDrop::take(&mut self.allocation) }) + Device::with_allocator(&self.device, |allocator| { + allocator.free(unsafe { ManuallyDrop::take(&mut self.allocation) }) + }) } .unwrap_or_else(|err| warn!("unable to free buffer allocation: {err}")); unsafe { - self.device.destroy_buffer(self.buffer, None); + self.device.destroy_buffer(self.handle, None); } } } +impl Eq for Buffer {} + +impl PartialEq for Buffer { + fn eq(&self, other: &Self) -> bool { + self.handle == other.handle + } +} + #[derive(Debug)] struct BufferAccess { accesses: Vec<(AccessType, vk::DeviceSize)>, @@ -702,7 +731,6 @@ where derive(Clone, Copy, Debug), pattern = "owned" )] -#[non_exhaustive] pub struct BufferInfo { /// Byte alignment of the base device address of the buffer. /// @@ -710,9 +738,25 @@ pub struct BufferInfo { #[builder(default = "1")] pub alignment: vk::DeviceSize, + /// Specifies a dedicated memory allocation managed by the Vulkan driver and not by the + /// internal memory allocation pool transient resources share. + /// + /// The driver may optimize access to dedicated buffers. + #[builder(default)] + pub dedicated: bool, + + /// Specifies a buffer whose memory is host visible and may be mapped. + /// + /// Memory optimal for CPU readback of data may be used. + #[builder(default)] + pub host_read: bool, + /// Specifies a buffer whose memory is host visible and may be mapped. + /// + /// Memory optimal for uploading data to the GPU and potentially for constant buffers may be + /// used. #[builder(default)] - pub mappable: bool, + pub host_write: bool, /// Size in bytes of the buffer to be created. pub size: vk::DeviceSize, @@ -730,7 +774,9 @@ impl BufferInfo { pub const fn device_mem(size: vk::DeviceSize, usage: vk::BufferUsageFlags) -> BufferInfo { BufferInfo { alignment: 1, - mappable: false, + dedicated: false, + host_read: false, + host_write: false, size, usage, } @@ -754,42 +800,47 @@ impl BufferInfo { BufferInfo { alignment: 1, - mappable: true, + dedicated: false, + host_read: true, + host_write: true, size, usage, } } - /// Specifies a non-mappable buffer with the given `size` and `usage` values. - #[allow(clippy::new_ret_no_self)] - #[deprecated = "Use BufferInfo::device_mem()"] - #[doc(hidden)] - pub fn new(size: vk::DeviceSize, usage: vk::BufferUsageFlags) -> BufferInfoBuilder { - Self::device_mem(size, usage).to_builder() + /// Creates a default `BufferInfoBuilder`. + pub fn builder() -> BufferInfoBuilder { + Default::default() } - /// Specifies a mappable buffer with the given `size` and `usage` values. - /// - /// # Note - /// - /// For convenience the given usage value will be bitwise OR'd with - /// `TRANSFER_DST | TRANSFER_SRC`. - #[deprecated = "Use BufferInfo::host_mem()"] - #[doc(hidden)] - pub fn new_mappable(size: vk::DeviceSize, usage: vk::BufferUsageFlags) -> BufferInfoBuilder { - Self::host_mem(size, usage).to_builder() + /// Returns `true` if this information specifies host-accessible memory. + pub fn is_host_mem(&self) -> bool { + self.host_read | self.host_write } /// Converts a `BufferInfo` into a `BufferInfoBuilder`. - #[inline(always)] - pub fn to_builder(self) -> BufferInfoBuilder { + pub fn into_builder(self) -> BufferInfoBuilder { BufferInfoBuilder { alignment: Some(self.alignment), - mappable: Some(self.mappable), + dedicated: Some(self.dedicated), + host_read: Some(self.host_read), + host_write: Some(self.host_write), size: Some(self.size), usage: Some(self.usage), } } + + #[deprecated = "use into_builder function"] + #[doc(hidden)] + pub fn to_builder(self) -> BufferInfoBuilder { + self.into_builder() + } +} + +impl From for BufferInfo { + fn from(info: BufferInfoBuilder) -> Self { + info.build() + } } impl BufferInfoBuilder { @@ -809,9 +860,8 @@ impl BufferInfoBuilder { Ok(info) => info, }; - assert_eq!( - res.alignment.count_ones(), - 1, + assert!( + res.alignment.is_power_of_two(), "Alignment must be a power of two" ); @@ -819,12 +869,6 @@ impl BufferInfoBuilder { } } -impl From for BufferInfo { - fn from(info: BufferInfoBuilder) -> Self { - info.build() - } -} - #[derive(Debug)] struct BufferInfoBuilderError(UninitializedFieldError); @@ -869,12 +913,6 @@ impl From> for BufferSubresourceRange { } } -impl From>> for BufferSubresourceRange { - fn from(range: Option>) -> Self { - range.unwrap_or(0..vk::WHOLE_SIZE).into() - } -} - impl From for Range { fn from(subresource: BufferSubresourceRange) -> Self { subresource.start..subresource.end @@ -882,7 +920,7 @@ impl From for Range { } #[cfg(test)] -mod tests { +mod test { use { super::*, rand::{Rng, SeedableRng, rngs::SmallRng}, @@ -1285,7 +1323,7 @@ mod tests { #[test] pub fn buffer_info() { let info = Info::device_mem(0, vk::BufferUsageFlags::empty()); - let builder = info.to_builder().build(); + let builder = info.into_builder().build(); assert_eq!(info, builder); } diff --git a/src/driver/cmd_buf.rs b/src/driver/cmd_buf.rs index 45d27f71..aae28d13 100644 --- a/src/driver/cmd_buf.rs +++ b/src/driver/cmd_buf.rs @@ -1,8 +1,11 @@ +//! Command buffer types + use { super::{DriverError, device::Device}, ash::vk, + derive_builder::{Builder, UninitializedFieldError}, log::{error, trace, warn}, - std::{fmt::Debug, ops::Deref, sync::Arc, thread::panicking}, + std::{fmt::Debug, slice, thread::panicking}, }; // TODO: Expose command functions so the fence, device, waiting flags do not @@ -10,75 +13,107 @@ use { /// Represents a Vulkan command buffer to which some work has been submitted. #[derive(Debug)] +#[read_only::cast] pub struct CommandBuffer { - cmd_buf: vk::CommandBuffer, - pub(crate) device: Arc, + /// The device which owns this command buffer resource. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub device: Device, + droppables: Vec>, - pub(crate) fence: vk::Fence, // Keeps state because everyone wants this + + /// The native Vulkan fence handle of this command buffer. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub fence: vk::Fence, + + /// The native Vulkan resource handle of this command buffer. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub handle: vk::CommandBuffer, /// Information used to create this object. + #[readonly] pub info: CommandBufferInfo, pub(crate) pool: vk::CommandPool, - pub(crate) waiting: bool, } impl CommandBuffer { + /// Creates a command buffer allocation backed by a transient resettable command pool. #[profiling::function] - pub(crate) fn create( - device: &Arc, - info: CommandBufferInfo, - ) -> Result { - let device = Arc::clone(device); - let cmd_pool_info = vk::CommandPoolCreateInfo::default() - .flags( - vk::CommandPoolCreateFlags::TRANSIENT - | vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER, - ) - .queue_family_index(info.queue_family_index); + pub fn create(device: &Device, info: CommandBufferInfo) -> Result { + let device = device.clone(); + let pool = unsafe { - device - .create_command_pool(&cmd_pool_info, None) - .map_err(|err| { - warn!("{err}"); - - DriverError::Unsupported - })? - }; - let cmd_buf_info = vk::CommandBufferAllocateInfo::default() - .command_buffer_count(1) - .command_pool(pool) - .level(vk::CommandBufferLevel::PRIMARY); - let cmd_buf = unsafe { - device - .allocate_command_buffers(&cmd_buf_info) - .map_err(|err| { - warn!("{err}"); - - DriverError::Unsupported - })? - }[0]; + device.create_command_pool( + &vk::CommandPoolCreateInfo::default() + .flags( + vk::CommandPoolCreateFlags::TRANSIENT + | vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER, + ) + .queue_family_index(info.queue_family_index), + None, + ) + } + .map_err(|err| { + warn!("unable to create command pool: {err}"); + + match err { + vk::Result::ERROR_OUT_OF_DEVICE_MEMORY | vk::Result::ERROR_OUT_OF_HOST_MEMORY => { + DriverError::OutOfMemory + } + _ => DriverError::Unsupported, + } + })?; + + let handle = unsafe { + device.allocate_command_buffers( + &vk::CommandBufferAllocateInfo::default() + .command_buffer_count(1) + .command_pool(pool) + .level(vk::CommandBufferLevel::PRIMARY), + ) + } + .map_err(|err| { + warn!("unable to allocate command buffer: {err}"); + + match err { + vk::Result::ERROR_OUT_OF_DEVICE_MEMORY | vk::Result::ERROR_OUT_OF_HOST_MEMORY => { + DriverError::OutOfMemory + } + _ => DriverError::Unsupported, + } + })?[0]; + let fence = Device::create_fence(&device, false)?; Ok(Self { - cmd_buf, device, droppables: vec![], fence, + handle, info, pool, - waiting: false, }) } + /// Drops an item after execution has been completed. + pub fn drop_after_executed(&mut self, x: impl Debug + Send + 'static) { + self.droppables.push(Box::new(x)); + } + /// Signals that execution has completed and it is time to drop anything we collected. #[profiling::function] - pub(crate) fn drop_fenced(this: &mut Self) { - if !this.droppables.is_empty() { - trace!("dropping {} shared references", this.droppables.len()); + fn drop_fenced(&mut self) { + if !self.droppables.is_empty() { + trace!("dropping {} shared references", self.droppables.len()); } - this.droppables.clear(); + self.droppables.clear(); } /// Returns `true` after the GPU has executed the previous submission to this command buffer. @@ -91,81 +126,109 @@ impl CommandBuffer { match res { Ok(status) => Ok(status), Err(err) if err == vk::Result::ERROR_DEVICE_LOST => { - error!("Device lost"); + error!("invalid device state: lost"); Err(DriverError::InvalidData) } Err(err) => { // VK_SUCCESS and VK_NOT_READY handled by get_fence_status in ash // VK_ERROR_DEVICE_LOST already handled above, so no idea what happened - error!("{}", err); + error!("unable to get fence status: {err}"); Err(DriverError::InvalidData) } } } - /// Drops an item after execution has been completed - pub(crate) fn push_fenced_drop(this: &mut Self, thing_to_drop: impl Debug + Send + 'static) { - this.droppables.push(Box::new(thing_to_drop)); - } - /// Stalls by blocking the current thread until the GPU has executed the previous submission to /// this command buffer. /// /// See [`Self::has_executed`] to check without blocking. #[profiling::function] pub fn wait_until_executed(&mut self) -> Result<(), DriverError> { - if !self.waiting { + if self.droppables.is_empty() { return Ok(()); } Device::wait_for_fence(&self.device, &self.fence)?; - self.waiting = false; - - Ok(()) - } -} -impl Deref for CommandBuffer { - type Target = vk::CommandBuffer; + self.drop_fenced(); - fn deref(&self) -> &Self::Target { - &self.cmd_buf + Ok(()) } } impl Drop for CommandBuffer { #[profiling::function] fn drop(&mut self) { - use std::slice::from_ref; - if panicking() { return; } - unsafe { - if self.waiting && Device::wait_for_fence(&self.device, &self.fence).is_err() { - return; - } - - Self::drop_fenced(self); + if self.wait_until_executed().is_err() { + return; + } + unsafe { self.device - .free_command_buffers(self.pool, from_ref(&self.cmd_buf)); + .free_command_buffers(self.pool, slice::from_ref(&self.handle)); self.device.destroy_command_pool(self.pool, None); self.device.destroy_fence(self.fence, None); } } } -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +/// Information used to create a [`CommandBuffer`] instance. +#[derive(Builder, Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +#[builder( + build_fn(private, name = "fallible_build", error = "UninitializedFieldError"), + derive(Clone, Copy, Debug), + pattern = "owned" +)] pub struct CommandBufferInfo { + /// Designates the queue family used by the command pool that allocates this command buffer. + /// + /// See [`VkCommandPoolCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkCommandPoolCreateInfo.html). pub queue_family_index: u32, } impl CommandBufferInfo { + /// Creates command buffer allocation info for the given queue family. pub fn new(queue_family_index: u32) -> Self { Self { queue_family_index } } } + +impl CommandBufferInfo { + /// Creates a default `CommandBufferInfoBuilder`. + pub fn builder() -> CommandBufferInfoBuilder { + Default::default() + } + + /// Converts a `CommandBufferInfo` into a `CommandBufferInfoBuilder`. + pub fn into_builder(self) -> CommandBufferInfoBuilder { + CommandBufferInfoBuilder { + queue_family_index: Some(self.queue_family_index), + } + } + + #[deprecated = "use into_builder function"] + #[doc(hidden)] + pub fn to_builder(self) -> CommandBufferInfoBuilder { + self.into_builder() + } +} + +impl From for CommandBufferInfo { + fn from(info: CommandBufferInfoBuilder) -> Self { + info.build() + } +} + +impl CommandBufferInfoBuilder { + /// Builds a new `CommandBufferInfo`. + #[inline(always)] + pub fn build(self) -> CommandBufferInfo { + self.fallible_build().expect("invalid command buffer info") + } +} diff --git a/src/driver/compute.rs b/src/driver/compute.rs index 7569e9c9..de7389e3 100644 --- a/src/driver/compute.rs +++ b/src/driver/compute.rs @@ -4,40 +4,26 @@ use { super::{ DriverError, device::Device, - shader::{DescriptorBindingMap, PipelineDescriptorInfo, Shader, align_spriv}, + shader::{DescriptorBindingMap, PipelineDescriptorInfo, Shader}, }, ash::vk, derive_builder::{Builder, UninitializedFieldError}, log::{trace, warn}, - std::{ffi::CString, ops::Deref, sync::Arc, thread::panicking}, + std::{ + ffi::CString, + hash::{Hash, Hasher}, + slice, + sync::{Arc, OnceLock}, + thread::panicking, + }, }; -/// Smart pointer handle to a [pipeline] object. +/// Smart pointer handle of a pipeline object. /// /// Also contains information about the object. -/// -/// ## `Deref` behavior -/// -/// `ComputePipeline` automatically dereferences to [`vk::Pipeline`] (via the [`Deref`] -/// trait), so you can call `vk::Pipeline`'s methods on a value of type `ComputePipeline`. -/// -/// [pipeline]: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPipeline.html -/// [deref]: core::ops::Deref -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct ComputePipeline { - pub(crate) descriptor_bindings: DescriptorBindingMap, - pub(crate) descriptor_info: PipelineDescriptorInfo, - device: Arc, - pub(crate) layout: vk::PipelineLayout, - - /// Information used to create this object. - pub info: ComputePipelineInfo, - - /// A descriptive name used in debugging messages. - pub name: Option, - - pipeline: vk::Pipeline, - pub(crate) push_constants: Option, + pub(crate) inner: Arc, } impl ComputePipeline { @@ -54,32 +40,29 @@ impl ComputePipeline { /// ```no_run /// # use std::sync::Arc; /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::compute::{ComputePipeline, ComputePipelineInfo}; - /// # use screen_13::driver::shader::{Shader}; + /// # use vk_graph::driver::DriverError; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::compute::{ComputePipeline, ComputePipelineInfo}; + /// # use vk_graph::driver::shader::{Shader}; /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); + /// # let device = Device::new(DeviceInfo::default())?; /// # let my_shader_code = [0u8; 1]; /// // my_shader_code is raw SPIR-V code as bytes /// let shader = Shader::new_compute(my_shader_code.as_slice()); /// let pipeline = ComputePipeline::create(&device, ComputePipelineInfo::default(), shader)?; /// - /// assert_ne!(*pipeline, vk::Pipeline::null()); + /// assert_ne!(pipeline.handle(), vk::Pipeline::null()); /// # Ok(()) } /// ``` #[profiling::function] pub fn create( - device: &Arc, + device: &Device, info: impl Into, shader: impl Into, ) -> Result { - use std::slice::from_ref; - trace!("create"); - let device = Arc::clone(device); - let info: ComputePipelineInfo = info.into(); + let info = info.into(); let shader = shader.into(); // Use SPIR-V reflection to get the types and counts of all descriptors @@ -90,34 +73,31 @@ impl ComputePipeline { } } - let descriptor_info = PipelineDescriptorInfo::create(&device, &descriptor_bindings)?; + let descriptor_info = PipelineDescriptorInfo::create(device, &descriptor_bindings)?; let descriptor_set_layouts = descriptor_info .layouts .values() - .map(|descriptor_set_layout| **descriptor_set_layout) - .collect::>(); + .map(|descriptor_set_layout| descriptor_set_layout.handle) + .collect::>(); unsafe { let shader_module = device .create_shader_module( - &vk::ShaderModuleCreateInfo::default().code(align_spriv(&shader.spirv)?), + &vk::ShaderModuleCreateInfo::default().code(shader.spirv.words()), None, ) .map_err(|err| { - warn!("{err}"); + warn!("unable to create compute shader module: {err}"); DriverError::Unsupported })?; - let entry_name = CString::new(shader.entry_name.as_bytes()).unwrap(); + let entry_name = + CString::new(shader.entry_name.as_bytes()).expect("invalid entry name"); let mut stage_create_info = vk::PipelineShaderStageCreateInfo::default() .module(shader_module) .stage(shader.stage) .name(&entry_name); - let specialization_info = shader.specialization_info.as_ref().map(|info| { - vk::SpecializationInfo::default() - .map_entries(&info.map_entries) - .data(&info.data) - }); + let specialization_info = shader.specialization.as_ref().map(Into::into); if let Some(specialization_info) = &specialization_info { stage_create_info = stage_create_info.specialization_info(specialization_info); @@ -128,27 +108,31 @@ impl ComputePipeline { let push_constants = shader.push_constant_range(); if let Some(push_constants) = &push_constants { - layout_info = layout_info.push_constant_ranges(from_ref(push_constants)); + layout_info = layout_info.push_constant_ranges(slice::from_ref(push_constants)); } let layout = device .create_pipeline_layout(&layout_info, None) .map_err(|err| { - warn!("{err}"); + warn!("unable to create compute pipeline layout: {err}"); + + device.destroy_shader_module(shader_module, None); DriverError::Unsupported })?; - let pipeline_info = vk::ComputePipelineCreateInfo::default() + let create_info = vk::ComputePipelineCreateInfo::default() .stage(stage_create_info) .layout(layout); - let pipeline = device + let handle = device .create_compute_pipelines( - Device::pipeline_cache(&device), - from_ref(&pipeline_info), + Device::pipeline_cache(device), + slice::from_ref(&create_info), None, ) .map_err(|(_, err)| { - warn!("{err}"); + warn!("unable to create compute pipeline: {err}"); + + device.destroy_shader_module(shader_module, None); DriverError::Unsupported })?[0]; @@ -156,59 +140,85 @@ impl ComputePipeline { device.destroy_shader_module(shader_module, None); Ok(ComputePipeline { - descriptor_bindings, - descriptor_info, - device, - info, - layout, - name: None, - pipeline, - push_constants, + inner: Arc::new(ComputePipelineInner { + descriptor_bindings, + descriptor_info, + device: device.clone(), + handle, + info, + layout, + name: Default::default(), + push_constants, + }), }) } } - /// Sets the debugging name assigned to this pipeline. - pub fn with_name(mut this: Self, name: impl Into) -> Self { - this.name = Some(name.into()); - this + /// Gets the debugging name assigned to this pipeline, if one has been set. + pub fn debug_name(&self) -> Option<&str> { + self.inner.name.get().map(String::as_str) + } + + /// The device which owns this compute pipeline. + pub fn device(&self) -> &Device { + &self.inner.device } -} -impl Deref for ComputePipeline { - type Target = vk::Pipeline; + /// The native Vulkan pipeline handle of this compute pipeline. + pub fn handle(&self) -> vk::Pipeline { + self.inner.handle + } - fn deref(&self) -> &Self::Target { - &self.pipeline + /// Gets the information used to create this object. + pub fn info(&self) -> ComputePipelineInfo { + self.inner.info } -} -impl Drop for ComputePipeline { - #[profiling::function] - fn drop(&mut self) { - if panicking() { + /// Sets the debugging name assigned to this pipeline. + /// + /// _Note:_ The pipeline name may only be assigned once. Subsequent calls will not update the + /// previously set name value. + pub fn set_debug_name(&mut self, name: impl Into) { + if !self.inner.device.physical_device.instance.info.debug { return; } - unsafe { - self.device.destroy_pipeline(self.pipeline, None); - self.device.destroy_pipeline_layout(self.layout, None); - } + // Both Ok and Err are valid conditions + let _ = self.inner.name.set(name.into()); + } + + /// Sets the debugging name assigned to this pipeline. + /// + /// _Note:_ The pipeline name may only be assigned once. Subsequent calls will not update the + /// previously set name value. + pub fn with_debug_name(mut self, name: impl Into) -> Self { + self.set_debug_name(name); + + self + } +} + +impl Eq for ComputePipeline {} + +impl Hash for ComputePipeline { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.inner).hash(state); + } +} + +impl PartialEq for ComputePipeline { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.inner, &other.inner) } } /// Information used to create a [`ComputePipeline`] instance. #[derive(Builder, Clone, Copy, Debug, Eq, Hash, PartialEq)] #[builder( - build_fn( - private, - name = "fallible_build", - error = "ComputePipelineInfoBuilderError" - ), + build_fn(private, name = "fallible_build", error = "UninitializedFieldError"), derive(Clone, Copy, Debug), pattern = "owned" )] -#[non_exhaustive] pub struct ComputePipelineInfo { /// The number of descriptors to allocate for a given binding when using bindless (unbounded) /// syntax. @@ -220,9 +230,10 @@ pub struct ComputePipelineInfo { /// Basic usage (GLSL): /// /// ``` - /// # inline_spirv::inline_spirv!(r#" + /// # vk_shader_macros::glsl!(r#" /// #version 460 core /// #extension GL_EXT_nonuniform_qualifier : require + /// #pragma shader_stage(compute) /// /// layout(set = 0, binding = 0, rgba8) writeonly uniform image2D my_binding[]; /// @@ -230,20 +241,30 @@ pub struct ComputePipelineInfo { /// { /// // my_binding will have space for 8,192 images by default /// } - /// # "#, comp); + /// # "#); /// ``` #[builder(default = "8192")] pub bindless_descriptor_count: u32, } impl ComputePipelineInfo { + /// Creates a default `ComputePipelineInfoBuilder`. + pub fn builder() -> ComputePipelineInfoBuilder { + Default::default() + } + /// Converts a `ComputePipelineInfo` into a `ComputePipelineInfoBuilder`. - #[inline(always)] - pub fn to_builder(self) -> ComputePipelineInfoBuilder { + pub fn into_builder(self) -> ComputePipelineInfoBuilder { ComputePipelineInfoBuilder { bindless_descriptor_count: Some(self.bindless_descriptor_count), } } + + #[deprecated = "use into_builder function"] + #[doc(hidden)] + pub fn to_builder(self) -> ComputePipelineInfoBuilder { + self.into_builder() + } } impl Default for ComputePipelineInfo { @@ -264,29 +285,51 @@ impl ComputePipelineInfoBuilder { /// Builds a new `ComputePipelineInfo`. #[inline(always)] pub fn build(self) -> ComputePipelineInfo { - let res = self.fallible_build(); + self.fallible_build() + .expect("invalid compute pipeline info") + } +} - #[cfg(test)] - let res = res.unwrap(); +#[derive(Debug)] +pub(crate) struct ComputePipelineInner { + pub descriptor_bindings: DescriptorBindingMap, + pub descriptor_info: PipelineDescriptorInfo, + pub device: Device, + pub handle: vk::Pipeline, + pub info: ComputePipelineInfo, + pub layout: vk::PipelineLayout, + pub name: OnceLock, + pub push_constants: Option, +} - #[cfg(not(test))] - let res = unsafe { res.unwrap_unchecked() }; +impl Drop for ComputePipelineInner { + #[profiling::function] + fn drop(&mut self) { + if panicking() { + return; + } - res + unsafe { + self.device.destroy_pipeline(self.handle, None); + self.device.destroy_pipeline_layout(self.layout, None); + } } } -#[derive(Debug)] -struct ComputePipelineInfoBuilderError; +mod deprecated { + use crate::driver::compute::ComputePipeline; -impl From for ComputePipelineInfoBuilderError { - fn from(_: UninitializedFieldError) -> Self { - Self + impl ComputePipeline { + #[deprecated = "use with_debug_name function"] + #[doc(hidden)] + pub fn with_name(this: Self, name: impl Into) -> Self { + this.with_debug_name(name) + } } } #[cfg(test)] -mod tests { +mod test { use super::*; type Info = ComputePipelineInfo; @@ -295,7 +338,7 @@ mod tests { #[test] pub fn compute_pipeline_info() { let info = Info::default(); - let builder = info.to_builder().build(); + let builder = info.into_builder().build(); assert_eq!(info, builder); } diff --git a/src/driver/descriptor_set.rs b/src/driver/descriptor_set.rs index 06b3c08a..99da3d0e 100644 --- a/src/driver/descriptor_set.rs +++ b/src/driver/descriptor_set.rs @@ -1,24 +1,42 @@ +//! Descriptor pool allocation helpers used by pipeline execution. + use { super::{DescriptorSetLayout, DriverError, device::Device}, ash::vk, log::warn, - std::{ops::Deref, sync::Arc, thread::panicking}, + std::{ops::Deref, slice, thread::panicking}, }; +/// Descriptor pool resource used to allocate descriptor sets for pipeline execution. #[derive(Debug)] +#[read_only::cast] pub struct DescriptorPool { + /// The device which owns this descriptor pool resource. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub device: Device, + + /// The native Vulkan resource handle of this descriptor pool. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub handle: vk::DescriptorPool, + + /// Information used to create this descriptor pool resource. + /// + /// _Note:_ This field is read-only. + #[readonly] pub info: DescriptorPoolInfo, - descriptor_pool: vk::DescriptorPool, - pub device: Arc, } impl DescriptorPool { #[profiling::function] - pub fn create( - device: &Arc, + pub(crate) fn create( + device: &Device, info: impl Into, ) -> Result { - let device = Arc::clone(device); + let device = device.clone(); let info = info.into(); let mut pool_sizes = [vk::DescriptorPoolSize { @@ -123,7 +141,7 @@ impl DescriptorPool { pool_size_count += 1; } - let descriptor_pool = unsafe { + let handle = unsafe { device.create_descriptor_pool( &vk::DescriptorPoolCreateInfo::default() .flags(vk::DescriptorPoolCreateFlags::FREE_DESCRIPTOR_SET) @@ -133,47 +151,45 @@ impl DescriptorPool { ) } .map_err(|err| { - warn!("{err}"); + warn!("unable to create descriptor pool: {err}"); DriverError::Unsupported })?; Ok(Self { - descriptor_pool, device, + handle, info, }) } - pub fn allocate_descriptor_set( + pub(crate) fn allocate_descriptor_set( this: &Self, layout: &DescriptorSetLayout, ) -> Result { Ok(Self::allocate_descriptor_sets(this, layout, 1)? .next() - .unwrap()) + .expect("missing descriptor set")) } #[profiling::function] - pub fn allocate_descriptor_sets<'a>( - this: &'a Self, + pub(crate) fn allocate_descriptor_sets<'a>( + &'a self, layout: &DescriptorSetLayout, count: u32, ) -> Result + 'a, DriverError> { - use std::slice::from_ref; - let mut create_info = vk::DescriptorSetAllocateInfo::default() - .descriptor_pool(this.descriptor_pool) - .set_layouts(from_ref(layout)); + .descriptor_pool(self.handle) + .set_layouts(slice::from_ref(&layout.handle)); create_info.descriptor_set_count = count; Ok(unsafe { - this.device + self.device .allocate_descriptor_sets(&create_info) .map_err(|err| { use {DriverError::*, vk::Result as vk}; - warn!("{err}"); + warn!("unable to allocate descriptor sets: {err}"); match err { e if e == vk::ERROR_FRAGMENTED_POOL => InvalidData, @@ -185,22 +201,14 @@ impl DescriptorPool { })? .into_iter() .map(move |descriptor_set| DescriptorSet { - descriptor_pool: this.descriptor_pool, + descriptor_pool: self.handle, descriptor_set, - device: Arc::clone(&this.device), + device: self.device.clone(), }) }) } } -impl Deref for DescriptorPool { - type Target = vk::DescriptorPool; - - fn deref(&self) -> &Self::Target { - &self.descriptor_pool - } -} - impl Drop for DescriptorPool { #[profiling::function] fn drop(&mut self) { @@ -209,31 +217,31 @@ impl Drop for DescriptorPool { } unsafe { - self.device - .destroy_descriptor_pool(self.descriptor_pool, None); + self.device.destroy_descriptor_pool(self.handle, None); } } } +/// Descriptor counts and limits used to create a [`DescriptorPool`]. #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] pub struct DescriptorPoolInfo { - pub acceleration_structure_count: u32, - pub combined_image_sampler_count: u32, - pub input_attachment_count: u32, - pub max_sets: u32, - pub sampled_image_count: u32, - pub sampler_count: u32, - pub storage_buffer_count: u32, - pub storage_buffer_dynamic_count: u32, - pub storage_image_count: u32, - pub storage_texel_buffer_count: u32, - pub uniform_buffer_count: u32, - pub uniform_buffer_dynamic_count: u32, - pub uniform_texel_buffer_count: u32, + pub(crate) acceleration_structure_count: u32, + pub(crate) combined_image_sampler_count: u32, + pub(crate) input_attachment_count: u32, + pub(crate) max_sets: u32, + pub(crate) sampled_image_count: u32, + pub(crate) sampler_count: u32, + pub(crate) storage_buffer_count: u32, + pub(crate) storage_buffer_dynamic_count: u32, + pub(crate) storage_image_count: u32, + pub(crate) storage_texel_buffer_count: u32, + pub(crate) uniform_buffer_count: u32, + pub(crate) uniform_buffer_dynamic_count: u32, + pub(crate) uniform_texel_buffer_count: u32, } impl DescriptorPoolInfo { - pub fn is_empty(&self) -> bool { + pub(crate) fn is_empty(&self) -> bool { self.acceleration_structure_count + self.combined_image_sampler_count + self.input_attachment_count @@ -251,10 +259,10 @@ impl DescriptorPoolInfo { } #[derive(Debug)] -pub struct DescriptorSet { +pub(crate) struct DescriptorSet { descriptor_pool: vk::DescriptorPool, descriptor_set: vk::DescriptorSet, - device: Arc, + device: Device, } impl Deref for DescriptorSet { @@ -268,16 +276,15 @@ impl Deref for DescriptorSet { impl Drop for DescriptorSet { #[profiling::function] fn drop(&mut self) { - use std::slice::from_ref; - if panicking() { return; } - unsafe { + if let Err(err) = unsafe { self.device - .free_descriptor_sets(self.descriptor_pool, from_ref(&self.descriptor_set)) - .unwrap_or_else(|_| warn!("Unable to free descriptor set")) + .free_descriptor_sets(self.descriptor_pool, slice::from_ref(&self.descriptor_set)) + } { + warn!("unable to free descriptor set: {err}"); } } } diff --git a/src/driver/descriptor_set_layout.rs b/src/driver/descriptor_set_layout.rs index 31051ea0..5a7e16b8 100644 --- a/src/driver/descriptor_set_layout.rs +++ b/src/driver/descriptor_set_layout.rs @@ -2,44 +2,34 @@ use { super::{DriverError, device::Device}, ash::vk, log::warn, - std::{ops::Deref, sync::Arc, thread::panicking}, + std::thread::panicking, }; #[derive(Debug)] +#[read_only::cast] pub struct DescriptorSetLayout { - device: Arc, - descriptor_set_layout: vk::DescriptorSetLayout, + pub device: Device, + pub handle: vk::DescriptorSetLayout, } impl DescriptorSetLayout { #[profiling::function] pub fn create( - device: &Arc, + device: &Device, info: &vk::DescriptorSetLayoutCreateInfo, ) -> Result { - let device = Arc::clone(device); - let descriptor_set_layout = unsafe { + let device = device.clone(); + let handle = unsafe { device .create_descriptor_set_layout(info, None) .map_err(|err| { - warn!("{err}"); + warn!("unable to create descriptor set layout: {err}"); DriverError::Unsupported }) }?; - Ok(Self { - device, - descriptor_set_layout, - }) - } -} - -impl Deref for DescriptorSetLayout { - type Target = vk::DescriptorSetLayout; - - fn deref(&self) -> &Self::Target { - &self.descriptor_set_layout + Ok(Self { device, handle }) } } @@ -51,8 +41,7 @@ impl Drop for DescriptorSetLayout { } unsafe { - self.device - .destroy_descriptor_set_layout(self.descriptor_set_layout, None); + self.device.destroy_descriptor_set_layout(self.handle, None); } } } diff --git a/src/driver/device.rs b/src/driver/device.rs index e6442e78..9b3ff956 100644 --- a/src/driver/device.rs +++ b/src/driver/device.rs @@ -1,9 +1,12 @@ -//! Logical device resource types +//! Logical device types use { - super::{DriverError, Instance, physical_device::PhysicalDevice}, - ash::{ext, khr, vk}, - ash_window::enumerate_required_extensions, + super::{ + DriverError, + instance::{Instance, InstanceInfoBuilder}, + physical_device::{PhysicalDevice, RayTraceProperties}, + }, + ash::{khr, vk}, derive_builder::{Builder, UninitializedFieldError}, gpu_allocator::{ AllocatorDebugSettings, @@ -12,12 +15,11 @@ use { log::{error, info, trace, warn}, raw_window_handle::HasDisplayHandle, std::{ - cmp::Ordering, - ffi::CStr, fmt::{Debug, Formatter}, - iter::{empty, repeat_n}, mem::{ManuallyDrop, forget}, ops::Deref, + slice, + sync::Arc, thread::panicking, time::Instant, }, @@ -29,230 +31,87 @@ use parking_lot::Mutex; #[cfg(not(feature = "parking_lot"))] use std::sync::Mutex; -/// Function type for selection of physical devices. -pub type SelectPhysicalDeviceFn = dyn FnOnce(&[PhysicalDevice]) -> usize; - -/// Opaque handle to a device object. -pub struct Device { - accel_struct_ext: Option, +fn select_physical_device( + instance: &Instance, + mut index: usize, +) -> Result { + let mut physical_devices = Instance::physical_devices(instance)? + .into_iter() + .collect::>(); + if physical_devices.is_empty() { + warn!("unable to find physical devices"); + + return Err(DriverError::Unsupported); + } - pub(super) allocator: ManuallyDrop>, + if index >= physical_devices.len() { + index = 0; + } - device: ash::Device, + let physical_device = physical_devices.remove(index); - /// Vulkan instance pointer, which includes useful functions. - instance: Instance, + Ok(physical_device) +} - pipeline_cache: vk::PipelineCache, +/// Opaque handle to a device object. +#[read_only::embed] +#[derive(Clone)] +pub struct Device { + #[readonly] + pub(self) inner: Arc, /// The physical device, which contains useful data about features, properties, and limits. - pub physical_device: PhysicalDevice, - - /// The physical execution queues which all work will be submitted to. - pub(crate) queues: Vec>, - - pub(crate) ray_trace_ext: Option, - - pub(super) surface_ext: Option, - pub(super) swapchain_ext: Option, + /// + /// _Note:_ This field is read-only. + #[readonly] + pub physical_device: Box, } impl Device { - /// Prepares device creation information and calls the provided callback to allow an application - /// to control the device creation process. - /// - /// # Safety - /// - /// This is only required for interoperting with other libraries and comes with all the caveats - /// of using `ash` builder types, which are inherently dangerous. Use with extreme caution. - #[profiling::function] - pub unsafe fn create_ash_device( - instance: &Instance, - physical_device: &PhysicalDevice, - display_window: bool, - create_fn: F, - ) -> ash::prelude::VkResult - where - F: FnOnce(vk::DeviceCreateInfo) -> ash::prelude::VkResult, - { - let mut enabled_ext_names = Vec::with_capacity(7); - - if display_window { - enabled_ext_names.push(khr::swapchain::NAME.as_ptr()); - } - - if physical_device.accel_struct_properties.is_some() { - enabled_ext_names.push(khr::acceleration_structure::NAME.as_ptr()); - enabled_ext_names.push(khr::deferred_host_operations::NAME.as_ptr()); - } - - if physical_device.ray_query_features.ray_query { - enabled_ext_names.push(khr::ray_query::NAME.as_ptr()); - } - - if physical_device.ray_trace_features.ray_tracing_pipeline { - enabled_ext_names.push(khr::ray_tracing_pipeline::NAME.as_ptr()); - } - - if physical_device.index_type_uint8_features.index_type_uint8 { - enabled_ext_names.push(ext::index_type_uint8::NAME.as_ptr()); - } - - // Molten-vk doesn't support the full Vulkan feature set, hence the portability subset extension must be enabled. - #[cfg(all(target_os = "macos", feature = "loaded"))] - { - enabled_ext_names.push(khr::portability_subset::NAME.as_ptr()); - } - - let priorities = repeat_n( - 1.0, - physical_device - .queue_families - .iter() - .map(|family| family.queue_count) - .max() - .unwrap_or_default() as _, - ) - .collect::>(); - - let queue_infos = physical_device - .queue_families - .iter() - .enumerate() - .map(|(idx, family)| { - let mut queue_info = vk::DeviceQueueCreateInfo::default() - .queue_family_index(idx as _) - .queue_priorities(&priorities[0..family.queue_count as usize]); - queue_info.queue_count = family.queue_count; - - queue_info - }) - .collect::>(); - - let ash::InstanceFnV1_1 { - get_physical_device_features2, - .. - } = instance.fp_v1_1(); - let mut features_v1_1 = vk::PhysicalDeviceVulkan11Features::default(); - let mut features_v1_2 = vk::PhysicalDeviceVulkan12Features::default(); - let mut acceleration_structure_features = - vk::PhysicalDeviceAccelerationStructureFeaturesKHR::default(); - let mut index_type_uint8_features = vk::PhysicalDeviceIndexTypeUint8FeaturesEXT::default(); - let mut ray_query_features = vk::PhysicalDeviceRayQueryFeaturesKHR::default(); - let mut ray_trace_features = vk::PhysicalDeviceRayTracingPipelineFeaturesKHR::default(); - let mut features = vk::PhysicalDeviceFeatures2::default() - .push_next(&mut features_v1_1) - .push_next(&mut features_v1_2); - - if physical_device.accel_struct_properties.is_some() { - features = features.push_next(&mut acceleration_structure_features); - } - - if physical_device.ray_query_features.ray_query { - features = features.push_next(&mut ray_query_features); - } - - if physical_device.ray_trace_features.ray_tracing_pipeline { - features = features.push_next(&mut ray_trace_features); - } - - if physical_device.index_type_uint8_features.index_type_uint8 { - features = features.push_next(&mut index_type_uint8_features); - } - - unsafe { get_physical_device_features2(**physical_device, &mut features) }; - - let device_create_info = vk::DeviceCreateInfo::default() - .queue_create_infos(&queue_infos) - .enabled_extension_names(&enabled_ext_names) - .push_next(&mut features); - - create_fn(device_create_info) + #[deprecated = "use create"] + #[doc(hidden)] + pub fn new(info: impl Into) -> Result { + Self::create(info) } - #[profiling::function] - fn create( - instance: Instance, - select_physical_device: Box, - display_window: bool, - ) -> Result { - let mut physical_devices = Instance::physical_devices(&instance)?; - - if physical_devices.is_empty() { - error!("no supported devices found"); - - return Err(DriverError::Unsupported); - } - - let mut phyical_device_idx = select_physical_device(&physical_devices); - - if phyical_device_idx >= physical_devices.len() { - warn!("invalid device selected"); - - phyical_device_idx = 0; - } - - let physical_device = physical_devices.remove(phyical_device_idx); + /// Begins recording a command buffer on this device. + /// + /// This is a thin wrapper around [`ash::Device::begin_command_buffer`] that maps Vulkan errors + /// to [`DriverError`] variants. + pub fn begin_command_buffer( + this: &Self, + cmd: vk::CommandBuffer, + begin_info: &vk::CommandBufferBeginInfo, + ) -> Result<(), DriverError> { + unsafe { + this.begin_command_buffer(cmd, begin_info).map_err(|err| { + warn!("unable to begin command buffer: {err}"); - let device = unsafe { - Self::create_ash_device( - &instance, - &physical_device, - display_window, - |device_create_info| { - instance.create_device(*physical_device, &device_create_info, None) - }, - ) + match err { + vk::Result::ERROR_OUT_OF_DEVICE_MEMORY + | vk::Result::ERROR_OUT_OF_HOST_MEMORY => DriverError::OutOfMemory, + _ => DriverError::Unsupported, + } + }) } - .map_err(|err| { - error!("unable to create device: {err}"); - - DriverError::Unsupported - })?; - - info!("created {}", physical_device.properties_v1_0.device_name); - - Self::load(instance, physical_device, device, display_window) - } - - /// Constructs a new device using the given configuration. - #[profiling::function] - pub fn create_headless(info: impl Into) -> Result { - let DeviceInfo { - debug, - select_physical_device, - } = info.into(); - let instance = Instance::create(debug, empty())?; - - Self::create(instance, select_physical_device, false) } /// Constructs a new device using the given configuration. + /// + /// This constructor is intended for headless or manually managed setups. It does not infer or + /// enable display platform surface extensions. Use [`Self::try_from_display`] when the + /// resulting device must be capable of later surface creation. #[profiling::function] - pub fn create_display( - info: impl Into, - display_handle: &impl HasDisplayHandle, - ) -> Result { + pub fn create(info: impl Into) -> Result { let DeviceInfo { debug, - select_physical_device, + physical_device_index, } = info.into(); - let display_handle = display_handle.display_handle().map_err(|err| { - warn!("{err}"); - - DriverError::Unsupported - })?; - let required_extensions = enumerate_required_extensions(display_handle.as_raw()) - .map_err(|err| { - warn!("{err}"); + let instance_info = InstanceInfoBuilder::default().debug(debug); + let instance = Instance::create(instance_info)?; + let physical_device = select_physical_device(&instance, physical_device_index)?; - DriverError::Unsupported - })? - .iter() - .map(|ext| unsafe { CStr::from_ptr(*ext as *const _) }); - let instance = Instance::create(debug, required_extensions)?; - - Self::create(instance, select_physical_device, true) + Self::try_from_physical_device(physical_device) } pub(crate) fn create_fence(this: &Self, signaled: bool) -> Result { @@ -266,21 +125,29 @@ impl Device { let allocation_callbacks = None; unsafe { this.create_fence(&create_info, allocation_callbacks) }.map_err(|err| { - warn!("{err}"); + warn!("unable to create fence: {err}"); DriverError::OutOfMemory }) } - pub(crate) fn create_semaphore(this: &Self) -> Result { - let create_info = vk::SemaphoreCreateInfo::default(); - let allocation_callbacks = None; - - unsafe { this.create_semaphore(&create_info, allocation_callbacks) }.map_err(|err| { - warn!("{err}"); - - DriverError::OutOfMemory - }) + /// Ends recording a command buffer on this device. + /// + /// This is a thin wrapper around [`ash::Device::end_command_buffer`] that maps Vulkan errors + /// to [`DriverError`] variants. + pub fn end_command_buffer(this: &Self, cmd: vk::CommandBuffer) -> Result<(), DriverError> { + unsafe { + this.end_command_buffer(cmd).map_err(|err| { + warn!("unable to end command buffer: {err}"); + + match err { + vk::Result::ERROR_INVALID_VIDEO_STD_PARAMETERS_KHR => DriverError::InvalidData, + vk::Result::ERROR_OUT_OF_DEVICE_MEMORY + | vk::Result::ERROR_OUT_OF_HOST_MEMORY => DriverError::OutOfMemory, + _ => DriverError::Unsupported, + } + }) + } } /// Helper for times when you already know that the device supports the acceleration @@ -290,9 +157,36 @@ impl Device { /// /// Panics if [Self.physical_device.accel_struct_properties] is `None`. pub(crate) fn expect_accel_struct_ext(this: &Self) -> &khr::acceleration_structure::Device { - this.accel_struct_ext + this.inner + .accel_struct_ext + .as_ref() + .expect("missing VK_KHR_acceleration_structure") + } + + /// Helper for times when you already know that the device supports the ray tracing pipeline + /// extension. + /// + /// # Panics + /// + /// Panics if [Self.physical_device.ray_trace_properties] is `None`. + pub(crate) fn expect_ray_trace_ext(this: &Self) -> &khr::ray_tracing_pipeline::Device { + this.inner + .ray_trace_ext + .as_ref() + .expect("missing VK_KHR_ray_tracing_pipeline") + } + + /// Helper for times when you already know that the device supports the ray tracing pipeline + /// extension. + /// + /// # Panics + /// + /// Panics if [Self.physical_device.ray_trace_properties] is `None`. + pub(crate) fn expect_ray_trace_properties(this: &Self) -> &RayTraceProperties { + this.physical_device + .ray_trace_properties .as_ref() - .expect("VK_KHR_acceleration_structure") + .expect("missing VK_KHR_ray_tracing_pipeline") } /// Helper for times when you already know that the instance supports the surface extension. @@ -301,7 +195,10 @@ impl Device { /// /// Panics if the device was not created for display window access. pub(crate) fn expect_surface_ext(this: &Self) -> &khr::surface::Instance { - this.surface_ext.as_ref().expect("VK_KHR_surface") + this.inner + .surface_ext + .as_ref() + .expect("missing VK_KHR_surface") } /// Helper for times when you already know that the device supports the swapchain extension. @@ -310,33 +207,73 @@ impl Device { /// /// Panics if the device was not created for display window access. pub(crate) fn expect_swapchain_ext(this: &Self) -> &khr::swapchain::Device { - this.swapchain_ext.as_ref().expect("VK_KHR_swapchain") + this.inner + .swapchain_ext + .as_ref() + .expect("missing VK_KHR_swapchain") + } + + pub(crate) fn pipeline_cache(this: &Self) -> vk::PipelineCache { + this.inner.pipeline_cache + } + + /// Submits command buffers to a queue, optionally signaling a fence. + pub fn queue_submit( + this: &Self, + queue: vk::Queue, + submits: &[vk::SubmitInfo], + fence: vk::Fence, + ) -> Result<(), DriverError> { + unsafe { + this.queue_submit(queue, submits, fence).map_err(|err| { + warn!("unable to queue submits: {err}"); + + match err { + vk::Result::ERROR_DEVICE_LOST => DriverError::InvalidData, + vk::Result::ERROR_OUT_OF_DEVICE_MEMORY + | vk::Result::ERROR_OUT_OF_HOST_MEMORY => DriverError::OutOfMemory, + _ => DriverError::Unsupported, + } + }) + } + } + + /// Resets one or more fences to the unsignaled state. + pub fn reset_fences(this: &Self, fences: &[vk::Fence]) -> Result<(), DriverError> { + unsafe { + this.reset_fences(fences).map_err(|err| { + warn!("unable to reset fences: {err}"); + + match err { + vk::Result::ERROR_OUT_OF_DEVICE_MEMORY => DriverError::OutOfMemory, + _ => DriverError::Unsupported, + } + }) + } } /// Loads and existing `ash` Vulkan device that may have been created by other means. #[profiling::function] - pub fn load( - instance: Instance, - physical_device: PhysicalDevice, + pub fn try_from_ash_device( device: ash::Device, - display_window: bool, + physical_device: PhysicalDevice, ) -> Result { - let debug = Instance::is_debug(&instance); + let debug = physical_device.instance.info.debug; let mut debug_settings = AllocatorDebugSettings::default(); debug_settings.log_leaks_on_shutdown = debug; debug_settings.log_memory_information = debug; debug_settings.log_allocations = debug; let allocator = Allocator::new(&AllocatorCreateDesc { - instance: (*instance).clone(), + instance: (*physical_device.instance).clone(), device: device.clone(), - physical_device: *physical_device, + physical_device: physical_device.handle, debug_settings, buffer_device_address: true, allocation_sizes: Default::default(), }) .map_err(|err| { - warn!("{err}"); + warn!("unable to create allocator: {err}"); DriverError::Unsupported })?; @@ -347,144 +284,188 @@ impl Device { let mut queue_family = Vec::with_capacity(properties.queue_count as _); for queue_index in 0..properties.queue_count { - queue_family - .push(unsafe { device.get_device_queue(queue_family_index as _, queue_index) }); + queue_family.push(Mutex::new(unsafe { + device.get_device_queue(queue_family_index as _, queue_index) + })); } - queues.push(queue_family); + queues.push(queue_family.into_boxed_slice()); } - let surface_ext = display_window - .then(|| khr::surface::Instance::new(Instance::entry(&instance), &instance)); - let swapchain_ext = display_window.then(|| khr::swapchain::Device::new(&instance, &device)); + let surface_ext = physical_device.swapchain_ext.then(|| { + let entry = Instance::entry(&physical_device.instance); + khr::surface::Instance::new(entry, &physical_device.instance) + }); + let swapchain_ext = physical_device + .swapchain_ext + .then(|| khr::swapchain::Device::new(&physical_device.instance, &device)); let accel_struct_ext = physical_device .accel_struct_properties .is_some() - .then(|| khr::acceleration_structure::Device::new(&instance, &device)); + .then(|| khr::acceleration_structure::Device::new(&physical_device.instance, &device)); let ray_trace_ext = physical_device .ray_trace_features .ray_tracing_pipeline - .then(|| khr::ray_tracing_pipeline::Device::new(&instance, &device)); + .then(|| khr::ray_tracing_pipeline::Device::new(&physical_device.instance, &device)); let pipeline_cache = unsafe { device.create_pipeline_cache(&vk::PipelineCacheCreateInfo::default(), None) } .map_err(|err| { - warn!("{err}"); + warn!("unable to create pipeline cache: {err}"); DriverError::Unsupported })?; Ok(Self { - accel_struct_ext, - allocator: ManuallyDrop::new(Mutex::new(allocator)), - device, - instance, - pipeline_cache, - physical_device, - queues, - ray_trace_ext, - surface_ext, - swapchain_ext, + read_only: ReadOnlyDevice { + inner: Arc::new(DeviceInner { + accel_struct_ext, + allocator: ManuallyDrop::new(Mutex::new(allocator)), + device, + pipeline_cache, + queues: queues.into_boxed_slice(), + ray_trace_ext, + surface_ext, + swapchain_ext, + }), + physical_device: Box::new(physical_device), + }, }) } - /// Lists the physical device's format capabilities. + /// Constructs a new device using the given configuration. #[profiling::function] - pub fn format_properties(this: &Self, format: vk::Format) -> vk::FormatProperties { - unsafe { - this.instance - .get_physical_device_format_properties(*this.physical_device, format) - } + pub fn try_from_display( + display: impl HasDisplayHandle, + info: impl Into, + ) -> Result { + let DeviceInfo { + debug, + physical_device_index, + } = info.into(); + let instance_info = InstanceInfoBuilder::default().debug(debug); + let instance = Instance::try_from_display(display, instance_info)?; + let physical_device = select_physical_device(&instance, physical_device_index)?; + + Self::try_from_physical_device(physical_device) } - /// Lists the physical device's image format capabilities. - /// - /// A result of `None` indicates the format is not supported. + /// Constructs a new device using the given physical device. #[profiling::function] - pub fn image_format_properties( - this: &Self, - format: vk::Format, - ty: vk::ImageType, - tiling: vk::ImageTiling, - usage: vk::ImageUsageFlags, - flags: vk::ImageCreateFlags, - ) -> Result, DriverError> { - unsafe { - match this.instance.get_physical_device_image_format_properties( - *this.physical_device, - format, - ty, - tiling, - usage, - flags, - ) { - Ok(properties) => Ok(Some(properties)), - Err(err) if err == vk::Result::ERROR_FORMAT_NOT_SUPPORTED => { - // We don't log this condition because it is normal for unsupported - // formats to be checked - we use the result to inform callers they - // cannot use those formats. - - Ok(None) - } - _ => Err(DriverError::OutOfMemory), - } + pub fn try_from_physical_device(physical_device: PhysicalDevice) -> Result { + let device = unsafe { + physical_device.create_ash_device(|device_create_info| { + physical_device.instance.create_device( + physical_device.handle, + &device_create_info, + None, + ) + }) } - } + .map_err(|err| { + error!("unable to create device: {err}"); - /// Provides a reference to the Vulkan instance used by this device. - pub fn instance(this: &Self) -> &Instance { - &this.instance - } + DriverError::Unsupported + })?; - pub(crate) fn pipeline_cache(this: &Self) -> vk::PipelineCache { - this.pipeline_cache + info!("created {}", physical_device.properties_v1_0.device_name); + + Self::try_from_ash_device(device, physical_device) } #[profiling::function] pub(crate) fn wait_for_fence(this: &Self, fence: &vk::Fence) -> Result<(), DriverError> { - use std::slice::from_ref; - - Device::wait_for_fences(this, from_ref(fence)) + Device::wait_for_fences(this, slice::from_ref(fence)) } #[profiling::function] pub(crate) fn wait_for_fences(this: &Self, fences: &[vk::Fence]) -> Result<(), DriverError> { unsafe { - match this.device.wait_for_fences(fences, true, 100) { + match this.wait_for_fences(fences, true, 100) { Ok(_) => return Ok(()), Err(err) if err == vk::Result::ERROR_DEVICE_LOST => { - error!("Device lost"); + error!("invalid device state: lost"); return Err(DriverError::InvalidData); } Err(err) if err == vk::Result::TIMEOUT => { trace!("waiting..."); } - _ => return Err(DriverError::OutOfMemory), + Err(err) => { + warn!("unable to wait for fences during polling phase: {err}"); + + return Err(DriverError::OutOfMemory); + } } let started = Instant::now(); - match this.device.wait_for_fences(fences, true, u64::MAX) { + match this.wait_for_fences(fences, true, u64::MAX) { Ok(_) => (), Err(err) if err == vk::Result::ERROR_DEVICE_LOST => { - error!("Device lost"); + error!("invalid device state: lost"); return Err(DriverError::InvalidData); } - _ => return Err(DriverError::OutOfMemory), + Err(err) => { + warn!("unable to wait for fences to completion: {err}"); + + return Err(DriverError::OutOfMemory); + } } let elapsed = Instant::now() - started; let elapsed_millis = elapsed.as_millis(); if elapsed_millis > 0 { - warn!("waited for {} ms", elapsed_millis); + warn!("slow fence wait: {} ms", elapsed_millis); } } Ok(()) } + + pub(crate) fn with_allocator(this: &Self, f: impl FnOnce(&mut Allocator) -> R) -> R { + let allocator = this.inner.allocator.lock(); + + #[cfg(not(feature = "parking_lot"))] + let allocator = allocator.expect("poisoned allocator lock"); + + let mut allocator = allocator; + + f(&mut allocator) + } + + /// Provides locked access to a device queue. + /// + /// Acquires the mutex for the queue at the given family and index, calls `f` with the + /// [`vk::Queue`], and releases the mutex after `f` returns. + /// + /// # Panics + /// + /// Panics if `queue_family_index` or `queue_index` is out of range for this device. + pub fn with_queue( + this: &Self, + queue_family_index: u32, + queue_index: u32, + f: impl FnOnce(vk::Queue) -> R, + ) -> R { + let queue_family = this + .inner + .queues + .get(queue_family_index as usize) + .expect("invalid queue family index"); + let queue = queue_family + .get(queue_index as usize) + .expect("invalid queue index"); + #[cfg(not(feature = "parking_lot"))] + let guard = queue.lock().expect("poisoned queue lock"); + + #[cfg(feature = "parking_lot")] + let guard = queue.lock(); + + f(*guard) + } } impl Debug for Device { @@ -493,52 +474,30 @@ impl Debug for Device { } } +#[cfg(doc)] impl Deref for Device { type Target = ash::Device; fn deref(&self) -> &Self::Target { - &self.device + unreachable!() } } -impl Drop for Device { - #[profiling::function] - fn drop(&mut self) { - if panicking() { - // When panicking we don't want the GPU allocator to complain about leaks - unsafe { - forget(ManuallyDrop::take(&mut self.allocator)); - } - - return; - } - - // trace!("drop"); - - if let Err(err) = unsafe { self.device.device_wait_idle() } { - warn!("device_wait_idle() failed: {err}"); - } - - unsafe { - self.device - .destroy_pipeline_cache(self.pipeline_cache, None); +impl Eq for Device {} - ManuallyDrop::drop(&mut self.allocator); - } - - unsafe { - self.device.destroy_device(None); - } +impl PartialEq for Device { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.inner, &other.inner) } } /// Information used to create a [`Device`] instance. -#[derive(Builder)] +#[derive(Builder, Clone, Copy, Debug, Default, Eq, PartialEq, Hash)] #[builder( - build_fn(private, name = "fallible_build", error = "DeviceInfoBuilderError"), + build_fn(private, name = "fallible_build", error = "UninitializedFieldError"), + derive(Clone, Copy, Debug), pattern = "owned" )] -#[non_exhaustive] pub struct DeviceInfo { /// Enables Vulkan validation layers. /// @@ -552,168 +511,191 @@ pub struct DeviceInfo { /// /// ## Platform-specific /// - /// **macOS:** Has no effect. + /// **macOS:** Has no effect unless the `loaded` feature is enabled. #[builder(default)] pub debug: bool, - /// Callback function used to select a [`PhysicalDevice`] from the available devices. The - /// callback must return the index of the selected device. - #[builder(default = "Box::new(DeviceInfo::discrete_gpu)")] - pub select_physical_device: Box, + /// Index of the [`PhysicalDevice`] from the available devices. See + /// [`Instance::physical_devices`]. + #[builder(default)] + pub physical_device_index: usize, } impl DeviceInfo { - /// Specifies default device information. - #[allow(clippy::new_ret_no_self)] - #[deprecated = "Use DeviceInfo::default()"] - #[doc(hidden)] - pub fn new() -> DeviceInfoBuilder { + /// Creates a default `DeviceInfoBuilder`. + pub fn builder() -> DeviceInfoBuilder { Default::default() } - /// A builtin [`DeviceInfo::select_physical_device`] function which prioritizes selection of - /// lower-power integrated GPU devices. - #[profiling::function] - pub fn integrated_gpu(physical_devices: &[PhysicalDevice]) -> usize { - assert!(!physical_devices.is_empty()); - - let mut physical_devices = physical_devices.iter().enumerate().collect::>(); - - if physical_devices.len() == 1 { - return 0; - } - - fn device_type(ty: vk::PhysicalDeviceType) -> usize { - match ty { - vk::PhysicalDeviceType::INTEGRATED_GPU => 0, - vk::PhysicalDeviceType::VIRTUAL_GPU => 1, - vk::PhysicalDeviceType::CPU => 2, - vk::PhysicalDeviceType::DISCRETE_GPU => 3, - _ => 4, - } + /// Converts a `DeviceInfo` into a `DeviceInfoBuilder`. + pub fn into_builder(self) -> DeviceInfoBuilder { + DeviceInfoBuilder { + debug: Some(self.debug), + physical_device_index: Some(self.physical_device_index), } - - physical_devices.sort_unstable_by(|(_, lhs), (_, rhs)| { - let lhs_device_ty = device_type(lhs.properties_v1_0.device_type); - let rhs_device_ty = device_type(rhs.properties_v1_0.device_type); - let device_ty = lhs_device_ty.cmp(&rhs_device_ty); - - if device_ty != Ordering::Equal { - return device_ty; - } - - // TODO: Select the device with the most memory - - Ordering::Equal - }); - - let (idx, _) = physical_devices[0]; - - idx } - /// A builtin [`DeviceInfo::select_physical_device`] function which prioritizes selection of - /// higher-performance discrete GPU devices. - #[profiling::function] - pub fn discrete_gpu(physical_devices: &[PhysicalDevice]) -> usize { - assert!(!physical_devices.is_empty()); - - let mut physical_devices = physical_devices.iter().enumerate().collect::>(); + #[deprecated = "use into_builder function"] + #[doc(hidden)] + pub fn to_builder(self) -> DeviceInfoBuilder { + self.into_builder() + } +} - if physical_devices.len() == 1 { - return 0; - } +impl From for DeviceInfo { + fn from(info: DeviceInfoBuilder) -> Self { + info.build() + } +} - fn device_type(ty: vk::PhysicalDeviceType) -> usize { - match ty { - vk::PhysicalDeviceType::DISCRETE_GPU => 0, - vk::PhysicalDeviceType::INTEGRATED_GPU => 1, - vk::PhysicalDeviceType::VIRTUAL_GPU => 2, - vk::PhysicalDeviceType::CPU => 3, - _ => 4, - } - } +impl DeviceInfoBuilder { + /// Builds a new `DeviceInfo`. + #[inline(always)] + pub fn build(self) -> DeviceInfo { + self.fallible_build().expect("invalid device info") + } +} - physical_devices.sort_unstable_by(|(_, lhs), (_, rhs)| { - let lhs_device_ty = device_type(lhs.properties_v1_0.device_type); - let rhs_device_ty = device_type(rhs.properties_v1_0.device_type); - let device_ty = lhs_device_ty.cmp(&rhs_device_ty); +struct DeviceInner { + accel_struct_ext: Option, + allocator: ManuallyDrop>, + device: ash::Device, + pipeline_cache: vk::PipelineCache, + queues: Box<[Box<[Mutex]>]>, + ray_trace_ext: Option, + surface_ext: Option, + swapchain_ext: Option, +} - if device_ty != Ordering::Equal { - return device_ty; +impl Drop for DeviceInner { + #[profiling::function] + fn drop(&mut self) { + if panicking() { + // When panicking we don't want the GPU allocator to complain about leaks + unsafe { + forget(ManuallyDrop::take(&mut self.allocator)); } - // TODO: Select the device with the most memory + return; + } - Ordering::Equal - }); + // trace!("drop"); - let (idx, _) = physical_devices[0]; + if let Err(err) = unsafe { self.device.device_wait_idle() } { + warn!("device_wait_idle() failed: {err}"); + } - idx - } + unsafe { + self.device + .destroy_pipeline_cache(self.pipeline_cache, None); - /// Converts a `DeviceInfo` into a `DeviceInfoBuilder`. - #[inline(always)] - pub fn to_builder(self) -> DeviceInfoBuilder { - DeviceInfoBuilder { - debug: Some(self.debug), - select_physical_device: Some(self.select_physical_device), + ManuallyDrop::drop(&mut self.allocator); } - } -} -impl Debug for DeviceInfo { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("DeviceInfo") - .field("debug", &self.debug) - .field("select_physical_device", &"fn") - .finish() + unsafe { + self.device.destroy_device(None); + } } } -impl Default for DeviceInfo { - fn default() -> Self { +#[doc(hidden)] +impl Clone for ReadOnlyDevice { + fn clone(&self) -> Self { Self { - debug: false, - select_physical_device: Box::new(DeviceInfo::discrete_gpu), + inner: self.inner.clone(), + physical_device: self.physical_device.clone(), } } } -impl From for DeviceInfo { - fn from(info: DeviceInfoBuilder) -> Self { - info.build() +#[doc(hidden)] +impl Deref for ReadOnlyDevice { + type Target = ash::Device; + + fn deref(&self) -> &Self::Target { + &self.inner.device } } -impl DeviceInfoBuilder { - /// Builds a new `DeviceInfo`. - #[inline(always)] - pub fn build(self) -> DeviceInfo { - let res = self.fallible_build(); +#[allow(deprecated)] +#[allow(unused)] +pub(crate) mod deprecated { + use { + crate::driver::{ + DriverError, + device::{Device, DeviceInfo, DeviceInfoBuilder}, + }, + ash::vk, + log::warn, + raw_window_handle::HasDisplayHandle, + std::any::Any, + }; + + impl Device { + #[deprecated = "use from_display function"] + #[doc(hidden)] + pub fn create_display( + info: impl Into, + display_handle: &impl HasDisplayHandle, + ) -> Result { + Self::try_from_display(display_handle, info) + } - #[cfg(test)] - let res = res.unwrap(); + #[deprecated = "use new function"] + #[doc(hidden)] + pub fn create_headless(info: impl Into) -> Result { + Self::new(info) + } + #[deprecated = "use format_properties function of physical_device field"] + #[doc(hidden)] + pub fn format_properties(this: &Self, format: vk::Format) -> vk::FormatProperties { + this.physical_device.format_properties(format) + } + + #[deprecated = "use image_format_properties function of physical_device field"] + #[doc(hidden)] + pub fn image_format_properties( + this: &Self, + format: vk::Format, + ty: vk::ImageType, + tiling: vk::ImageTiling, + usage: vk::ImageUsageFlags, + flags: vk::ImageCreateFlags, + ) -> Result, DriverError> { + this.physical_device + .image_format_properties(format, ty, tiling, usage, flags) + } + } - #[cfg(not(test))] - let res = unsafe { res.unwrap_unchecked() }; + impl DeviceInfo { + #[deprecated = "no effect; use physical_device_index"] + #[doc(hidden)] + pub fn integrated_gpu() { + warn!("invalid deprecated device selection hint: integrated_gpu has no effect"); + } - res + #[deprecated = "no effect; use physical_device_index"] + #[doc(hidden)] + pub fn discrete_gpu() { + warn!("invalid deprecated device selection hint: discrete_gpu has no effect"); + } } -} -#[derive(Debug)] -struct DeviceInfoBuilderError; + impl DeviceInfoBuilder { + #[deprecated = "no effect; use physical_device_index"] + #[doc(hidden)] + pub fn select_physical_device(self, _: Box) -> Self { + warn!( + "invalid deprecated device selection callback: select_physical_device has no effect" + ); -impl From for DeviceInfoBuilderError { - fn from(_: UninitializedFieldError) -> Self { - Self + self + } } } #[cfg(test)] -mod tests { +mod test { use super::*; type Info = DeviceInfo; @@ -721,7 +703,7 @@ mod tests { #[test] pub fn device_info() { - Info::default().to_builder().build(); + Info::default().into_builder().build(); } #[test] diff --git a/src/driver/graphic.rs b/src/driver/graphic.rs index ac8051e7..9d29a92a 100644 --- a/src/driver/graphic.rs +++ b/src/driver/graphic.rs @@ -6,17 +6,29 @@ use { device::Device, image::SampleCount, merge_push_constant_ranges, - shader::{ - DescriptorBindingMap, PipelineDescriptorInfo, Shader, SpecializationInfo, align_spriv, - }, + shader::{DescriptorBindingMap, PipelineDescriptorInfo, Shader, SpecializationMap}, }, ash::vk, derive_builder::{Builder, UninitializedFieldError}, log::{Level::Trace, log_enabled, trace, warn}, ordered_float::OrderedFloat, - std::{collections::HashSet, ffi::CString, sync::Arc, thread::panicking}, + std::{ + collections::HashSet, + ffi::CString, + hash::{Hash, Hasher}, + sync::{Arc, OnceLock}, + thread::panicking, + }, }; +#[deprecated = "use BlendInfo instead"] +#[doc(hidden)] +pub type BlendMode = BlendInfo; + +#[deprecated = "use DepthStencilInfo instead"] +#[doc(hidden)] +pub type DepthStencilMode = DepthStencilInfo; + const RGBA_COLOR_COMPONENTS: vk::ColorComponentFlags = vk::ColorComponentFlags::from_raw( vk::ColorComponentFlags::R.as_raw() | vk::ColorComponentFlags::G.as_raw() @@ -27,15 +39,14 @@ const RGBA_COLOR_COMPONENTS: vk::ColorComponentFlags = vk::ColorComponentFlags:: /// Specifies color blend state used when rasterization is enabled for any color attachments /// accessed during rendering. /// -/// See -/// [VkPipelineColorBlendAttachmentState](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPipelineColorBlendAttachmentState.html). +/// See [`VkPipelineColorBlendAttachmentState`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPipelineColorBlendAttachmentState.html). #[derive(Builder, Clone, Copy, Debug, Eq, Hash, PartialEq)] #[builder( - build_fn(private, name = "fallible_build", error = "BlendModeBuilderError"), + build_fn(private, name = "fallible_build", error = "UninitializedFieldError"), derive(Clone, Copy, Debug), pattern = "owned" )] -pub struct BlendMode { +pub struct BlendInfo { /// Controls whether blending is enabled for the corresponding color attachment. /// /// If blending is not enabled, the source fragment’s color for that attachment is passed @@ -70,13 +81,12 @@ pub struct BlendMode { pub alpha_blend_op: vk::BlendOp, /// A bitmask of specifying which of the R, G, B, and/or A components are enabled for writing, - /// as described for the - /// [Color Write Mask](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#framebuffer-color-write-mask). + /// as described for [`VkPipelineColorBlendAttachmentState`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPipelineColorBlendAttachmentState.html). #[builder(default = "RGBA_COLOR_COMPONENTS")] pub color_write_mask: vk::ColorComponentFlags, } -impl BlendMode { +impl BlendInfo { /// A commonly used blend mode for replacing color attachment values with new ones. pub const REPLACE: Self = Self { blend_enable: false, @@ -115,21 +125,34 @@ impl BlendMode { }; /// Specifies a default blend mode which is not enabled. - #[allow(clippy::new_ret_no_self)] - pub fn new() -> BlendModeBuilder { - BlendModeBuilder::default() + pub fn builder() -> BlendInfoBuilder { + BlendInfoBuilder::default() + } + + /// Converts a `BlendInfo` into a `BlendInfoBuilder`. + pub fn into_builder(self) -> BlendInfoBuilder { + BlendInfoBuilder { + blend_enable: Some(self.blend_enable), + src_color_blend_factor: Some(self.src_color_blend_factor), + dst_color_blend_factor: Some(self.dst_color_blend_factor), + color_blend_op: Some(self.color_blend_op), + src_alpha_blend_factor: Some(self.src_alpha_blend_factor), + dst_alpha_blend_factor: Some(self.dst_alpha_blend_factor), + alpha_blend_op: Some(self.alpha_blend_op), + color_write_mask: Some(self.color_write_mask), + } } } // the Builder derive Macro wants Default to be implemented for BlendMode -impl Default for BlendMode { +impl Default for BlendInfo { fn default() -> Self { Self::REPLACE } } -impl From for vk::PipelineColorBlendAttachmentState { - fn from(mode: BlendMode) -> Self { +impl From for vk::PipelineColorBlendAttachmentState { + fn from(mode: BlendInfo) -> Self { Self { blend_enable: mode.blend_enable as _, src_color_blend_factor: mode.src_color_blend_factor, @@ -143,105 +166,83 @@ impl From for vk::PipelineColorBlendAttachmentState { } } -// HACK: https://github.com/colin-kiegel/rust-derive-builder/issues/56 -impl BlendModeBuilder { +impl BlendInfoBuilder { /// Builds a new `BlendMode`. - pub fn build(self) -> BlendMode { - self.fallible_build().unwrap() - } -} - -#[derive(Debug)] -struct BlendModeBuilderError; - -impl From for BlendModeBuilderError { - fn from(_: UninitializedFieldError) -> Self { - Self + pub fn build(self) -> BlendInfo { + self.fallible_build().expect("invalid blend info") } } +// TODO: This could be simplified (bounds_test controsl min/max etc) /// Specifies the [depth bounds tests], [stencil test], and [depth test] pipeline state. /// -/// [depth bounds tests]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#fragops-dbt -/// [stencil test]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#fragops-stencil -/// [depth test]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#fragops-depth -#[derive(Builder, Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +/// See [`VkPipelineDepthStencilStateCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPipelineDepthStencilStateCreateInfo.html). +#[derive(Builder, Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] #[builder( - build_fn( - private, - name = "fallible_build", - error = "DepthStencilModeBuilderError" - ), + build_fn(private, name = "fallible_build", error = "UninitializedFieldError"), derive(Clone, Copy, Debug), pattern = "owned" )] -pub struct DepthStencilMode { +pub struct DepthStencilInfo { /// Control parameters of the stencil test. + #[builder(default)] pub back: StencilMode, /// Controls whether [depth bounds testing] is enabled. /// - /// [depth bounds testing]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#fragops-dbt + /// See [`VkPipelineMultisampleStateCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPipelineMultisampleStateCreateInfo.html). + #[builder(default)] pub bounds_test: bool, /// A value specifying the comparison operator to use in the [depth comparison] step of the /// [depth test]. /// - /// [depth comparison]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#fragops-depth-comparison - /// [depth test]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#fragops-depth + /// See [`VkPipelineDepthStencilStateCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPipelineDepthStencilStateCreateInfo.html). + #[builder(default)] pub compare_op: vk::CompareOp, /// Controls whether [depth testing] is enabled. /// - /// [depth testing]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#fragops-depth + /// See [`VkPipelineDepthStencilStateCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPipelineDepthStencilStateCreateInfo.html). + #[builder(default)] pub depth_test: bool, /// Controls whether [depth writes] are enabled when `depth_test` is `true`. /// /// Depth writes are always disabled when `depth_test` is `false`. /// - /// [depth writes]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#fragops-depth-write + /// See [`VkPipelineDepthStencilStateCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPipelineDepthStencilStateCreateInfo.html). + #[builder(default)] pub depth_write: bool, /// Control parameters of the stencil test. + #[builder(default)] pub front: StencilMode, // Note: Using setter(into) so caller does not need our version of OrderedFloat /// Minimum depth bound used in the [depth bounds test]. /// - /// [depth bounds test]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#fragops-dbt - #[builder(setter(into))] + /// See [`VkPipelineDepthStencilStateCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPipelineDepthStencilStateCreateInfo.html). + #[builder(default, setter(into))] pub min: OrderedFloat, // Note: Using setter(into) so caller does not need our version of OrderedFloat /// Maximum depth bound used in the [depth bounds test]. /// - /// [depth bounds test]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#fragops-dbt - #[builder(setter(into))] + /// See [`VkPipelineDepthStencilStateCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPipelineDepthStencilStateCreateInfo.html). + #[builder(default, setter(into))] pub max: OrderedFloat, /// Controls whether [stencil testing] is enabled. /// - /// [stencil testing]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#fragops-stencil + /// See [`VkPipelineDepthStencilStateCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPipelineDepthStencilStateCreateInfo.html). + #[builder(default)] pub stencil_test: bool, } -impl DepthStencilMode { - /// A commonly used depth/stencil mode - pub const DEPTH_READ: Self = Self { - back: StencilMode::IGNORE, - bounds_test: true, - compare_op: vk::CompareOp::LESS, - depth_test: true, - depth_write: false, - front: StencilMode::IGNORE, - min: OrderedFloat(0.0), - max: OrderedFloat(1.0), - stencil_test: false, - }; - +impl DepthStencilInfo { /// A commonly used depth/stencil mode - pub const DEPTH_WRITE: Self = Self { + pub const DEPTH_WRITE_LESS_IGNORE_STENCIL: Self = Self { back: StencilMode::IGNORE, bounds_test: true, compare_op: vk::CompareOp::LESS, @@ -254,6 +255,8 @@ impl DepthStencilMode { }; /// Specifies a no-depth/no-stencil mode. + /// + /// This is the default state. pub const IGNORE: Self = Self { back: StencilMode::IGNORE, bounds_test: false, @@ -266,105 +269,64 @@ impl DepthStencilMode { stencil_test: false, }; - /// Specifies a default depth/stencil mode which is equal to [`DepthStencilMode::IGNORE`]. - #[allow(clippy::new_ret_no_self)] - pub fn new() -> DepthStencilModeBuilder { - DepthStencilModeBuilder::default() + /// Creates a default `DepthStencilInfoBuilder`. + pub fn builder() -> DepthStencilInfoBuilder { + Default::default() } -} -impl From for vk::PipelineDepthStencilStateCreateInfo<'_> { - fn from(mode: DepthStencilMode) -> Self { - Self::default() - .back(mode.back.into()) - .depth_bounds_test_enable(mode.bounds_test as _) - .depth_compare_op(mode.compare_op) - .depth_test_enable(mode.depth_test as _) - .depth_write_enable(mode.depth_write as _) - .front(mode.front.into()) - .max_depth_bounds(mode.max.into_inner()) - .min_depth_bounds(mode.min.into_inner()) - .stencil_test_enable(mode.stencil_test as _) + #[deprecated = "use builder function"] + #[doc(hidden)] + pub fn build() -> DepthStencilInfoBuilder { + Self::builder() } -} - -// HACK: https://github.com/colin-kiegel/rust-derive-builder/issues/56 -impl DepthStencilModeBuilder { - /// Builds a new `DepthStencilMode`. - pub fn build(mut self) -> DepthStencilMode { - if self.back.is_none() { - self.back = Some(Default::default()); - } - - if self.bounds_test.is_none() { - self.bounds_test = Some(Default::default()); - } - if self.compare_op.is_none() { - self.compare_op = Some(Default::default()); + /// Converts a `DepthStencilInfo` into a `DepthStencilInfoBuilder`. + pub fn into_builder(self) -> DepthStencilInfoBuilder { + DepthStencilInfoBuilder { + back: Some(self.back), + bounds_test: Some(self.bounds_test), + compare_op: Some(self.compare_op), + depth_test: Some(self.depth_test), + depth_write: Some(self.depth_write), + front: Some(self.front), + max: Some(self.max), + min: Some(self.min), + stencil_test: Some(self.stencil_test), } - - if self.depth_test.is_none() { - self.depth_test = Some(Default::default()); - } - - if self.depth_write.is_none() { - self.depth_write = Some(Default::default()); - } - - if self.front.is_none() { - self.front = Some(Default::default()); - } - - if self.min.is_none() { - self.min = Some(Default::default()); - } - - if self.max.is_none() { - self.max = Some(Default::default()); - } - - if self.stencil_test.is_none() { - self.stencil_test = Some(Default::default()); - } - - self.fallible_build() - .expect("All required fields set at initialization") } } -#[derive(Debug)] -struct DepthStencilModeBuilderError; +impl From for vk::PipelineDepthStencilStateCreateInfo<'_> { + fn from(info: DepthStencilInfo) -> Self { + Self::default() + .back(info.back.into()) + .depth_bounds_test_enable(info.bounds_test as _) + .depth_compare_op(info.compare_op) + .depth_test_enable(info.depth_test as _) + .depth_write_enable(info.depth_write as _) + .front(info.front.into()) + .max_depth_bounds(info.max.into_inner()) + .min_depth_bounds(info.min.into_inner()) + .stencil_test_enable(info.stencil_test as _) + } +} -impl From for DepthStencilModeBuilderError { - fn from(_: UninitializedFieldError) -> Self { - Self +impl DepthStencilInfoBuilder { + /// Builds a new `DepthStencilInfo`. + pub fn build(self) -> DepthStencilInfo { + self.fallible_build().expect("invalid depth stencil info") } } -/// Opaque representation of a [pipeline] object. +/// Opaque representation of a pipeline object. /// /// Also contains information about the object. /// /// [pipeline]: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPipeline.html -#[derive(Debug)] +#[derive(Clone, Debug)] +#[read_only::cast] pub struct GraphicPipeline { - pub(crate) descriptor_bindings: DescriptorBindingMap, - pub(crate) descriptor_info: PipelineDescriptorInfo, - device: Arc, - - /// Information used to create this object. - pub info: GraphicPipelineInfo, - - pub(crate) input_attachments: Box<[u32]>, - pub(crate) layout: vk::PipelineLayout, - - /// A descriptive name used in debugging messages. - pub name: Option, - - pub(crate) push_constants: Vec, - pub(crate) shader_modules: Vec, - pub(super) state: GraphicPipelineState, + pub(crate) inner: Arc, } impl GraphicPipeline { @@ -384,12 +346,12 @@ impl GraphicPipeline { /// ```no_run /// # use std::sync::Arc; /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::graphic::{GraphicPipeline, GraphicPipelineInfo}; - /// # use screen_13::driver::shader::Shader; + /// # use vk_graph::driver::DriverError; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::graphic::{GraphicPipeline, GraphicPipelineInfo}; + /// # use vk_graph::driver::shader::Shader; /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); + /// # let device = Device::new(DeviceInfo::default())?; /// # let my_frag_code = [0u8; 1]; /// # let my_vert_code = [0u8; 1]; /// // shader code is raw SPIR-V code as bytes @@ -398,12 +360,12 @@ impl GraphicPipeline { /// let info = GraphicPipelineInfo::default(); /// let pipeline = GraphicPipeline::create(&device, info, [vert, frag])?; /// - /// assert_eq!(pipeline.info.front_face, vk::FrontFace::COUNTER_CLOCKWISE); + /// assert_eq!(pipeline.info().front_face, vk::FrontFace::COUNTER_CLOCKWISE); /// # Ok(()) } /// ``` #[profiling::function] pub fn create( - device: &Arc, + device: &Device, info: impl Into, shaders: impl IntoIterator, ) -> Result @@ -412,7 +374,7 @@ impl GraphicPipeline { { trace!("create"); - let device = Arc::clone(device); + let device = device.clone(); let info = info.into(); let shaders = shaders .into_iter() @@ -422,8 +384,8 @@ impl GraphicPipeline { let vertex_input = shaders .iter() .find(|shader| shader.stage == vk::ShaderStageFlags::VERTEX) - .expect("vertex shader not found") - .vertex_input(); + .expect("missing vertex shader") + .try_vertex_input()?; // Check for proper stages because vulkan may not complain but this is bad let has_fragment_stage = shaders @@ -460,8 +422,8 @@ impl GraphicPipeline { let descriptor_sets_layouts = descriptor_info .layouts .values() - .map(|descriptor_set_layout| **descriptor_set_layout) - .collect::>(); + .map(|descriptor_set_layout| descriptor_set_layout.handle) + .collect::>(); let push_constants = shaders .iter() @@ -473,7 +435,7 @@ impl GraphicPipeline { let (input, write) = shaders .iter() .find(|shader| shader.stage == vk::ShaderStageFlags::FRAGMENT) - .expect("fragment shader not found") + .expect("missing fragment shader") .attachments(); let (input, write) = ( input @@ -505,42 +467,33 @@ impl GraphicPipeline { None, ) .map_err(|err| { - warn!("{err}"); + warn!("unable to create graphic pipeline layout: {err}"); DriverError::Unsupported })?; - let shader_info = shaders + let shader_stages = shaders .into_iter() .map(|shader| { let shader_module = device .create_shader_module( - &vk::ShaderModuleCreateInfo::default() - .code(align_spriv(&shader.spirv)?), + &vk::ShaderModuleCreateInfo::default().code(shader.spirv.words()), None, ) .map_err(|err| { - warn!("{err}"); + warn!("unable to create graphic shader module: {err}"); DriverError::Unsupported })?; - let shader_stage = Stage { + let shader_stage = ShaderStage { flags: shader.stage, module: shader_module, - name: CString::new(shader.entry_name.as_str()).unwrap(), - specialization_info: shader.specialization_info, + name: CString::new(shader.entry_name.as_str()).expect("invalid entry name"), + specialization: shader.specialization, }; - Result::<_, DriverError>::Ok((shader_module, shader_stage)) + Result::<_, DriverError>::Ok(shader_stage) }) - .collect::, _>>()?; - let mut shader_modules = vec![]; - let mut stages = vec![]; - shader_info - .into_iter() - .for_each(|(shader_module, shader_stage)| { - shader_modules.push(shader_module); - stages.push(shader_stage); - }); + .collect::, _>>()?; let mut multisample = MultisampleState { alpha_to_coverage_enable: info.alpha_to_coverage, @@ -568,69 +521,89 @@ impl GraphicPipeline { multisample.min_sample_shading = min_sample_shading; } - let push_constants = merge_push_constant_ranges(&push_constants); + let push_constants = merge_push_constant_ranges(&push_constants).into_boxed_slice(); Ok(Self { - descriptor_bindings, - descriptor_info, - device, - info, - input_attachments, - layout, - name: None, - push_constants, - shader_modules, - state: GraphicPipelineState { + inner: Arc::new(GraphicPipelineInner { + descriptor_bindings, + descriptor_info, + device, + info, + input_attachments, layout, multisample, - stages, + name: Default::default(), + push_constants, + shader_stages, vertex_input, - }, + }), }) } } - /// Sets the debugging name assigned to this pipeline. - pub fn with_name(mut this: Self, name: impl Into) -> Self { - this.name = Some(name.into()); - this + /// Gets the debugging name assigned to this pipeline, if one has been set. + pub fn debug_name(&self) -> Option<&str> { + self.inner.name.get().map(String::as_str) } -} -impl Drop for GraphicPipeline { - #[profiling::function] - fn drop(&mut self) { - if panicking() { + /// The device which owns this graphic pipeline. + pub fn device(&self) -> &Device { + &self.inner.device + } + + /// Gets the information used to create this object. + pub fn info(&self) -> GraphicPipelineInfo { + self.inner.info + } + + /// Sets the debugging name assigned to this pipeline. + /// + /// _Note:_ The pipeline name may only be assigned once. Subsequent calls will not update the + /// previously set name value. + pub fn set_debug_name(&mut self, name: impl Into) { + if !self.inner.device.physical_device.instance.info.debug { return; } - unsafe { - self.device.destroy_pipeline_layout(self.layout, None); - } + // Both Ok and Err are valid conditions + let _ = self.inner.name.set(name.into()); + } - for shader_module in self.shader_modules.drain(..) { - unsafe { - self.device.destroy_shader_module(shader_module, None); - } - } + /// Sets the debugging name assigned to this pipeline. + /// + /// _Note:_ The pipeline name may only be assigned once. Subsequent calls will not update the + /// previously set name value. + pub fn with_debug_name(mut self, name: impl Into) -> Self { + self.set_debug_name(name); + + self + } +} + +impl Eq for GraphicPipeline {} + +impl Hash for GraphicPipeline { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.inner).hash(state); + } +} + +impl PartialEq for GraphicPipeline { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.inner, &other.inner) } } /// Information used to create a [`GraphicPipeline`] instance. #[derive(Builder, Clone, Copy, Debug, Eq, Hash, PartialEq)] #[builder( - build_fn( - private, - name = "fallible_build", - error = "GraphicPipelineInfoBuilderError" - ), + build_fn(private, name = "fallible_build", error = "UninitializedFieldError"), derive(Clone, Copy, Debug), pattern = "owned" )] -#[non_exhaustive] pub struct GraphicPipelineInfo { - /// Controls whether a temporary coverage value is generated based on the alpha component of the - /// fragment’s first color output. + /// Controls whether a temporary coverage value is generated based on the alpha component of + /// the fragment’s first color output. #[builder(default)] pub alpha_to_coverage: bool, @@ -649,17 +622,17 @@ pub struct GraphicPipelineInfo { /// Basic usage (GLSL): /// /// ``` - /// # inline_spirv::inline_spirv!(r#" + /// # vk_shader_macros::glsl!(r#" /// #version 460 core /// #extension GL_EXT_nonuniform_qualifier : require + /// #pragma shader_stage(fragment) /// /// layout(set = 0, binding = 0) uniform sampler2D my_binding[]; /// - /// void main() - /// { + /// void main() { /// // my_binding will have space for 8,192 images by default /// } - /// # "#, frag); + /// # "#); /// ``` #[builder(default = "8192")] pub bindless_descriptor_count: u32, @@ -667,9 +640,9 @@ pub struct GraphicPipelineInfo { /// Specifies color blend state used when rasterization is enabled for any color attachments /// accessed during rendering. /// - /// The default value is [`BlendMode::REPLACE`]. + /// The default value is [`BlendInfo::REPLACE`]. #[builder(default)] - pub blend: BlendMode, + pub blend: BlendInfo, /// Bitmask controlling triangle culling. /// @@ -703,21 +676,19 @@ pub struct GraphicPipelineInfo { /// /// The default value is `SampleCount::Type1`. /// - /// See [multisampling](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#primsrast-multisampling). + /// See [`VkPipelineMultisampleStateCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPipelineMultisampleStateCreateInfo.html). #[builder(default = "SampleCount::Type1")] pub samples: SampleCount, } impl GraphicPipelineInfo { /// Creates a default `GraphicPipelineInfoBuilder`. - #[allow(clippy::new_ret_no_self)] pub fn builder() -> GraphicPipelineInfoBuilder { Default::default() } /// Converts a `GraphicPipelineInfo` into a `GraphicPipelineInfoBuilder`. - #[inline(always)] - pub fn to_builder(self) -> GraphicPipelineInfoBuilder { + pub fn into_builder(self) -> GraphicPipelineInfoBuilder { GraphicPipelineInfoBuilder { alpha_to_coverage: Some(self.alpha_to_coverage), alpha_to_one: Some(self.alpha_to_one), @@ -731,6 +702,12 @@ impl GraphicPipelineInfo { samples: Some(self.samples), } } + + #[deprecated = "use into_builder function"] + #[doc(hidden)] + pub fn to_builder(self) -> GraphicPipelineInfoBuilder { + self.into_builder() + } } impl Default for GraphicPipelineInfo { @@ -739,7 +716,7 @@ impl Default for GraphicPipelineInfo { alpha_to_coverage: false, alpha_to_one: false, bindless_descriptor_count: 8192, - blend: BlendMode::REPLACE, + blend: BlendInfo::REPLACE, cull_mode: vk::CullModeFlags::BACK, front_face: vk::FrontFace::COUNTER_CLOCKWISE, min_sample_shading: None, @@ -760,37 +737,47 @@ impl GraphicPipelineInfoBuilder { /// Builds a new `GraphicPipelineInfo`. #[inline(always)] pub fn build(self) -> GraphicPipelineInfo { - let res = self.fallible_build(); - - #[cfg(test)] - let res = res.unwrap(); - - #[cfg(not(test))] - let res = unsafe { res.unwrap_unchecked() }; - - res - } -} - -#[derive(Debug)] -struct GraphicPipelineInfoBuilderError; - -impl From for GraphicPipelineInfoBuilderError { - fn from(_: UninitializedFieldError) -> Self { - Self + self.fallible_build() + .expect("invalid graphic pipeline info") } } #[derive(Debug)] -pub(super) struct GraphicPipelineState { +pub(crate) struct GraphicPipelineInner { + pub descriptor_bindings: DescriptorBindingMap, + pub descriptor_info: PipelineDescriptorInfo, + pub device: Device, + pub info: GraphicPipelineInfo, + pub input_attachments: Box<[u32]>, pub layout: vk::PipelineLayout, pub multisample: MultisampleState, - pub stages: Vec, + pub name: OnceLock, + pub push_constants: Box<[vk::PushConstantRange]>, + pub shader_stages: Box<[ShaderStage]>, pub vertex_input: VertexInputState, } +impl Drop for GraphicPipelineInner { + #[profiling::function] + fn drop(&mut self) { + if panicking() { + return; + } + + unsafe { + self.device.destroy_pipeline_layout(self.layout, None); + } + + for shader_stage in &mut self.shader_stages { + unsafe { + self.device.destroy_shader_module(shader_stage.module, None); + } + } + } +} + #[derive(Debug, Default)] -pub(super) struct MultisampleState { +pub(crate) struct MultisampleState { pub alpha_to_coverage_enable: bool, pub alpha_to_one_enable: bool, pub flags: vk::PipelineMultisampleStateCreateFlags, @@ -801,17 +788,16 @@ pub(super) struct MultisampleState { } #[derive(Debug)] -pub(super) struct Stage { +pub(crate) struct ShaderStage { pub flags: vk::ShaderStageFlags, pub module: vk::ShaderModule, - pub name: CString, - pub specialization_info: Option, + pub name: CString, // TODO + pub specialization: Option, } /// Specifies stencil mode during rasterization. /// -/// See -/// [stencil test](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#fragops-stencil). +/// See [`VkStencilOpState`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkStencilOpState.html). #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct StencilMode { /// The action performed on samples that fail the stencil test. @@ -833,7 +819,8 @@ pub struct StencilMode { /// framebuffer attachment. pub write_mask: u32, - /// An unsigned integer stencil reference value that is used in the unsigned stencil comparison. + /// An unsigned integer stencil reference value that is used in the unsigned stencil + /// comparison. pub reference: u32, } @@ -871,30 +858,123 @@ impl From for vk::StencilOpState { } #[derive(Clone, Debug, Default)] -pub(super) struct VertexInputState { +pub(crate) struct VertexInputState { pub vertex_binding_descriptions: Vec, pub vertex_attribute_descriptions: Vec, } +mod deprecated { + use { + crate::driver::graphic::{ + BlendInfo, BlendInfoBuilder, DepthStencilInfo, GraphicPipeline, StencilMode, + }, + ash::vk, + ordered_float::OrderedFloat, + }; + + impl BlendInfo { + #[allow(clippy::new_ret_no_self)] + #[deprecated = "use builder function or BlendInfoBuilder"] + #[doc(hidden)] + pub fn new() -> BlendInfoBuilder { + Default::default() + } + } + + impl DepthStencilInfo { + /// A commonly used depth/stencil mode + #[deprecated = "use constructor or builder function"] + pub const DEPTH_READ: Self = Self { + back: StencilMode::IGNORE, + bounds_test: true, + compare_op: vk::CompareOp::LESS, + depth_test: true, + depth_write: false, + front: StencilMode::IGNORE, + min: OrderedFloat(0.0), + max: OrderedFloat(1.0), + stencil_test: false, + }; + + /// A commonly used depth/stencil mode + #[deprecated = "use DEPTH_WRITE_LESS_IGNORE_STENCIL"] + pub const DEPTH_WRITE: Self = Self { + back: StencilMode::IGNORE, + bounds_test: true, + compare_op: vk::CompareOp::LESS, + depth_test: true, + depth_write: true, + front: StencilMode::IGNORE, + min: OrderedFloat(0.0), + max: OrderedFloat(1.0), + stencil_test: false, + }; + + #[allow(clippy::new_ret_no_self)] + #[deprecated = "use builder function or DepthStencilInfoBuilder"] + #[doc(hidden)] + pub fn new() -> BlendInfoBuilder { + Default::default() + } + } + + impl GraphicPipeline { + #[deprecated = "use with_debug_name function"] + #[doc(hidden)] + pub fn with_name(this: Self, name: impl Into) -> Self { + this.with_debug_name(name) + } + } +} + #[cfg(test)] -mod tests { +mod test { use super::*; - type Info = GraphicPipelineInfo; - type Builder = GraphicPipelineInfoBuilder; + #[test] + pub fn blend_info() { + let info = BlendInfo::default(); + let builder = info.into_builder().build(); + + assert_eq!(info, builder); + } + + #[test] + pub fn blend_info_builder() { + let info = BlendInfo::default(); + let builder = BlendInfoBuilder::default().build(); + + assert_eq!(info, builder); + } + + #[test] + pub fn depth_stencil_info() { + let info = DepthStencilInfo::default(); + let builder = info.into_builder().build(); + + assert_eq!(info, builder); + } + + #[test] + pub fn depth_stencil_info_builder() { + let info = DepthStencilInfo::default(); + let builder = DepthStencilInfoBuilder::default().build(); + + assert_eq!(info, builder); + } #[test] pub fn graphic_pipeline_info() { - let info = Info::default(); - let builder = info.to_builder().build(); + let info = GraphicPipelineInfo::default(); + let builder = info.into_builder().build(); assert_eq!(info, builder); } #[test] pub fn graphic_pipeline_info_builder() { - let info = Info::default(); - let builder = Builder::default().build(); + let info = GraphicPipelineInfo::default(); + let builder = GraphicPipelineInfoBuilder::default().build(); assert_eq!(info, builder); } diff --git a/src/driver/image.rs b/src/driver/image.rs index 536a3827..7c6b28cc 100644 --- a/src/driver/image.rs +++ b/src/driver/image.rs @@ -13,18 +13,17 @@ use { collections::{HashMap, hash_map::Entry}, fmt::{Debug, Formatter}, mem::{replace, take}, - ops::{Deref, DerefMut}, - sync::Arc, + ops::DerefMut, thread::panicking, }, vk_sync::AccessType, }; #[cfg(feature = "parking_lot")] -use parking_lot::Mutex; +use parking_lot::{Mutex, MutexGuard}; #[cfg(not(feature = "parking_lot"))] -use std::sync::Mutex; +use std::sync::{Mutex, MutexGuard}; #[cfg(debug_assertions)] fn assert_aspect_mask_supported(aspect_mask: vk::ImageAspectFlags) { @@ -67,40 +66,48 @@ pub(crate) fn image_subresource_range_intersects( /// /// Also contains information about the object. /// -/// ## `Deref` behavior -/// -/// `Image` automatically dereferences to [`vk::Image`] (via the [`Deref`] trait), so you can -/// call `vk::Image`'s methods on a value of type `Image`. To avoid name clashes with `vk::Image`'s -/// methods, the methods of `Image` itself are associated functions, called using -/// [fully qualified syntax]: -/// /// ```no_run -/// # use std::sync::Arc; /// # use ash::vk; -/// # use screen_13::driver::{AccessType, DriverError}; -/// # use screen_13::driver::device::{Device, DeviceInfo}; -/// # use screen_13::driver::image::{Image, ImageInfo}; +/// # use vk_graph::driver::{AccessType, DriverError}; +/// # use vk_graph::driver::device::{Device, DeviceInfo}; +/// # use vk_graph::driver::image::{Image, ImageInfo}; /// # fn main() -> Result<(), DriverError> { -/// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); -/// # let info = ImageInfo::image_1d(1, vk::Format::R8_UINT, vk::ImageUsageFlags::STORAGE); -/// # let my_image = Image::create(&device, info)?; -/// # let my_subresource_range = vk::ImageSubresourceRange::default(); -/// let prev = Image::access(&my_image, AccessType::AnyShaderWrite, my_subresource_range); +/// # let device = Device::new(DeviceInfo::default())?; +/// let fmt = vk::Format::R8G8B8A8_UNORM; +/// let usage = vk::ImageUsageFlags::SAMPLED; +/// let info = ImageInfo::image_2d(320, 200, fmt, usage); +/// let my_img = Image::create(&device, info)?; +/// +/// assert_eq!(my_img.info, info); +/// assert_ne!(my_img.handle, vk::Image::null()); /// # Ok(()) } /// ``` /// -/// [image]: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkImage.html -/// [deref]: core::ops::Deref -/// [fully qualified syntax]: https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#fully-qualified-syntax-for-disambiguation-calling-methods-with-the-same-name +/// [image]: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkBuffer.html +#[read_only::cast] pub struct Image { accesses: Mutex>, allocation: Option, // None when we don't own the image (Swapchain images) - pub(super) device: Arc, - image: vk::Image, + + /// The device which owns this image resource. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub device: Device, + + /// The native Vulkan resource handle of this image. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub handle: vk::Image, + #[allow(clippy::type_complexity)] image_view_cache: Mutex>, - /// Information used to create this object. + /// Information used to create this resource. + /// + /// _Note:_ This field is read-only. + #[readonly] pub info: ImageInfo, /// A name for debugging purposes. @@ -117,21 +124,26 @@ impl Image { /// ```no_run /// # use std::sync::Arc; /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::image::{Image, ImageInfo}; + /// # use vk_graph::driver::DriverError; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::image::{Image, ImageInfo}; /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); - /// let info = ImageInfo::image_2d(32, 32, vk::Format::R8G8B8A8_UNORM, vk::ImageUsageFlags::SAMPLED); + /// # let device = Device::new(DeviceInfo::default())?; + /// let info = ImageInfo::image_2d( + /// 32, + /// 32, + /// vk::Format::R8G8B8A8_UNORM, + /// vk::ImageUsageFlags::SAMPLED, + /// ); /// let image = Image::create(&device, info)?; /// - /// assert_ne!(*image, vk::Image::null()); + /// assert_ne!(image.handle, vk::Image::null()); /// assert_eq!(image.info.width, 32); /// assert_eq!(image.info.height, 32); /// # Ok(()) } /// ``` #[profiling::function] - pub fn create(device: &Arc, info: impl Into) -> Result { + pub fn create(device: &Device, info: impl Into) -> Result { let info: ImageInfo = info.into(); //trace!("create: {:?}", &info); @@ -145,72 +157,77 @@ impl Image { let accesses = Mutex::new(ImageAccess::new(info, AccessType::Nothing)); - let device = Arc::clone(device); + let device = device.clone(); let create_info: ImageCreateInfo = info.into(); let create_info = create_info.queue_family_indices(&device.physical_device.queue_family_indices); - let image = unsafe { + let handle = unsafe { device.create_image(&create_info, None).map_err(|err| { warn!("unable to create image: {err}"); DriverError::Unsupported })? }; - let requirements = unsafe { device.get_image_memory_requirements(image) }; + let requirements = unsafe { device.get_image_memory_requirements(handle) }; + let allocation_scheme = if info.dedicated { + AllocationScheme::DedicatedImage(handle) + } else { + AllocationScheme::GpuAllocatorManaged + }; let allocation = { profiling::scope!("allocate"); - #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] - let mut allocator = device.allocator.lock(); - - #[cfg(not(feature = "parking_lot"))] - let mut allocator = allocator.unwrap(); - - allocator - .allocate(&AllocationCreateDesc { - name: "image", - requirements, - location: MemoryLocation::GpuOnly, - linear: false, - allocation_scheme: AllocationScheme::GpuAllocatorManaged, - }) - .map_err(|err| { - warn!("unable to allocate image memory: {err}"); - - unsafe { - device.destroy_image(image, None); - } - - DriverError::from_alloc_err(err) - }) - .and_then(|allocation| { - if let Err(err) = unsafe { - device.bind_image_memory(image, allocation.memory(), allocation.offset()) - } { - warn!("unable to bind image memory: {err}"); - - if let Err(err) = allocator.free(allocation) { - warn!("unable to free image allocation: {err}") - } + Device::with_allocator(&device, |allocator| { + allocator + .allocate(&AllocationCreateDesc { + name: "image", + requirements, + location: MemoryLocation::GpuOnly, + linear: false, + allocation_scheme, + }) + .map_err(|err| { + warn!("unable to allocate image memory: {err}"); unsafe { - device.destroy_image(image, None); + device.destroy_image(handle, None); } - Err(DriverError::OutOfMemory) - } else { - Ok(allocation) - } - }) + DriverError::from_alloc_err(err) + }) + .and_then(|allocation| { + if let Err(err) = unsafe { + device.bind_image_memory( + handle, + allocation.memory(), + allocation.offset(), + ) + } { + warn!("unable to bind image memory: {err}"); + + if let Err(err) = allocator.free(allocation) { + warn!("unable to free image allocation: {err}") + } + + unsafe { + device.destroy_image(handle, None); + } + + Err(DriverError::OutOfMemory) + } else { + Ok(allocation) + } + }) + }) }?; - debug_assert_ne!(image, vk::Image::null()); + debug_assert_ne!(handle, vk::Image::null()); Ok(Self { accesses, allocation: Some(allocation), device, - image, + handle, image_view_cache: Mutex::new(Default::default()), info, name: None, @@ -224,7 +241,7 @@ impl Image { /// /// # Note /// - /// Used to maintain object state when passing a _Screen 13_-created `vk::Image` handle to + /// Used to maintain object state when passing a _vk-graph_-created `vk::Image` handle to /// external code such as [_Ash_] or [_Erupt_] bindings. /// /// # Examples @@ -234,11 +251,11 @@ impl Image { /// ```no_run /// # use std::sync::Arc; /// # use ash::vk; - /// # use screen_13::driver::{AccessType, DriverError}; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::image::{Image, ImageInfo}; + /// # use vk_graph::driver::{AccessType, DriverError}; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::image::{Image, ImageInfo}; /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); + /// # let device = Device::new(DeviceInfo::default())?; /// # let info = ImageInfo::image_1d(1, vk::Format::R8_UINT, vk::ImageUsageFlags::STORAGE); /// # let my_image = Image::create(&device, info)?; /// # let my_subresource_range = vk::ImageSubresourceRange::default(); @@ -262,7 +279,7 @@ impl Image { /// [_Erupt_]: https://crates.io/crates/erupt #[profiling::function] pub fn access( - this: &Self, + &self, access: AccessType, mut access_range: vk::ImageSubresourceRange, ) -> impl Iterator + '_ { @@ -270,93 +287,56 @@ impl Image { { assert_aspect_mask_supported(access_range.aspect_mask); - assert!(format_aspect_mask(this.info.fmt).contains(access_range.aspect_mask)); + assert!(format_aspect_mask(self.info.fmt).contains(access_range.aspect_mask)); } if access_range.layer_count == vk::REMAINING_ARRAY_LAYERS { - debug_assert!(access_range.base_array_layer < this.info.array_layer_count); + debug_assert!(access_range.base_array_layer < self.info.array_layer_count); - access_range.layer_count = this.info.array_layer_count - access_range.base_array_layer + access_range.layer_count = self.info.array_layer_count - access_range.base_array_layer } debug_assert!( - access_range.base_array_layer + access_range.layer_count <= this.info.array_layer_count + access_range.base_array_layer + access_range.layer_count <= self.info.array_layer_count ); if access_range.level_count == vk::REMAINING_MIP_LEVELS { - debug_assert!(access_range.base_mip_level < this.info.mip_level_count); + debug_assert!(access_range.base_mip_level < self.info.mip_level_count); - access_range.level_count = this.info.mip_level_count - access_range.base_mip_level + access_range.level_count = self.info.mip_level_count - access_range.base_mip_level } debug_assert!( - access_range.base_mip_level + access_range.level_count <= this.info.mip_level_count + access_range.base_mip_level + access_range.level_count <= self.info.mip_level_count ); - let accesses = this.accesses.lock(); - - #[cfg(not(feature = "parking_lot"))] - let accesses = accesses.unwrap(); - - ImageAccessIter::new(accesses, access, access_range) + ImageAccessIter::new(self.lock_accesses(), access, access_range) } - #[profiling::function] - pub(super) fn clone_swapchain(this: &Self) -> Self { - // Moves the image view cache from the current instance to the clone! - #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] - let mut image_view_cache = this.image_view_cache.lock(); - - #[cfg(not(feature = "parking_lot"))] - let mut image_view_cache = image_view_cache.unwrap(); - - let image_view_cache = take(&mut *image_view_cache); - - // Does NOT copy over the image accesses! - // Force previous access to general to wait for presentation - let Self { image, info, .. } = *this; - let accesses = ImageAccess::new(info, AccessType::General); - let accesses = Mutex::new(accesses); + /// Sets the debugging name assigned to this image. + pub fn debug_name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); - Self { - accesses, - allocation: None, - device: Arc::clone(&this.device), - image, - image_view_cache: Mutex::new(image_view_cache), - info, - name: this.name.clone(), - } + self } + /// Drops the given allocation, all views, and the handle. #[profiling::function] - fn drop_allocation(this: &Self, allocation: Allocation) { + fn drop_allocation(&self, allocation: Allocation) { { profiling::scope!("views"); - #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] - let mut image_view_cache = this.image_view_cache.lock(); - - #[cfg(not(feature = "parking_lot"))] - let mut image_view_cache = image_view_cache.unwrap(); - - image_view_cache.clear(); + self.with_image_view_cache(|cache| cache.clear()); } unsafe { - this.device.destroy_image(this.image, None); + self.device.destroy_image(self.handle, None); } { profiling::scope!("deallocate"); - #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] - let mut allocator = this.device.allocator.lock(); - - #[cfg(not(feature = "parking_lot"))] - let mut allocator = allocator.unwrap(); - - allocator.free(allocation) + Device::with_allocator(&self.device, |allocator| allocator.free(allocation)) } .unwrap_or_else(|err| warn!("unable to free image allocation: {err}")); } @@ -366,64 +346,104 @@ impl Image { /// The image is not destroyed automatically on drop, unlike images created through the /// [`Image::create`] function. #[profiling::function] - pub fn from_raw(device: &Arc, image: vk::Image, info: impl Into) -> Self { - let device = Arc::clone(device); + pub fn from_raw(device: &Device, handle: vk::Image, info: impl Into) -> Self { + let device = device.clone(); let info = info.into(); - // For now default all image access to general, but maybe make this configurable later. - // This helps make sure the first presentation of a swapchain image doesn't throw a - // validation error, but it could also be very useful for raw vulkan images from other - // sources. - let accesses = ImageAccess::new(info, AccessType::General); + let accesses = ImageAccess::new(info, AccessType::Nothing); Self { accesses: Mutex::new(accesses), allocation: None, device, - image, + handle, image_view_cache: Mutex::new(Default::default()), info, name: None, } } - #[profiling::function] - pub(crate) fn view(this: &Self, info: ImageViewInfo) -> Result { - #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] - let mut image_view_cache = this.image_view_cache.lock(); + fn lock_accesses(&self) -> MutexGuard<'_, ImageAccess> { + let accesses = self.accesses.lock(); #[cfg(not(feature = "parking_lot"))] - let mut image_view_cache = image_view_cache.unwrap(); - - Ok(match image_view_cache.entry(info) { - Entry::Occupied(entry) => entry.get().image_view, - Entry::Vacant(entry) => { - entry - .insert(ImageView::create(&this.device, info, this.image)?) - .image_view - } + let accesses = accesses.expect("poisoned image access lock"); + + accesses + } + + /// Produces a new `Image` sharing the same Vulkan handle with independent access tracking. + /// + /// The returned image retains the handle, device, and debug name of `self` but starts with + /// no prior access history (`AccessType::Nothing`) and does not claim ownership of the image's + /// memory backing. Internal caches are moved out of `self` so they are not duplicated. + /// + /// This is used to create separate tracking instances for swapchain images that may be + /// used concurrently across different graph executions. + /// + /// # Safety + /// + /// The caller must ensure the Vulkan image handle remains valid for the lifetime of the + /// returned `Image`. This function should only be called on swapchain images or other + /// platform or extension images. + #[profiling::function] + pub unsafe fn to_detached(&self) -> Self { + debug_assert!(self.allocation.is_none()); + + let image_view_cache = self.with_image_view_cache(take); + + let Self { handle, info, .. } = *self; + + Self { + accesses: Mutex::new(ImageAccess::new(info, AccessType::Nothing)), + allocation: None, + device: self.device.clone(), + handle, + image_view_cache: Mutex::new(image_view_cache), + info, + name: self.name.clone(), + } + } + + #[profiling::function] + pub(crate) fn view(&self, info: ImageViewInfo) -> Result { + self.with_image_view_cache(|cache| { + Ok(match cache.entry(info) { + Entry::Occupied(entry) => entry.get().image_view, + Entry::Vacant(entry) => { + entry + .insert(ImageView::create(&self.device, info, self.handle)?) + .image_view + } + }) }) } + + fn with_image_view_cache( + &self, + f: impl FnOnce(&mut HashMap) -> R, + ) -> R { + let cache = self.image_view_cache.lock(); + + #[cfg(not(feature = "parking_lot"))] + let cache = cache.expect("poisoned image view lock"); + + let mut cache = cache; + + f(&mut cache) + } } impl Debug for Image { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if let Some(name) = &self.name { - write!(f, "{} ({:?})", name, self.image) + write!(f, "{} ({:?})", name, self.handle) } else { - write!(f, "{:?}", self.image) + write!(f, "{:?}", self.handle) } } } -impl Deref for Image { - type Target = vk::Image; - - fn deref(&self) -> &Self::Target { - &self.image - } -} - impl Drop for Image { // This function is not profiled because drop_allocation is fn drop(&mut self) { @@ -439,6 +459,14 @@ impl Drop for Image { } } +impl Eq for Image {} + +impl PartialEq for Image { + fn eq(&self, other: &Self) -> bool { + self.handle == other.handle + } +} + #[derive(Debug)] pub(crate) struct ImageAccess { accesses: Box<[A]>, @@ -700,12 +728,18 @@ struct ImageAccessRange { derive(Copy, Clone, Debug), pattern = "owned" )] -#[non_exhaustive] pub struct ImageInfo { /// The number of layers in the image. #[builder(default = "1", setter(strip_option))] pub array_layer_count: u32, + /// Specifies a dedicated memory allocation managed by the Vulkan driver and not by the + /// internal memory allocation pool transient resources share. + /// + /// The driver may optimize access to dedicated buffers. + #[builder(default)] + pub dedicated: bool, + /// Image extent of the Z axis, when describing a three dimensional image. #[builder(setter(strip_option))] pub depth: u32, @@ -728,7 +762,7 @@ pub struct ImageInfo { /// Specifies the number of [samples per texel]. /// - /// [samples per texel]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#primsrast-multisampling + /// See [`VkImageCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkImageCreateInfo.html). #[builder(default = "SampleCount::Type1", setter(strip_option))] pub sample_count: SampleCount, @@ -825,6 +859,7 @@ impl ImageInfo { usage: vk::ImageUsageFlags, ) -> Self { Self { + dedicated: false, ty, width, height, @@ -839,8 +874,13 @@ impl ImageInfo { } } + /// Creates a default `ImageInfoBuilder`. + pub fn builder() -> ImageInfoBuilder { + Default::default() + } + /// Provides an `ImageViewInfo` for this format, type, aspect, array elements, and mip levels. - pub fn default_view_info(self) -> ImageViewInfo { + pub fn into_image_view(self) -> ImageViewInfo { self.into() } @@ -864,10 +904,10 @@ impl ImageInfo { } /// Converts an `ImageInfo` into an `ImageInfoBuilder`. - #[inline(always)] - pub fn to_builder(self) -> ImageInfoBuilder { + pub fn into_builder(self) -> ImageInfoBuilder { ImageInfoBuilder { array_layer_count: Some(self.array_layer_count), + dedicated: Some(self.dedicated), depth: Some(self.depth), flags: Some(self.flags), fmt: Some(self.fmt), @@ -880,6 +920,12 @@ impl ImageInfo { width: Some(self.width), } } + + #[deprecated = "use into_builder function"] + #[doc(hidden)] + pub fn to_builder(self) -> ImageInfoBuilder { + self.into_builder() + } } impl From for vk::ImageCreateInfo<'_> { @@ -909,6 +955,14 @@ impl From for ImageInfo { } } +impl From for vk::ImageSubresourceRange { + fn from(info: ImageInfo) -> Self { + let image_view_info: ImageViewInfo = info.into(); + + image_view_info.into() + } +} + impl ImageInfoBuilder { /// Builds a new `ImageInfo`. /// @@ -928,6 +982,11 @@ impl ImageInfoBuilder { Ok(info) => info, } } + + /// Provides an `ImageViewInfo` for this format, type, aspect, array elements, and mip levels. + pub fn into_image_view(self) -> ImageViewInfoBuilder { + self.build().into_image_view().into_builder() + } } #[derive(Debug)] @@ -939,32 +998,20 @@ impl From for ImageInfoBuilderError { } } -impl From for vk::ImageSubresourceRange { - fn from(info: ImageViewInfo) -> Self { - Self { - aspect_mask: info.aspect_mask, - base_mip_level: info.base_mip_level, - base_array_layer: info.base_array_layer, - layer_count: info.array_layer_count, - level_count: info.mip_level_count, - } - } -} - struct ImageView { - device: Arc, + device: Device, image_view: vk::ImageView, } impl ImageView { #[profiling::function] fn create( - device: &Arc, + device: &Device, info: impl Into, image: vk::Image, ) -> Result { let info = info.into(); - let device = Arc::clone(device); + let device = device.clone(); let create_info = vk::ImageViewCreateInfo::default() .view_type(info.ty) .format(info.fmt) @@ -985,7 +1032,7 @@ impl ImageView { let image_view = unsafe { device.create_image_view(&create_info, None) }.map_err(|err| { - warn!("{err}"); + warn!("unable to create image view: {err}"); DriverError::Unsupported })?; @@ -1014,7 +1061,6 @@ impl Drop for ImageView { derive(Clone, Copy, Debug), pattern = "owned" )] -#[non_exhaustive] pub struct ImageViewInfo { /// The number of layers that will be contained in the view. /// @@ -1066,8 +1112,7 @@ impl ImageViewInfo { } /// Converts a `ImageViewInfo` into a `ImageViewInfoBuilder`. - #[inline(always)] - pub fn to_builder(self) -> ImageViewInfoBuilder { + pub fn into_builder(self) -> ImageViewInfoBuilder { ImageViewInfoBuilder { array_layer_count: Some(self.array_layer_count), aspect_mask: Some(self.aspect_mask), @@ -1079,6 +1124,12 @@ impl ImageViewInfo { } } + #[deprecated = "use into_builder function"] + #[doc(hidden)] + pub fn to_builder(self) -> ImageViewInfoBuilder { + self.into_builder() + } + /// Takes this instance and returns it with a newly specified `ImageViewType`. pub fn with_type(mut self, ty: vk::ImageViewType) -> Self { self.ty = ty; @@ -1088,7 +1139,14 @@ impl ImageViewInfo { impl From for ImageViewInfo { fn from(info: ImageInfo) -> Self { - Self { + Self::from_image_info(info).expect("unsupported image type for image view info") + } +} + +impl ImageViewInfo { + /// Creates an image view description from image creation info. + pub fn from_image_info(info: ImageInfo) -> Result { + Ok(Self { array_layer_count: info.array_layer_count, aspect_mask: format_aspect_mask(info.fmt), base_array_layer: 0, @@ -1112,9 +1170,16 @@ impl From for ImageViewInfo { } (vk::ImageType::TYPE_2D, _) => vk::ImageViewType::TYPE_2D_ARRAY, (vk::ImageType::TYPE_3D, _) => vk::ImageViewType::TYPE_3D, - _ => unimplemented!(), + _ => { + warn!( + "invalid image view source info: image type {:?} with {} array layers", + info.ty, info.array_layer_count + ); + + return Err(DriverError::InvalidData); + } }, - } + }) } } @@ -1124,6 +1189,18 @@ impl From for ImageViewInfo { } } +impl From for vk::ImageSubresourceRange { + fn from(info: ImageViewInfo) -> Self { + Self { + aspect_mask: info.aspect_mask, + base_mip_level: info.base_mip_level, + base_array_layer: info.base_array_layer, + layer_count: info.array_layer_count, + level_count: info.mip_level_count, + } + } +} + impl ImageViewInfoBuilder { /// Builds a new 'ImageViewInfo'. /// @@ -1209,8 +1286,21 @@ impl From for vk::SampleCountFlags { } } +#[allow(unused)] +mod deprecated { + use crate::driver::image::{ImageInfo, ImageViewInfo}; + + impl ImageInfo { + #[deprecated = "use into_image_view function"] + #[doc(hidden)] + pub fn default_view_info(self) -> ImageViewInfo { + self.into_image_view() + } + } +} + #[cfg(test)] -mod tests { +mod test { use {super::*, std::ops::Range}; // ImageSubresourceRange does not implement PartialEq @@ -2035,7 +2125,7 @@ mod tests { #[test] pub fn image_info_cube() { let info = ImageInfo::cube(42, vk::Format::R32_SFLOAT, vk::ImageUsageFlags::empty()); - let builder = info.to_builder().build(); + let builder = info.into_builder().build(); assert_eq!(info, builder); } @@ -2059,7 +2149,7 @@ mod tests { #[test] pub fn image_info_image_1d() { let info = ImageInfo::image_1d(42, vk::Format::R32_SFLOAT, vk::ImageUsageFlags::empty()); - let builder = info.to_builder().build(); + let builder = info.into_builder().build(); assert_eq!(info, builder); } @@ -2082,7 +2172,7 @@ mod tests { pub fn image_info_image_2d() { let info = ImageInfo::image_2d(42, 84, vk::Format::R32_SFLOAT, vk::ImageUsageFlags::empty()); - let builder = info.to_builder().build(); + let builder = info.into_builder().build(); assert_eq!(info, builder); } @@ -2111,7 +2201,7 @@ mod tests { vk::Format::default(), vk::ImageUsageFlags::empty(), ); - let builder = info.to_builder().build(); + let builder = info.into_builder().build(); assert_eq!(info, builder); } @@ -2146,7 +2236,7 @@ mod tests { vk::Format::R32_SFLOAT, vk::ImageUsageFlags::empty(), ); - let builder = info.to_builder().build(); + let builder = info.into_builder().build(); assert_eq!(info, builder); } @@ -2219,7 +2309,7 @@ mod tests { mip_level_count: u32, ) -> ImageInfo { ImageInfo::image_2d(1, 1, fmt, vk::ImageUsageFlags::empty()) - .to_builder() + .into_builder() .array_layer_count(array_layer_count) .mip_level_count(mip_level_count) .build() @@ -2293,7 +2383,7 @@ mod tests { #[test] pub fn image_view_info() { let info = ImageViewInfo::new(vk::Format::default(), vk::ImageViewType::TYPE_1D); - let builder = info.to_builder().build(); + let builder = info.into_builder().build(); assert_eq!(info, builder); } diff --git a/src/driver/instance.rs b/src/driver/instance.rs index 7d989e76..5bd9ec8e 100644 --- a/src/driver/instance.rs +++ b/src/driver/instance.rs @@ -1,12 +1,17 @@ +//! Vulkan initialization types + use { super::{DriverError, physical_device::PhysicalDevice}, - ash::{Entry, ext, vk}, + ash::{ext, khr, vk, vk::Handle}, + derive_builder::{Builder, UninitializedFieldError}, log::{debug, error, trace, warn}, + raw_window_handle::{HasDisplayHandle, RawDisplayHandle}, std::{ - ffi::{CStr, CString}, - fmt::{Debug, Formatter}, + error::Error, + ffi::CStr, + fmt::{Debug, Display, Formatter}, ops::Deref, - os::raw::c_char, + sync::Arc, thread::panicking, }, }; @@ -22,91 +27,254 @@ use { }, }; +#[cfg(any(not(target_os = "macos"), feature = "loaded"))] +const SKIP_VALIDATION_PARK_ENV: &str = "VK_GRAPH_SKIP_VALIDATION_PARK"; + #[cfg(target_os = "macos")] use std::env::set_var; #[cfg(any(not(target_os = "macos"), feature = "loaded"))] -unsafe extern "system" fn vulkan_debug_callback( - _flags: vk::DebugReportFlagsEXT, - _obj_type: vk::DebugReportObjectTypeEXT, - _src_obj: u64, - _location: usize, - _msg_code: i32, - _layer_prefix: *const c_char, - message: *const c_char, +unsafe extern "system" fn debug_callback( + message_severity: vk::DebugUtilsMessageSeverityFlagsEXT, + _message_types: vk::DebugUtilsMessageTypeFlagsEXT, + callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>, _user_data: *mut c_void, -) -> u32 { +) -> vk::Bool32 { if panicking() { return vk::FALSE; } - assert!(!message.is_null()); + assert!(!callback_data.is_null()); - let mut found_null = false; - for i in 0..u16::MAX as _ { - if unsafe { *message.add(i) } == 0 { - found_null = true; - break; + let callback_data = unsafe { &*callback_data }; + let message = if callback_data.p_message.is_null() { + "" + } else { + unsafe { CStr::from_ptr(callback_data.p_message) } + .to_str() + .unwrap_or("") + }; + + if !callback_data.p_message_id_name.is_null() { + let vuid = unsafe { CStr::from_ptr(callback_data.p_message_id_name) } + .to_str() + .unwrap_or(""); + if vuid != "Loader Message" { + debug!("{vuid}"); } + }; + + let is_error = message_severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::ERROR); + + // HACK: This is not production-quality + // TODO: This was debugged and the issue has not been found, so this may or may not be valid + // The validation layer reports `UNASSIGNED-Threading-MultipleThreads-Write` when two threads + // touch different VkQueue handles at the same time. Ignoring until the issue is found. + if is_error + && message.contains("THREADING ERROR") + && message.contains("VkQueue is simultaneously used") + { + info!("ignoring: {message}"); + + return vk::FALSE; } - assert!(found_null); + if is_error { + error!("{message}"); + } else if message_severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::WARNING) { + warn!("{message}"); + } else if message_severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::INFO) { + info!("{message}"); + } else if message_severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE) { + debug!("{message}"); + } - let message = unsafe { CStr::from_ptr(message) }.to_str().unwrap(); + if !is_error { + return vk::FALSE; + } - if message.starts_with("Validation Warning: [ UNASSIGNED-BestPractices-pipeline-stage-flags ]") + if !logger().enabled(&Metadata::builder().level(Level::Debug).build()) + || var("RUST_LOG") + .map(|rust_log| rust_log.is_empty()) + .unwrap_or(true) { - // vk_sync uses vk::PipelineStageFlags::ALL_COMMANDS with AccessType::NOTHING and others - warn!("{}", message); - } else { - let prefix = "Validation Error: [ "; + eprintln!( + "note: run with `RUST_LOG=trace` environment variable to display more information" + ); + eprintln!("note: see https://github.com/rust-lang/log#in-executables"); + abort() + } - let (vuid, message) = if message.starts_with(prefix) { - let (vuid, message) = message - .trim_start_matches(prefix) - .split_once(" ]") - .unwrap_or_default(); - let message = message.split(" | ").nth(2).unwrap_or(message); + if current().name() != Some("main") { + warn!("invalid validation callback thread: child thread") + } - (Some(vuid.trim()), message) - } else { - (None, message) - }; + if var(SKIP_VALIDATION_PARK_ENV) + .map(|value| !matches!(value.as_str(), "" | "0" | "false" | "False" | "FALSE")) + .unwrap_or(false) + { + warn!("validation callback park skipped; execution will continue"); + logger().flush(); - if let Some(vuid) = vuid { - info!("{vuid}"); - } + return vk::FALSE; + } - error!("🆘 {message}"); + debug!( + "parking validation callback thread `{}` for debugger attach to pid {}", + current().name().unwrap_or_default(), + id() + ); - if !logger().enabled(&Metadata::builder().level(Level::Debug).build()) - || var("RUST_LOG") - .map(|rust_log| rust_log.is_empty()) - .unwrap_or(true) - { - eprintln!( - "note: run with `RUST_LOG=trace` environment variable to display more information" - ); - eprintln!("note: see https://github.com/rust-lang/log#in-executables"); - abort() + logger().flush(); + park(); + + vk::FALSE +} + +#[cfg(any(not(target_os = "macos"), feature = "loaded"))] +fn debug_extension_names() -> &'static [&'static CStr] { + &[ext::debug_utils::NAME] +} + +#[cfg(all(target_os = "macos", not(feature = "loaded")))] +fn debug_extension_names() -> &'static [&'static CStr] { + &[] +} + +#[cfg(any(not(target_os = "macos"), feature = "loaded"))] +fn debug_layer_names() -> &'static [&'static CStr] { + &[c"VK_LAYER_KHRONOS_validation"] +} + +#[cfg(all(target_os = "macos", not(feature = "loaded")))] +fn debug_layer_names() -> &'static [&'static CStr] { + &[] +} + +// Copied from ash_window::enumerate_required_extensions to change the signature. +fn display_extension_names( + display_handle: RawDisplayHandle, +) -> Result<&'static [&'static CStr], DriverError> { + let extensions = match display_handle { + RawDisplayHandle::Windows(_) => &[khr::surface::NAME, khr::win32_surface::NAME], + RawDisplayHandle::Wayland(_) => &[khr::surface::NAME, khr::wayland_surface::NAME], + RawDisplayHandle::Xlib(_) => &[khr::surface::NAME, khr::xlib_surface::NAME], + RawDisplayHandle::Xcb(_) => &[khr::surface::NAME, khr::xcb_surface::NAME], + RawDisplayHandle::Android(_) => &[khr::surface::NAME, khr::android_surface::NAME], + RawDisplayHandle::AppKit(_) | RawDisplayHandle::UiKit(_) => { + &[khr::surface::NAME, ext::metal_surface::NAME] } + _ => { + warn!("unsupported display handle type: {display_handle:?}"); - if current().name() != Some("main") { - warn!("executing on a child thread!") + return Err(DriverError::Unsupported); } + }; - debug!( - "🛑 PARKING THREAD `{}` -> attach debugger to pid {}!", - current().name().unwrap_or_default(), - id() - ); + Ok(extensions) +} - logger().flush(); +// Estimates surface extension support. +// +// Imported instances do not expose their enabled extension list, so we infer support by +// checking that the VK_KHR_surface entry points resolve for this instance handle. +fn has_surface_ext(entry: &ash::Entry, instance: vk::Instance) -> bool { + [ + c"vkGetPhysicalDeviceSurfaceCapabilitiesKHR", + c"vkGetPhysicalDeviceSurfaceFormatsKHR", + c"vkGetPhysicalDeviceSurfacePresentModesKHR", + c"vkGetPhysicalDeviceSurfaceSupportKHR", + c"vkDestroySurfaceKHR", + ] + .into_iter() + .all(|name| unsafe { + entry + .get_instance_proc_addr(instance, name.as_ptr()) + .is_some() + }) +} + +/// Vulkan API version. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)] +pub enum ApiVersion { + /// Version `1.2`. + #[default] + Vulkan12, - park(); + /// Version `1.3`. + Vulkan13, +} + +impl ApiVersion { + /// Returns a version parsed from a native Vulkan value. + pub fn try_parse_vk_api_version(version: u32) -> Result { + Self::try_from(version) } - vk::FALSE + /// Vulkan API major version number component. Ex: `vX.0.0-0`. + /// + /// Always one. + pub fn major(self) -> u32 { + 1 + } + + /// Vulkan API minor version number component. Ex: `v0.X.0-0`. + pub fn minor(self) -> u32 { + match self { + Self::Vulkan12 => 2, + Self::Vulkan13 => 3, + } + } + + /// Vulkan API minor version number component. Ex: `v0.0.X-0`. + /// + /// Always zero. + pub fn patch(self) -> u32 { + 0 + } + + /// Returns a native Vulkan value. + pub fn to_vk_api_version(self) -> u32 { + self.into() + } + + /// Vulkan API variant version number component. Ex: `v0.0.0-X`. + /// + /// Always zero. + pub fn variant(self) -> u32 { + 0 + } +} + +impl From for u32 { + fn from(val: ApiVersion) -> Self { + vk::make_api_version(val.variant(), val.major(), val.minor(), val.patch()) + } +} + +impl TryFrom for ApiVersion { + type Error = ParseApiVersionError; + + fn try_from(val: u32) -> Result { + let major = vk::api_version_major(val); + let minor = vk::api_version_minor(val); + let patch = vk::api_version_patch(val); + let variant = vk::api_version_variant(val); + + if variant != 0 || major != 1 || minor < 2 { + return Err(ParseApiVersionError { + major, + minor, + patch, + variant, + }); + } + + Ok(match minor { + 2 => ApiVersion::Vulkan12, + _ => ApiVersion::Vulkan13, + }) + } } /// There is no global state in Vulkan and all per-application state is stored in a VkInstance @@ -114,21 +282,60 @@ unsafe extern "system" fn vulkan_debug_callback( /// /// Creating an Instance initializes the Vulkan library and allows the application to pass /// information about itself to the implementation. +#[read_only::embed] +#[allow(private_interfaces)] pub struct Instance { - _debug_callback: Option, - #[allow(deprecated)] // TODO: Remove? Look into this.... - _debug_loader: Option, - debug_utils: Option, - entry: Entry, - instance: ash::Instance, + /// Information used to create this resource. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub info: InstanceInfo, + + #[readonly] + pub(self) inner: Arc, + + /// True if `VK_KHR_surface` is enabled on this instance. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub surface_ext: bool, +} + +impl Clone for Instance { + fn clone(&self) -> Self { + Self { + read_only: ReadOnlyInstance { + info: self.info, + surface_ext: self.surface_ext, + inner: self.inner.clone(), + }, + } + } } impl Instance { + /// The most recent supported version of Vulkan. + pub const LATEST_API_VERSION: ApiVersion = ApiVersion::Vulkan13; + + #[deprecated = "use create"] + #[doc(hidden)] + pub fn new(info: impl Into) -> Result { + Self::create(info) + } + /// Creates a new Vulkan instance. + /// + /// This constructor is intended for headless or manually managed setups. It does not infer or + /// enable display platform surface extensions. Use [`Self::try_from_display`] when the + /// resulting instance must be capable of later surface creation. #[profiling::function] - pub fn create<'a>( - debug: bool, - required_extensions: impl Iterator, + pub fn create(info: impl Into) -> Result { + Self::create_with_extension_names(info.into(), &[]) + } + + fn create_with_extension_names( + info: InstanceInfo, + extra_extension_names: &[&CStr], ) -> Result { // Required to enable non-uniform descriptor indexing (bindless) #[cfg(target_os = "macos")] @@ -136,217 +343,320 @@ impl Instance { set_var("MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS", "1"); } - // Link molten-vk dynamically if not on MacOS, or if explicitly requested. - #[cfg(any(not(target_os = "macos"), feature = "loaded"))] + // Link the Vulkan loader dynamically (default feature). + #[cfg(feature = "loaded")] let entry = unsafe { - Entry::load().map_err(|err| { - error!("Vulkan driver not found: {err}"); + ash::Entry::load().map_err(|err| { + error!("unable to load Vulkan driver: {err}"); DriverError::Unsupported })? }; - // On MacOS, by default link molten-vk statically using ash-molten. - #[cfg(all(target_os = "macos", not(feature = "loaded")))] - let entry = ash_molten::load(); - #[allow(unused_mut)] - let mut required_extensions = required_extensions.collect::>(); + // Link the Vulkan loader statically if explicitly requested + #[cfg(not(feature = "loaded"))] + let entry = { + #[cfg(not(target_os = "macos"))] + let entry = ash::Entry::linked(); + + // On MacOS, by default link molten-vk statically using ash-molten. + #[cfg(target_os = "macos")] + let entry = ash_molten::load(); + }; + + let mut extension_names = info + .extension_names + .iter() + .chain(extra_extension_names) + .copied() + .collect::>(); + + if info.debug { + extension_names.extend(debug_extension_names()); + } // If linking dynamically on MacOS, we require a few additional extensions. // Based on "Encountered VK_ERROR_INCOMPATIBLE_DRIVER" section in: // https://vulkan.lunarg.com/doc/view/latest/mac/getting_started.html #[cfg(all(target_os = "macos", feature = "loaded"))] { - required_extensions.push(ash::khr::get_physical_device_properties2::NAME); - required_extensions.push(ash::khr::portability_enumeration::NAME); + extension_names.extend(&[ + ash::khr::get_physical_device_properties2::NAME, + ash::khr::portability_enumeration::NAME, + ]); } - let instance_extensions = required_extensions + let surface_ext = extension_names.contains(&khr::surface::NAME); + + let extension_name_ptrs = extension_names .iter() - .map(|ext| ext.as_ptr()) - .chain(unsafe { Self::extension_names(debug).into_iter() }) - .collect::>(); - let layer_names = Self::layer_names(debug); - let layer_names: Vec<*const c_char> = layer_names + .copied() + .map(CStr::as_ptr) + .collect::>(); + + let mut layer_names = Vec::with_capacity(info.debug as _); + + if info.debug { + layer_names.extend(debug_layer_names()); + } + + let layer_name_ptrs = layer_names .iter() - .map(|raw_name| raw_name.as_ptr()) - .collect(); - let app_desc = vk::ApplicationInfo::default().api_version(vk::API_VERSION_1_2); + .copied() + .map(CStr::as_ptr) + .collect::>(); + + let app_desc = + vk::ApplicationInfo::default().api_version(info.api_version.to_vk_api_version()); let instance_desc = vk::InstanceCreateInfo::default() .application_info(&app_desc) - .enabled_layer_names(&layer_names) - .enabled_extension_names(&instance_extensions); + .enabled_layer_names(&layer_name_ptrs) + .enabled_extension_names(&extension_name_ptrs); - // Molten-vk doesn't support the full Vulkan feature set, hence the portability flag needs to be set. + // Molten-vk doesn't support the full Vulkan feature set, hence the portability flag needs + // to be set. #[cfg(all(target_os = "macos", feature = "loaded"))] let instance_desc = instance_desc.flags(vk::InstanceCreateFlags::ENUMERATE_PORTABILITY_KHR); + #[cfg(any(not(target_os = "macos"), feature = "loaded"))] + let mut debug_create_info = vk::DebugUtilsMessengerCreateInfoEXT::default() + .message_severity( + vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE + | vk::DebugUtilsMessageSeverityFlagsEXT::INFO + | vk::DebugUtilsMessageSeverityFlagsEXT::WARNING + | vk::DebugUtilsMessageSeverityFlagsEXT::ERROR, + ) + .message_type( + vk::DebugUtilsMessageTypeFlagsEXT::GENERAL + | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION + | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE, + ) + .pfn_user_callback(Some(debug_callback)); + + #[cfg(any(not(target_os = "macos"), feature = "loaded"))] + let instance_desc = if info.debug { + instance_desc.push_next(&mut debug_create_info) + } else { + instance_desc + }; + let instance = unsafe { - entry - .create_instance(&instance_desc, None) - .map_err(|vkerr| { - if debug { - warn!("debug may only be enabled with a valid Vulkan SDK installation"); - } - error!("Vulkan Error: {}", vkerr); - error!("Vulkan driver does not support API v1.2"); + entry.create_instance(&instance_desc, None).map_err(|_| { + if info.debug { + warn!("debug may only be enabled with a valid Vulkan SDK installation"); + } - for layer_name in Self::layer_names(debug) { - debug!("Layer: {:?}", layer_name); + error!( + "Vulkan driver does not support API v{}", + match info.api_version { + ApiVersion::Vulkan12 => "1.2", + ApiVersion::Vulkan13 => "1.3", } + ); - for extension_name in required_extensions { - debug!("Extension: {:?}", extension_name); - } + for layer_name in &layer_names { + debug!("Layer: {:?}", layer_name); + } - DriverError::Unsupported - })? + for extension_name in extension_names { + debug!("Extension: {:?}", extension_name); + } + + DriverError::Unsupported + })? }; trace!("created a Vulkan instance"); #[cfg(all(target_os = "macos", not(feature = "loaded")))] - let (debug_loader, debug_callback, debug_utils) = (None, None, None); + let debug_utils = None; #[cfg(any(not(target_os = "macos"), feature = "loaded"))] - let (debug_loader, debug_callback, debug_utils) = if debug { - let debug_info = vk::DebugReportCallbackCreateInfoEXT { - flags: vk::DebugReportFlagsEXT::ERROR - | vk::DebugReportFlagsEXT::WARNING - | vk::DebugReportFlagsEXT::PERFORMANCE_WARNING, - pfn_callback: Some(vulkan_debug_callback), - ..Default::default() - }; - - #[allow(deprecated)] - let debug_loader = ext::debug_report::Instance::new(&entry, &instance); - - let debug_callback = unsafe { - #[allow(deprecated)] - debug_loader - .create_debug_report_callback(&debug_info, None) - .unwrap() - }; - + let debug_utils = if info.debug { let debug_utils = ext::debug_utils::Instance::new(&entry, &instance); + let debug_messenger = + unsafe { debug_utils.create_debug_utils_messenger(&debug_create_info, None) } + .map_err(|err| { + unsafe { + instance.destroy_instance(None); + } - (Some(debug_loader), Some(debug_callback), Some(debug_utils)) - } else { - (None, None, None) - }; + error!("unable to create debug utils messenger: {err}"); - Ok(Self { - _debug_callback: debug_callback, - _debug_loader: debug_loader, - debug_utils, - entry, - instance, - }) - } + DriverError::Unsupported + })?; - /// Loads an existing Vulkan instance that may have been created by other means. - /// - /// This is useful when you want to use a Vulkan instance created by some other library, such - /// as OpenXR. - #[profiling::function] - pub fn load(entry: Entry, instance: vk::Instance) -> Result { - if instance == vk::Instance::null() { - return Err(DriverError::InvalidData); - } - - let instance = unsafe { ash::Instance::load(entry.static_fn(), instance) }; + Some((debug_utils, debug_messenger)) + } else { + None + }; Ok(Self { - _debug_callback: None, - _debug_loader: None, - debug_utils: None, - entry, - instance, + read_only: ReadOnlyInstance { + info, + inner: Arc::new(InstanceInner { + debug_utils, + entry, + instance, + instance_created: true, + }), + surface_ext, + }, }) } - /// Returns the `ash` entrypoint for Vulkan functions. - pub fn entry(this: &Self) -> &Entry { - &this.entry + /// The ash entrypoint used to load Vulkan instance functions. + pub fn entry(this: &Self) -> &ash::Entry { + &this.inner.entry } - unsafe fn extension_names( - #[cfg_attr(target_os = "macos", allow(unused_variables))] debug: bool, - ) -> Vec<*const c_char> { - if cfg!(all( - target_os = "macos", - not(feature = "loaded") - )) { - vec![] - } else { - let mut res = vec![]; - if debug { - #[allow(deprecated)] - res.push(ext::debug_report::NAME.as_ptr()); - res.push(ext::debug_utils::NAME.as_ptr()); - } - res - } + #[deprecated = "use try_from_entry"] + #[doc(hidden)] + pub fn from_entry(entry: ash::Entry, instance: vk::Instance) -> Result { + Self::try_from_entry(entry, instance) } - /// Returns `true` if this instance was created with debug layers enabled. - pub fn is_debug(this: &Self) -> bool { - this.debug_utils.is_some() - } + /// Returns a wrapper structure for a physical device of this instance. + #[profiling::function] + pub fn physical_device( + this: &Self, + physical_device: vk::PhysicalDevice, + ) -> Result { + let physical_device = PhysicalDevice::new(this.clone(), physical_device)?; + if let Err(err) = + ApiVersion::try_parse_vk_api_version(physical_device.properties_v1_0.api_version) + { + warn!( + "unsupported physical device `{}`: {err}", + physical_device.properties_v1_0.device_name + ); - fn layer_names( - #[cfg_attr(target_os = "macos", allow(unused_variables))] debug: bool, - ) -> Vec { - if cfg!(all( - target_os = "macos", - not(feature = "loaded") - )) { - vec![] - } else { - let mut res = vec![]; - if debug { - res.push(CString::new("VK_LAYER_KHRONOS_validation").unwrap()); - } - res + return Err(DriverError::Unsupported); } + + Ok(physical_device) } /// Returns the available physical devices of this instance. #[profiling::function] - pub fn physical_devices(this: &Self) -> Result, DriverError> { - let physical_devices = unsafe { this.enumerate_physical_devices() }; + pub fn physical_devices( + this: &Self, + ) -> Result, DriverError> { + let physical_devices = unsafe { this.enumerate_physical_devices() }.map_err(|err| { + error!("unable to enumerate physical devices: {err}"); + + match err { + vk::Result::ERROR_INITIALIZATION_FAILED => DriverError::Unsupported, + vk::Result::ERROR_OUT_OF_DEVICE_MEMORY | vk::Result::ERROR_OUT_OF_HOST_MEMORY => { + DriverError::OutOfMemory + } + vk::Result::ERROR_VALIDATION_FAILED_EXT => DriverError::InvalidData, + _ => { + warn!("unexpected enumerate_physical_devices error: {err}"); - Ok(physical_devices - .map_err(|err| { - error!("unable to enumerate physical devices: {err}"); + DriverError::Unsupported + } + } + })?; - DriverError::Unsupported - })? + Ok(physical_devices .into_iter() .enumerate() .filter_map(|(idx, physical_device)| { - let res = PhysicalDevice::new(this, physical_device); + let res = PhysicalDevice::new(this.clone(), physical_device); if let Err(err) = &res { - warn!("unable to create physical device at index {idx}: {err}"); + warn!("unsupported physical device #{idx}: {err}"); } res.ok().filter(|physical_device| { - let major = vk::api_version_major(physical_device.properties_v1_0.api_version); - let minor = vk::api_version_minor(physical_device.properties_v1_0.api_version); - let supports_vulkan_1_2 = major > 1 || (major == 1 && minor >= 2); - - if !supports_vulkan_1_2 { - warn!( - "physical device `{}` does not support Vulkan v1.2", + ApiVersion::try_parse_vk_api_version( + physical_device.properties_v1_0.api_version, + ) + .inspect_err(|err| { + debug!( + "unsupported physical device `{}`: {err}", physical_device.properties_v1_0.device_name ); - } - - supports_vulkan_1_2 + }) + .is_ok() }) + })) + } + + /// Creates a new Vulkan instance with the platform surface extensions required by the provided + /// display handle. + #[profiling::function] + pub fn try_from_display( + display: impl HasDisplayHandle, + info: impl Into, + ) -> Result { + let display_handle = display.display_handle().map_err(|err| { + warn!("unable to get display handle: {err}"); + + DriverError::Unsupported + })?; + let display_extension_names = + display_extension_names(display_handle.as_raw()).map_err(|err| { + warn!("unable to enumerate display extensions: {err}"); + + DriverError::Unsupported + })?; + + Self::create_with_extension_names(info.into(), display_extension_names) + } + + /// Loads an existing Vulkan instance that may have been created by other means. + /// + /// This is useful when you want to use a Vulkan instance created by some other library, such + /// as OpenXR. + #[profiling::function] + pub fn try_from_entry(entry: ash::Entry, instance: vk::Instance) -> Result { + if instance == vk::Instance::null() { + warn!("invalid VkInstance handle: null"); + + return Err(DriverError::InvalidData); + } + + let api_version = unsafe { entry.try_enumerate_instance_version() } + .map_err(|err| match err { + vk::Result::ERROR_OUT_OF_HOST_MEMORY => DriverError::OutOfMemory, + vk::Result::ERROR_VALIDATION_FAILED_EXT => DriverError::InvalidData, + err => { + error!("unable to enumerate instance version: {err}"); + + DriverError::Unsupported + } + })? + .unwrap_or_else(|| { + // The implementation *should* provide a version. If it does not we just send it. + Self::LATEST_API_VERSION.to_vk_api_version() }) - .collect()) + .try_into() + .map_err(|err| { + warn!("unsupported instance: {err}"); + + DriverError::Unsupported + })?; + let surface_ext = has_surface_ext(&entry, instance); + + let instance = unsafe { ash::Instance::load(entry.static_fn(), instance) }; + + Ok(Self { + read_only: ReadOnlyInstance { + info: InstanceInfo { + api_version, + ..Default::default() + }, + inner: Arc::new(InstanceInner { + debug_utils: None, + entry, + instance, + instance_created: false, + }), + surface_ext, + }, + }) } } @@ -356,15 +666,86 @@ impl Debug for Instance { } } -impl Deref for Instance { - type Target = ash::Instance; +/// Information used to create an [`Instance`] instance. +#[derive(Builder, Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +#[builder( + build_fn(private, name = "fallible_build", error = "UninitializedFieldError"), + derive(Clone, Copy, Debug), + pattern = "owned" +)] +pub struct InstanceInfo { + /// The Vulkan API version to target. + #[builder(default = "ApiVersion::Vulkan13")] + pub api_version: ApiVersion, + + /// Enables Vulkan validation layers. + /// + /// This requires a Vulkan SDK installation and will cause validation errors to introduce + /// panics as they happen. + /// + /// Set `VK_GRAPH_SKIP_VALIDATION_PARK=1` to keep logging validation errors without parking the + /// callback thread for debugger attach. + /// + /// _NOTE:_ Consider turning OFF debug if you discover an unknown issue. Often the validation + /// layers will throw an error before other layers can provide additional context such as the + /// API dump info or other messages. You might find the "actual" issue is detailed in those + /// subsequent details. + /// + /// ## Platform-specific + /// + /// **macOS:** Has no effect unless the `loaded` feature is enabled. + #[builder(default)] + pub debug: bool, - fn deref(&self) -> &Self::Target { - &self.instance + /// Required Vulkan instance extension names to load. + #[builder(default)] + pub extension_names: &'static [&'static CStr], +} + +impl InstanceInfo { + /// Creates a default `InstanceInfoBuilder`. + pub fn builder() -> InstanceInfoBuilder { + Default::default() + } + + /// Converts a `InstanceInfo` into a `InstanceInfoBuilder`. + pub fn into_builder(self) -> InstanceInfoBuilder { + InstanceInfoBuilder { + api_version: Some(self.api_version), + debug: Some(self.debug), + extension_names: Some(self.extension_names), + } + } + + #[deprecated = "use into_builder function"] + #[doc(hidden)] + pub fn to_builder(self) -> InstanceInfoBuilder { + self.into_builder() } } -impl Drop for Instance { +impl InstanceInfoBuilder { + /// Builds a new `InstanceInfo`. + #[inline(always)] + pub fn build(self) -> InstanceInfo { + self.fallible_build().expect("invalid instance info") + } +} + +impl From for InstanceInfo { + fn from(info: InstanceInfoBuilder) -> Self { + info.build() + } +} + +struct InstanceInner { + debug_utils: Option<(ext::debug_utils::Instance, vk::DebugUtilsMessengerEXT)>, + entry: ash::Entry, + instance: ash::Instance, + instance_created: bool, +} + +impl Drop for InstanceInner { #[profiling::function] fn drop(&mut self) { if panicking() { @@ -372,13 +753,89 @@ impl Drop for Instance { } unsafe { - #[allow(deprecated)] - if let Some(debug_loader) = &self._debug_loader { - let debug_callback = self._debug_callback.unwrap(); - debug_loader.destroy_debug_report_callback(debug_callback, None); + if let Some((debug_utils, debug_messenger)) = self.debug_utils.take() { + trace!("destroy debug_utils_messenger {}", debug_messenger.as_raw()); + debug_utils.destroy_debug_utils_messenger(debug_messenger, None); + trace!( + "destroy debug_utils_messenger {} DONE", + debug_messenger.as_raw() + ); } - self.instance.destroy_instance(None); + if self.instance_created { + trace!("destroy instance {}", self.instance.handle().as_raw()); + self.instance.destroy_instance(None); + self.instance_created = false; + } } } } + +/// Data returned when attempting to parse a Vulkan API version number. +#[derive(Clone, Copy, Debug)] +pub struct ParseApiVersionError { + /// The _major_ version indicates a significant change in the API, which will encompass a + /// wholly new version of the specification. + pub major: u32, + + /// The _minor_ version indicates the incorporation of new functionality into the core + /// specification. + pub minor: u32, + + /// The _patch_ version indicates bug fixes, clarifications, and language improvements have + /// been incorporated into the specification. + pub patch: u32, + + /// The _variant_ indicates the variant of the Vulkan API supported by the implementation. This + /// is always 0 for the Vulkan API. + pub variant: u32, +} + +impl Display for ParseApiVersionError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "v{}.{}.{}-{}", + self.major, self.minor, self.patch, self.variant + )) + } +} + +impl Error for ParseApiVersionError {} + +#[doc(hidden)] +impl Deref for ReadOnlyInstance { + type Target = ash::Instance; + + fn deref(&self) -> &Self::Target { + &self.inner.instance + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + pub fn api_versions_match() { + assert_eq!( + ApiVersion::Vulkan12.to_vk_api_version(), + vk::API_VERSION_1_2 + ); + assert_eq!( + ApiVersion::Vulkan13.to_vk_api_version(), + vk::API_VERSION_1_3 + ); + } + + #[test] + pub fn api_versions_from() { + assert_eq!( + ApiVersion::try_parse_vk_api_version(vk::API_VERSION_1_2).unwrap(), + ApiVersion::Vulkan12 + ); + assert_eq!( + ApiVersion::try_parse_vk_api_version(vk::API_VERSION_1_3).unwrap(), + ApiVersion::Vulkan13 + ); + } +} diff --git a/src/driver/mod.rs b/src/driver/mod.rs index 6a011fb7..1c853f82 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -1,11 +1,8 @@ -//! [Vulkan 1.2](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/index.html) interface -//! based on smart pointers. +//! Vulkan interface based on smart pointers. //! //! # Resources //! -//! Each resource contains an opaque Vulkan object handle and an information structure which -//! describes the object. Resources also contain an atomic [`AccessType`] state value which is used to -//! maintain consistency in any system which accesses the resource. +//! Resources are created and destroyed using RAII-style wrapper structures. //! //! The following resources are available: //! @@ -13,25 +10,47 @@ //! - [`Buffer`] //! - [`Image`](image::Image) //! -//! # Pipelines +//! Resources are logically mutable. All resource types contain useful read-only public fields, for +//! example: +//! +//! [`Buffer`] Field|`->` +//! -|- +//! [`device`](Buffer::device)|[`Device`](device::Device) +//! [`handle`](Buffer::handle)|[`vk::Buffer`] +//! [`info`](Buffer::info)|[`BufferInfo`] //! -//! Pipelines allow you to run shader code which read and write resources using graphics hardware. +//! Resources use atomic [`AccessType`](sync::AccessType) values to maintain consistency and track +//! changes. //! -//! Each pipeline contains an opaque Vulkan object handle and an information structure which -//! describes the configuration and shaders. They are immutable once created. +//! # Pipelines +//! +//! Pipelines enable reading and writing resources using shader code running on physical graphics +//! hardware. //! //! The following pipelines are available: //! //! - [`ComputePipeline`](compute::ComputePipeline) -//! - [`GraphicPipeline`] +//! - [`GraphicPipeline`](graphic::GraphicPipeline) //! - [`RayTracePipeline`](ray_trace::RayTracePipeline) +//! +//! Pipelines are immutable. All pipeline types contain useful public methods, for +//! example: +//! +//! [`ComputePipeline`](compute::ComputePipeline) Method | `->` +//! -|- +//! [`device(&self)`](compute::ComputePipeline::device)|[`Device`](device::Device) +//! [`handle(&self)`](compute::ComputePipeline::handle)|[`vk::Pipeline`] +//! [`info(&self)`](compute::ComputePipeline::info) +//! | [`ComputePipelineInfo`](compute::ComputePipelineInfo) pub mod accel_struct; pub mod buffer; +pub mod cmd_buf; pub mod compute; pub mod device; pub mod graphic; pub mod image; +pub mod instance; pub mod physical_device; pub mod ray_trace; pub mod render_pass; @@ -39,28 +58,26 @@ pub mod shader; pub mod surface; pub mod swapchain; -mod cmd_buf; -mod descriptor_set; +#[doc(hidden)] +pub mod descriptor_set; + mod descriptor_set_layout; -mod instance; pub use { - self::{cmd_buf::CommandBuffer, instance::Instance}, ash::{self}, - vk_sync::AccessType, + vk_sync::{self as sync}, }; -/// Specifying depth and stencil resolve modes. #[deprecated = "Use driver::render_pass::ResolveMode instead"] +#[doc(hidden)] pub type ResolveMode = self::render_pass::ResolveMode; pub(crate) use self::{ - cmd_buf::CommandBufferInfo, - descriptor_set::{DescriptorPool, DescriptorPoolInfo, DescriptorSet}, + descriptor_set::DescriptorSet, descriptor_set_layout::DescriptorSetLayout, render_pass::{ - AttachmentInfo, AttachmentRef, FramebufferAttachmentImageInfo, FramebufferInfo, RenderPass, - RenderPassInfo, SubpassDependency, SubpassInfo, + AttachmentInfo, AttachmentRef, FramebufferAttachmentImageInfo, FramebufferInfo, + SubpassDependency, SubpassInfo, }, shader::{Descriptor, DescriptorBindingMap, DescriptorInfo}, surface::Surface, @@ -69,8 +86,7 @@ pub(crate) use self::{ use { self::{ buffer::{Buffer, BufferInfo}, - graphic::{DepthStencilMode, GraphicPipeline, VertexInputState}, - image::SampleCount, + graphic::VertexInputState, }, ash::vk, gpu_allocator::AllocationError, @@ -79,9 +95,13 @@ use { error::Error, fmt::{Display, Formatter}, }, - vk_sync::ImageLayout, }; +// When removing, fix all the extra-overly-qualiified references in this file +#[deprecated = "use from sync module"] +#[doc(hidden)] +pub type AccessType = self::sync::AccessType; + pub(super) const fn format_aspect_mask(fmt: vk::Format) -> vk::ImageAspectFlags { match fmt { vk::Format::D16_UNORM | vk::Format::D32_SFLOAT | vk::Format::X8_D24_UNORM_PACK32 => { @@ -97,10 +117,14 @@ pub(super) const fn format_aspect_mask(fmt: vk::Format) -> vk::ImageAspectFlags } } -/// Returns number of bytes used to store one texel block (a single addressable element of an uncompressed image, or a single compressed block of a compressed image) -/// See [Representation and Texel Block Size](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#texel-block-size) +/// Returns number of bytes used to store one texel block (a single addressable element of an +/// uncompressed image, or a single compressed block of a compressed image). +/// +/// See the [Texel Block Size](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#texel-block-size) +/// section of the Vulkan specification. pub const fn format_texel_block_size(fmt: vk::Format) -> u32 { match fmt { + vk::Format::UNDEFINED => 0, vk::Format::R4G4_UNORM_PACK8 | vk::Format::R8_UNORM | vk::Format::R8_SNORM @@ -349,18 +373,18 @@ pub const fn format_texel_block_size(fmt: vk::Format) -> u32 { vk::Format::G10X6_B10X6R10X6_2PLANE_444_UNORM_3PACK16 | vk::Format::G12X4_B12X4R12X4_2PLANE_444_UNORM_3PACK16 | vk::Format::G16_B16R16_2PLANE_444_UNORM => 6, - _ => { - // Remaining formats should be implemented in the future - unimplemented!() - } + _ => panic!("unsupported texel block size format"), } } /// Returns the extent of a block of texels for the given Vulkan format. /// Uncompressed formats typically have a block extent of `(1, 1)`. -/// See [Representation and Texel Block Size](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#texel-block-size) +/// +/// See the [Texel Block Size](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#texel-block-size) +/// section of the Vulkan specification. pub const fn format_texel_block_extent(vk_format: vk::Format) -> (u32, u32) { match vk_format { + vk::Format::UNDEFINED => (1, 1), vk::Format::R4G4_UNORM_PACK8 | vk::Format::R8_UNORM | vk::Format::R8_SNORM @@ -609,10 +633,7 @@ pub const fn format_texel_block_extent(vk_format: vk::Format) -> (u32, u32) { | vk::Format::PVRTC1_4BPP_SRGB_BLOCK_IMG | vk::Format::PVRTC2_4BPP_UNORM_BLOCK_IMG | vk::Format::PVRTC2_4BPP_SRGB_BLOCK_IMG => (4, 4), - _ => { - // Remaining formats should be implemented in the future - unimplemented!() - } + _ => panic!("unsupported texel block extent format"), } } @@ -633,24 +654,18 @@ pub(super) const fn image_subresource_range_from_layers( } } -pub(super) const fn image_access_layout(access: AccessType) -> ImageLayout { - if matches!(access, AccessType::Present | AccessType::ComputeShaderWrite) { - ImageLayout::General - } else { - ImageLayout::Optimal - } -} - -pub(super) const fn initial_image_layout_access(ty: AccessType) -> AccessType { - use AccessType::*; +pub(super) const fn initial_image_layout_access( + ty: self::sync::AccessType, +) -> self::sync::AccessType { + use self::sync::AccessType::*; match ty { DepthStencilAttachmentReadWrite => DepthStencilAttachmentRead, _ => ty, } } -pub(super) const fn is_read_access(ty: AccessType) -> bool { - use AccessType::*; +pub(super) const fn is_read_access(ty: self::sync::AccessType) -> bool { + use self::sync::AccessType::*; match ty { Nothing | CommandBufferWriteNVX @@ -723,8 +738,8 @@ pub(super) const fn is_read_access(ty: AccessType) -> bool { } } -pub(super) const fn is_write_access(ty: AccessType) -> bool { - use AccessType::*; +pub(super) const fn is_write_access(ty: self::sync::AccessType) -> bool { + use self::sync::AccessType::*; match ty { Nothing | CommandBufferReadNVX @@ -899,11 +914,11 @@ fn merge_push_constant_ranges(pcr: &[vk::PushConstantRange]) -> Vec (vk::PipelineStageFlags, vk::AccessFlags) { use { - AccessType as ty, vk::{AccessFlags as access, PipelineStageFlags as stage}, + vk_sync::AccessType as ty, }; match access_type { @@ -1100,14 +1115,14 @@ pub(super) const fn pipeline_stage_access_flags( /// Describes the general category of all graphics driver failure cases. /// -/// In the event of a failure you should follow the _Screen 13_ code to the responsible Vulkan API +/// In the event of a failure you should follow the _vk-graph_ code to the responsible Vulkan API /// and then to the `Ash` stub call; it will generally contain a link to the appropriate /// specification. The specifications provide a table of possible error conditions which can be a /// good starting point to debug the issue. /// -/// Feel free to open an issue on GitHub, [here](https://github.com/attackgoat/screen-13/issues) for +/// Feel free to open an issue on GitHub, [here](https://github.com/attackgoat/vk-graph/issues) for /// help debugging the issue. -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub enum DriverError { /// The input data, or referenced data, is not valid for the current state. InvalidData, @@ -1146,7 +1161,7 @@ impl Display for DriverError { impl Error for DriverError {} #[cfg(test)] -mod tests { +mod test { use {super::merge_push_constant_ranges, ash::vk}; macro_rules! assert_pcr_eq { diff --git a/src/driver/physical_device.rs b/src/driver/physical_device.rs index f8b09f3d..15bea15a 100644 --- a/src/driver/physical_device.rs +++ b/src/driver/physical_device.rs @@ -1,14 +1,15 @@ -//! Physical device resource types +//! Physical device types use { - super::{DriverError, Instance}, + super::{DriverError, instance::Instance}, + crate::driver::device::Device, ash::{ext, khr, vk}, - log::{debug, error}, + log::{debug, error, warn}, std::{ collections::HashSet, ffi::{CStr, c_char}, fmt::{Debug, Formatter}, - ops::Deref, + iter::repeat_n, }, }; @@ -22,10 +23,8 @@ fn vk_cstr_to_string_lossy(cstr: &[c_char]) -> String { /// Properties of the physical device for acceleration structures. /// -/// See -/// [`VkPhysicalDeviceAccelerationStructurePropertiesKHR`](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceAccelerationStructurePropertiesKHR.html) -/// manual page. -#[derive(Debug)] +/// See [`VkPhysicalDeviceAccelerationStructurePropertiesKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPhysicalDeviceAccelerationStructurePropertiesKHR.html). +#[derive(Clone, Copy, Debug)] pub struct AccelerationStructureProperties { /// The maximum number of geometries in a bottom level acceleration structure. pub max_geometry_count: u64, @@ -76,10 +75,8 @@ impl From> /// Structure describing depth/stencil resolve properties that can be supported by an /// implementation. /// -/// See -/// [`VkPhysicalDeviceDepthStencilResolveProperties`](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceDepthStencilResolveProperties.html) -/// manual page. -#[derive(Debug)] +/// See [`VkPhysicalDeviceDepthStencilResolveProperties`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPhysicalDeviceDepthStencilResolveProperties.html). +#[derive(Clone, Copy, Debug)] pub struct DepthStencilResolveProperties { /// A bitmask indicating the set of supported depth resolve modes. /// @@ -120,10 +117,8 @@ impl From> for DepthStencilR /// Features of the physical device for vertex indexing. /// -/// See -/// [`VkPhysicalDeviceIndexTypeUint8FeaturesEXT`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceIndexTypeUint8FeaturesEXT.html) -/// manual page. -#[derive(Debug, Default)] +/// See [`VkPhysicalDeviceIndexTypeUint8FeaturesEXT`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPhysicalDeviceIndexTypeUint8FeaturesEXT.html). +#[derive(Clone, Copy, Debug, Default)] pub struct IndexTypeUint8Features { /// Indicates that VK_INDEX_TYPE_UINT8_EXT can be used with vkCmdBindIndexBuffer2KHR and /// vkCmdBindIndexBuffer. @@ -139,68 +134,118 @@ impl From> for IndexTypeUint8Fea } /// Structure which holds data about the physical hardware selected by the current device. +#[derive(Clone)] +#[read_only::cast] pub struct PhysicalDevice { /// Describes the properties of the device which relate to acceleration structures, if /// available. + /// + /// _Note:_ This field is read-only. pub accel_struct_properties: Option, /// Describes the properties of the device which relate to depth/stencil resolve operations. + /// + /// _Note:_ This field is read-only. pub depth_stencil_resolve_properties: DepthStencilResolveProperties, - /// Describes the features of the physical device which are part of the Vulkan 1.0 base feature set. + /// Describes the features of the physical device which are part of the Vulkan 1.0 base feature + /// set. + /// + /// _Note:_ This field is read-only. pub features_v1_0: Vulkan10Features, - /// Describes the features of the physical device which are part of the Vulkan 1.1 base feature set. + /// Describes the features of the physical device which are part of the Vulkan 1.1 base feature + /// set. + /// + /// _Note:_ This field is read-only. pub features_v1_1: Vulkan11Features, - /// Describes the features of the physical device which are part of the Vulkan 1.2 base feature set. + /// Describes the features of the physical device which are part of the Vulkan 1.2 base feature + /// set. + /// + /// _Note:_ This field is read-only. pub features_v1_2: Vulkan12Features, + /// The native Vulkan resource handle of this buffer. + /// + /// _Note:_ This field is read-only. + pub handle: vk::PhysicalDevice, + /// Describes the features of the physical device which relate to vertex indexing. + /// + /// _Note:_ This field is read-only. pub index_type_uint8_features: IndexTypeUint8Features, + /// The Vulkan instance which owns this device. + /// + /// _Note:_ This field is read-only. + pub instance: Instance, + /// Memory properties of the physical device. + /// + /// _Note:_ This field is read-only. pub memory_properties: vk::PhysicalDeviceMemoryProperties, /// Device properties of the physical device which are part of the Vulkan 1.0 base feature set. + /// + /// _Note:_ This field is read-only. pub properties_v1_0: Vulkan10Properties, /// Describes the properties of the physical device which are part of the Vulkan 1.1 base /// feature set. + /// + /// _Note:_ This field is read-only. pub properties_v1_1: Vulkan11Properties, /// Describes the properties of the physical device which are part of the Vulkan 1.2 base /// feature set. + /// + /// _Note:_ This field is read-only. pub properties_v1_2: Vulkan12Properties, - physical_device: vk::PhysicalDevice, - /// Describes the queues offered by this physical device. + /// + /// _Note:_ This field is read-only. pub queue_families: Box<[vk::QueueFamilyProperties]>, pub(crate) queue_family_indices: Box<[u32]>, /// Describes the features of the device which relate to ray query, if available. + /// + /// _Note:_ This field is read-only. pub ray_query_features: RayQueryFeatures, /// Describes the features of the device which relate to ray tracing, if available. + /// + /// _Note:_ This field is read-only. pub ray_trace_features: RayTraceFeatures, /// Describes the properties of the device which relate to ray tracing, if available. + /// + /// _Note:_ This field is read-only. pub ray_trace_properties: Option, /// Describes the properties of the device which relate to min/max sampler filtering. + /// + /// _Note:_ This field is read-only. pub sampler_filter_minmax_properties: SamplerFilterMinmaxProperties, + + /// True if the device supports swapchain use. + /// + /// _Note:_ This field is read-only. + pub swapchain_ext: bool, } impl PhysicalDevice { /// Creates a physical device wrapper which reports features and properties. #[profiling::function] - pub fn new( - instance: &Instance, + pub(super) fn new( + instance: Instance, physical_device: vk::PhysicalDevice, ) -> Result { if physical_device == vk::PhysicalDevice::null() { + warn!("invalid physical device handle: null"); + return Err(DriverError::InvalidData); } @@ -273,11 +318,11 @@ impl PhysicalDevice { let depth_stencil_resolve_properties = depth_stencil_resolve_properties.into(); let sampler_filter_minmax_properties = sampler_filter_minmax_properties.into(); - let extensions = unsafe { + let extension_properties = unsafe { instance .enumerate_device_extension_properties(physical_device) .map_err(|err| { - error!("Unable to enumerate device extensions {err}"); + error!("unable to enumerate device extensions: {err}"); DriverError::Unsupported })? @@ -285,30 +330,33 @@ impl PhysicalDevice { debug!("physical device: {}", &properties_v1_0.device_name); - for property in &extensions { - let extension_name = property.extension_name.as_ptr(); + for prop in &extension_properties { + let extension_name = prop.extension_name.as_ptr(); if extension_name.is_null() { + warn!("invalid device extension name pointer: null"); + return Err(DriverError::InvalidData); } let extension_name = unsafe { CStr::from_ptr(extension_name) }; - debug!("extension {:?} v{}", extension_name, property.spec_version); + debug!("extension {:?} v{}", extension_name, prop.spec_version); } // Check for supported extensions - let extensions = extensions + let extension_names = extension_properties .iter() - .map(|property: &vk::ExtensionProperties| property.extension_name.as_ptr()) - .filter(|&extension_name| !extension_name.is_null()) + .map(|prop| prop.extension_name.as_ptr()) + .filter(|extension_name| !extension_name.is_null()) .map(|extension_name| unsafe { CStr::from_ptr(extension_name) }) .collect::>(); - let supports_accel_struct = extensions.contains(khr::acceleration_structure::NAME) - && extensions.contains(khr::deferred_host_operations::NAME); - let supports_index_type_uint8 = extensions.contains(ext::index_type_uint8::NAME); - let supports_ray_query = extensions.contains(khr::ray_query::NAME); - let supports_ray_trace = extensions.contains(khr::ray_tracing_pipeline::NAME); + let supports_accel_struct = extension_names.contains(khr::acceleration_structure::NAME) + && extension_names.contains(khr::deferred_host_operations::NAME); + let supports_index_type_uint8 = extension_names.contains(ext::index_type_uint8::NAME); + let supports_ray_query = extension_names.contains(khr::ray_query::NAME); + let supports_ray_trace = extension_names.contains(khr::ray_tracing_pipeline::NAME); + let swapchain_ext = extension_names.contains(khr::swapchain::NAME) && instance.surface_ext; // Gather optional features and properties of the physical device let index_type_uint8_features = if supports_index_type_uint8 { @@ -335,9 +383,10 @@ impl PhysicalDevice { features_v1_0, features_v1_1, features_v1_2, + handle: physical_device, index_type_uint8_features, + instance, memory_properties, - physical_device, properties_v1_0, properties_v1_1, properties_v1_2, @@ -347,8 +396,173 @@ impl PhysicalDevice { ray_trace_features, ray_trace_properties, sampler_filter_minmax_properties, + swapchain_ext, }) } + + /// Prepares device creation information and calls the provided callback to allow an application + /// to control the device creation process. + /// + /// _Note:_ This is only useful for interoperating with other libraries as device creation is + /// normally handled by the [`Device::create_display`] and [`Device::new`] + /// functions. + /// + /// # Safety + /// + /// This comes with all the caveats of using `ash` builder types, which are inherently + /// dangerous. Use with extreme caution. + #[profiling::function] + pub unsafe fn create_ash_device(&self, create_fn: F) -> ash::prelude::VkResult + where + F: FnOnce(vk::DeviceCreateInfo) -> ash::prelude::VkResult, + { + let mut enabled_ext_names = Vec::with_capacity(6); + + // The swapchain extension is required for presentation support, so we enable it whenever + // the physical device reports support. Imported instances may already carry the required + // instance extensions even though vk-graph did not create them. + if self.swapchain_ext { + enabled_ext_names.push(khr::swapchain::NAME.as_ptr()); + } + + if self.accel_struct_properties.is_some() { + enabled_ext_names.push(khr::acceleration_structure::NAME.as_ptr()); + enabled_ext_names.push(khr::deferred_host_operations::NAME.as_ptr()); + } + + if self.ray_query_features.ray_query { + enabled_ext_names.push(khr::ray_query::NAME.as_ptr()); + } + + if self.ray_trace_features.ray_tracing_pipeline { + enabled_ext_names.push(khr::ray_tracing_pipeline::NAME.as_ptr()); + } + + if self.index_type_uint8_features.index_type_uint8 { + enabled_ext_names.push(ext::index_type_uint8::NAME.as_ptr()); + } + + // Molten-vk doesn't support the full Vulkan feature set, hence the portability subset + // extension must be enabled. + #[cfg(all(target_os = "macos", feature = "loaded"))] + enabled_ext_names.push(khr::portability_subset::NAME.as_ptr()); + + let priorities = repeat_n( + 1.0, + self.queue_families + .iter() + .map(|family| family.queue_count) + .max() + .unwrap_or_default() as _, + ) + .collect::>(); + + let queue_infos = self + .queue_families + .iter() + .enumerate() + .map(|(idx, family)| { + let mut queue_info = vk::DeviceQueueCreateInfo::default() + .queue_family_index(idx as _) + .queue_priorities(&priorities[0..family.queue_count as usize]); + queue_info.queue_count = family.queue_count; + + queue_info + }) + .collect::>(); + + let ash::InstanceFnV1_1 { + get_physical_device_features2, + .. + } = self.instance.fp_v1_1(); + let mut features_v1_1 = vk::PhysicalDeviceVulkan11Features::default(); + let mut features_v1_2 = vk::PhysicalDeviceVulkan12Features::default(); + let mut acceleration_structure_features = + vk::PhysicalDeviceAccelerationStructureFeaturesKHR::default(); + let mut index_type_uint8_features = vk::PhysicalDeviceIndexTypeUint8FeaturesEXT::default(); + let mut ray_query_features = vk::PhysicalDeviceRayQueryFeaturesKHR::default(); + let mut ray_trace_features = vk::PhysicalDeviceRayTracingPipelineFeaturesKHR::default(); + let mut features = vk::PhysicalDeviceFeatures2::default() + .push_next(&mut features_v1_1) + .push_next(&mut features_v1_2); + + if self.accel_struct_properties.is_some() { + features = features.push_next(&mut acceleration_structure_features); + } + + if self.ray_query_features.ray_query { + features = features.push_next(&mut ray_query_features); + } + + if self.ray_trace_features.ray_tracing_pipeline { + features = features.push_next(&mut ray_trace_features); + } + + if self.index_type_uint8_features.index_type_uint8 { + features = features.push_next(&mut index_type_uint8_features); + } + + unsafe { get_physical_device_features2(self.handle, &mut features) }; + + let device_create_info = vk::DeviceCreateInfo::default() + .queue_create_infos(&queue_infos) + .enabled_extension_names(&enabled_ext_names) + .push_next(&mut features); + + create_fn(device_create_info) + } + + /// Lists the capabilities of a given format. + #[profiling::function] + pub fn format_properties(&self, format: vk::Format) -> vk::FormatProperties { + unsafe { + self.instance + .get_physical_device_format_properties(self.handle, format) + } + } + + /// Lists the physical device's image format capabilities. + /// + /// A result of `None` indicates the format is not supported. + #[profiling::function] + pub fn image_format_properties( + &self, + format: vk::Format, + ty: vk::ImageType, + tiling: vk::ImageTiling, + usage: vk::ImageUsageFlags, + flags: vk::ImageCreateFlags, + ) -> Result, DriverError> { + unsafe { + match self.instance.get_physical_device_image_format_properties( + self.handle, + format, + ty, + tiling, + usage, + flags, + ) { + Ok(properties) => Ok(Some(properties)), + Err(err) if err == vk::Result::ERROR_FORMAT_NOT_SUPPORTED => { + // We don't log this condition because it is normal for unsupported + // formats to be checked - we use the result to inform callers they + // cannot use those formats. + + Ok(None) + } + Err(err) => { + warn!("unable to query image format properties: {err}"); + + Err(DriverError::OutOfMemory) + } + } + } + } + + /// Creates a logical [`Device`] from this selected physical device. + pub fn try_into_device(self) -> Result { + Device::try_from_physical_device(self) + } } impl Debug for PhysicalDevice { @@ -361,20 +575,10 @@ impl Debug for PhysicalDevice { } } -impl Deref for PhysicalDevice { - type Target = vk::PhysicalDevice; - - fn deref(&self) -> &Self::Target { - &self.physical_device - } -} - /// Features of the physical device for ray query. /// -/// See -/// [`VkPhysicalDeviceRayQueryFeaturesKHR`](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceRayQueryFeaturesKHR.html) -/// manual page. -#[derive(Debug, Default)] +/// See [`VkPhysicalDeviceRayQueryFeaturesKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPhysicalDeviceRayQueryFeaturesKHR.html). +#[derive(Clone, Copy, Debug, Default)] pub struct RayQueryFeatures { /// Indicates whether the implementation supports ray query (`OpRayQueryProceedKHR`) /// functionality. @@ -391,15 +595,13 @@ impl From> for RayQueryFeatures { /// Features of the physical device for ray tracing. /// -/// See -/// [`VkPhysicalDeviceRayTracingPipelineFeaturesKHR`](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceRayTracingPipelineFeaturesKHR.html) -/// manual page. -#[derive(Debug, Default)] +/// See [`VkPhysicalDeviceRayTracingPipelineFeaturesKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPhysicalDeviceRayTracingPipelineFeaturesKHR.html). +#[derive(Clone, Copy, Debug, Default)] pub struct RayTraceFeatures { /// Indicates whether the implementation supports the ray tracing pipeline functionality. /// /// See - /// [Ray Tracing](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#ray-tracing). + /// See the [ray tracing pipeline chapter](https://docs.vulkan.org/spec/latest/chapters/raytracing.html). pub ray_tracing_pipeline: bool, /// Indicates whether the implementation supports saving and reusing shader group handles, e.g. @@ -441,10 +643,8 @@ impl From> for RayTraceFeatu /// Properties of the physical device for ray tracing. /// -/// See -/// [`VkPhysicalDeviceRayTracingPipelinePropertiesKHR`](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceRayTracingPipelinePropertiesKHR.html) -/// manual page. -#[derive(Debug)] +/// See [`VkPhysicalDeviceRayTracingPipelinePropertiesKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPhysicalDeviceRayTracingPipelinePropertiesKHR.html). +#[derive(Clone, Copy, Debug)] pub struct RayTraceProperties { /// The size in bytes of the shader header. pub shader_group_handle_size: u32, @@ -492,9 +692,8 @@ impl From> for RayTracePro /// Properties of the physical device for min/max sampler filtering. /// -/// See -/// [`VkPhysicalDeviceSamplerFilterMinmaxProperties`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceSamplerFilterMinmaxPropertiesEXT.html) -#[derive(Debug)] +/// See [`VkPhysicalDeviceLimits`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPhysicalDeviceLimits.html). +#[derive(Clone, Copy, Debug)] pub struct SamplerFilterMinmaxProperties { /// When `false` the component mapping of the image view used with min/max filtering must have /// been created with the r component set to the identity swizzle. Only the r component of the @@ -537,10 +736,8 @@ impl From> for SamplerFilter /// Description of Vulkan features. /// -/// See -/// [`VkPhysicalDeviceFeatures`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceFeatures.html) -/// manual page. -#[derive(Debug)] +/// See [`VkPhysicalDeviceProperties`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPhysicalDeviceProperties.html). +#[derive(Clone, Copy, Debug)] pub struct Vulkan10Features { /// Specifies that accesses to buffers are bounds-checked against the range of the buffer /// descriptor. @@ -553,8 +750,8 @@ pub struct Vulkan10Features { /// primitive restart index, which is always 2^32 - 1 when the VkIndexType is /// `VK_INDEX_TYPE_UINT32`). /// - /// If this feature is supported, `maxDrawIndexedIndexValue` must be 2^32 - 1; otherwise it must - /// be no smaller than 2^24 - 1. See maxDrawIndexedIndexValue. + /// If this feature is supported, `maxDrawIndexedIndexValue` must be 2^32 - 1; otherwise it + /// must be no smaller than 2^24 - 1. See maxDrawIndexedIndexValue. pub full_draw_index_uint32: bool, /// Specifies whether image views with a `VkImageViewType` of `VK_IMAGE_VIEW_TYPE_CUBE_ARRAY` @@ -602,26 +799,27 @@ pub struct Vulkan10Features { /// /// If this feature is not enabled, the `VK_BLEND_FACTOR_SRC1_COLOR`, /// `VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR`, `VK_BLEND_FACTOR_SRC1_ALPHA`, and - /// `VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA` enum values must not be used as source or destination - /// blending factors. + /// `VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA` enum values must not be used as source or + /// destination blending factors. /// - /// See [dual-source blending](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#framebuffer-dsb). + /// See the [features chapter](https://docs.vulkan.org/spec/latest/chapters/features.html). pub dual_src_blend: bool, /// Specifies whether logic operations are supported. /// /// If this feature is not enabled, the `logicOpEnable` member of the - /// `VkPipelineColorBlendStateCreateInfo` structure must be set to `VK_FALSE`, and the `logicOp` - /// member is ignored. + /// `VkPipelineColorBlendStateCreateInfo` structure must be set to `VK_FALSE`, and the + /// `logicOp` member is ignored. pub logic_op: bool, /// Specifies whether multiple draw indirect is supported. /// /// If this feature is not enabled, the `drawCount` parameter to the `vkCmdDrawIndirect` and - /// `vkCmdDrawIndexedIndirect` commands must be `0` or `1`. The `maxDrawIndirectCount` member of the - /// `VkPhysicalDeviceLimits` structure must also be `1` if this feature is not supported. + /// `vkCmdDrawIndexedIndirect` commands must be `0` or `1`. The `maxDrawIndirectCount` member + /// of the `VkPhysicalDeviceLimits` structure must also be `1` if this feature is not + /// supported. /// - /// See [maxDrawIndirectCount](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#limits-maxDrawIndirectCount). + /// See the [features chapter](https://docs.vulkan.org/spec/latest/chapters/features.html). pub multi_draw_indirect: bool, /// Specifies whether indirect drawing calls support the `firstInstance` parameter. @@ -674,14 +872,16 @@ pub struct Vulkan10Features { /// Specifies whether points with size greater than `1.0` are supported. /// - /// If this feature is not enabled, only a point size of `1.0` written by a shader is supported. + /// If this feature is not enabled, only a point size of `1.0` written by a shader is + /// supported. /// - /// The range and granularity of supported point sizes are indicated by the `pointSizeRange` and - /// `pointSizeGranularity` members of the `VkPhysicalDeviceLimits` structure, respectively. + /// The range and granularity of supported point sizes are indicated by the `pointSizeRange` + /// and `pointSizeGranularity` members of the `VkPhysicalDeviceLimits` structure, + /// respectively. pub large_points: bool, /// Specifies whether the implementation is able to replace the alpha value of the fragment - /// shader color output in the [multisample coverage](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#fragops-covg) + /// See the [features chapter](https://docs.vulkan.org/spec/latest/chapters/features.html). /// fragment operation. /// /// If this feature is not enabled, then the `alphaToOneEnable` member of the @@ -834,7 +1034,8 @@ pub struct Vulkan10Features { /// If this feature is not enabled, the `OpImage*Gather` instructions do not support the /// `Offset` and `ConstOffsets` operands. /// - /// This also specifies whether shader modules can declare the `ImageGatherExtended` capability. + /// This also specifies whether shader modules can declare the `ImageGatherExtended` + /// capability. pub shader_image_gather_extended: bool, /// Specifies whether all the “storage image extended formats” below are supported. @@ -866,9 +1067,9 @@ pub struct Vulkan10Features { /// - VK_FORMAT_R16_UINT /// - VK_FORMAT_R8_UINT /// - /// _Note:_ `shaderStorageImageExtendedFormats` feature only adds a guarantee of format support, - /// which is specified for the whole physical device. Therefore enabling or disabling the - /// feature via vkCreateDevice has no practical effect. + /// _Note:_ `shaderStorageImageExtendedFormats` feature only adds a guarantee of format + /// support, which is specified for the whole physical device. Therefore enabling or + /// disabling the feature via vkCreateDevice has no practical effect. /// /// To query for additional properties, or if the feature is not supported, /// `vkGetPhysicalDeviceFormatProperties` and `vkGetPhysicalDeviceImageFormatProperties` can be @@ -876,7 +1077,7 @@ pub struct Vulkan10Features { /// /// `VK_FORMAT_R32G32_UINT`, `VK_FORMAT_R32G32_SINT`, and `VK_FORMAT_R32G32_SFLOAT` from /// `StorageImageExtendedFormats` SPIR-V capability, are already covered by core Vulkan - /// [mandatory format support](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#formats-mandatory-features-32bit). + /// See the [features chapter](https://docs.vulkan.org/spec/latest/chapters/features.html). pub shader_storage_image_extended_formats: bool, /// Specifies whether multisampled storage images are supported. @@ -892,7 +1093,7 @@ pub struct Vulkan10Features { /// specified when reading. /// /// `shaderStorageImageReadWithoutFormat` applies only to formats listed in the - /// [storage without format](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#formats-without-shader-storage-format) + /// See the [features chapter](https://docs.vulkan.org/spec/latest/chapters/features.html). /// list. pub shader_storage_image_read_without_format: bool, @@ -900,7 +1101,7 @@ pub struct Vulkan10Features { /// specified when writing. /// /// `shaderStorageImageWriteWithoutFormat` applies only to formats listed in the - /// [storage without format](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#formats-without-shader-storage-format) + /// See the [features chapter](https://docs.vulkan.org/spec/latest/chapters/features.html). /// list. pub shader_storage_image_write_without_format: bool, @@ -915,8 +1116,8 @@ pub struct Vulkan10Features { /// `UniformBufferArrayDynamicIndexing` capability. pub shader_uniform_buffer_array_dynamic_indexing: bool, - /// Specifies whether arrays of samplers or sampled images can be indexed by dynamically uniform - /// integer expressions in shader code. + /// Specifies whether arrays of samplers or sampled images can be indexed by dynamically + /// uniform integer expressions in shader code. /// /// If this feature is not enabled, resources with a descriptor type of /// `VK_DESCRIPTOR_TYPE_SAMPLER`, `VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER`, or @@ -967,7 +1168,8 @@ pub struct Vulkan10Features { /// Specifies whether 64-bit floats (doubles) are supported in shader code. /// - /// If this feature is not enabled, 64-bit floating-point types must not be used in shader code. + /// If this feature is not enabled, 64-bit floating-point types must not be used in shader + /// code. /// /// This also specifies whether shader modules can declare the `Float64` capability. Declaring /// and using 64-bit floats is enabled for all storage classes that SPIR-V allows with the @@ -978,21 +1180,21 @@ pub struct Vulkan10Features { /// /// If this feature is not enabled, 64-bit integer types must not be used in shader code. /// - /// This also specifies whether shader modules can declare the `Int64` capability. Declaring and - /// using 64-bit integers is enabled for all storage classes that SPIR-V allows with the `Int64` - /// capability. + /// This also specifies whether shader modules can declare the `Int64` capability. Declaring + /// and using 64-bit integers is enabled for all storage classes that SPIR-V allows with + /// the `Int64` capability. pub shader_int64: bool, /// Specifies whether 16-bit integers (signed and unsigned) are supported in shader code. /// /// If this feature is not enabled, 16-bit integer types must not be used in shader code. /// - /// This also specifies whether shader modules can declare the `Int16` capability. However, this - /// only enables a subset of the storage classes that SPIR-V allows for the `Int16` SPIR-V - /// capability: Declaring and using 16-bit integers in the `Private`, `Workgroup` (for non-Block - /// variables), and `Function` storage classes is enabled, while declaring them in the interface - /// storage classes (e.g., `UniformConstant`, `Uniform`, `StorageBuffer`, `Input`, `Output`, and - /// `PushConstant`) is not enabled. + /// This also specifies whether shader modules can declare the `Int16` capability. However, + /// this only enables a subset of the storage classes that SPIR-V allows for the `Int16` + /// SPIR-V capability: Declaring and using 16-bit integers in the `Private`, `Workgroup` + /// (for non-Block variables), and `Function` storage classes is enabled, while declaring + /// them in the interface storage classes (e.g., `UniformConstant`, `Uniform`, + /// `StorageBuffer`, `Input`, `Output`, and `PushConstant`) is not enabled. pub shader_int16: bool, /// Specifies whether image operations specifying the minimum resource LOD are supported in @@ -1100,50 +1302,48 @@ impl From for Vulkan10Features { /// Description of Vulkan limitations. /// -/// See -/// [`VkPhysicalDeviceLimits`](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceLimits.html) -/// manual page. -#[allow(missing_docs)] // TODO: Finish docs! -#[derive(Debug)] +/// See [`VkPhysicalDeviceVulkan11Features`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPhysicalDeviceVulkan11Features.html). +#[derive(Clone, Copy, Debug)] pub struct Vulkan10Limits { - /// The largest dimension (width) that is guaranteed to be supported for all images created with - /// an image type of [`ImageType::Texture1D`](super::image::ImageType). + /// The largest dimension (width) that is guaranteed to be supported for all images created + /// with an image type of [`vk::ImageType::TYPE_1D`]. /// /// Some combinations of image parameters (format, usage, etc.) may allow support for larger /// dimensions, which can be queried using - /// [`Device::image_format_properties`](super::device::Device::image_format_properties). + /// [`PhysicalDevice::image_format_properties`]. pub max_image_dimension1_d: u32, /// The largest dimension (width or height) that is guaranteed to be supported for all images - /// created with an image type of [`ImageType::Texture2D`](super::image::ImageType) and without + /// created with an image type of [`vk::ImageType::TYPE_2D`] and without /// [`vk::ImageCreateFlags::CUBE_COMPATIBLE`] set in /// [`ImageInfo::flags`](super::image::ImageInfo::flags). /// /// Some combinations of image parameters (format, usage, etc.) may allow support for larger /// dimensions, which can be queried using - /// [`Device::image_format_properties`](super::device::Device::image_format_properties). + /// [`PhysicalDevice::image_format_properties`]. pub max_image_dimension2_d: u32, /// The largest dimension (width, height, or depth) that is guaranteed to be supported for all - /// images created with an image type of [`ImageType::Texture3D`](super::image::ImageType). + /// images created with an image type of [`vk::ImageType::TYPE_3D`]. /// /// Some combinations of image parameters (format, usage, etc.) may allow support for larger /// dimensions, which can be queried using - /// [`Device::image_format_properties`](super::device::Device::image_format_properties). + /// [`PhysicalDevice::image_format_properties`]. pub max_image_dimension3_d: u32, /// The largest dimension (width or height) that is guaranteed to be supported for all images - /// created with an image type of [`ImageType::Texture2D`](super::image::ImageType) and with + /// created with an image type of [`vk::ImageType::TYPE_2D`] and with /// [`vk::ImageCreateFlags::CUBE_COMPATIBLE`] set in /// [`ImageInfo::flags`](super::image::ImageInfo::flags). /// /// Some combinations of image parameters (format, usage, etc.) may allow support for larger /// dimensions, which can be queried using - /// [`Device::image_format_properties`](super::device::Device::image_format_properties). + /// [`PhysicalDevice::image_format_properties`]. pub max_image_dimension_cube: u32, /// The maximum number of layers - /// ([`ImageInfo::array_elements`](super::image::ImageInfo::array_elements)) for an image. + /// ([`ImageInfo::array_layer_count`](super::image::ImageInfo::array_layer_count)) for an + /// image. pub max_image_array_layers: u32, /// The maximum number of addressable texels for a buffer view created on a buffer which was @@ -1151,105 +1351,305 @@ pub struct Vulkan10Limits { /// [`vk::BufferUsageFlags::STORAGE_TEXEL_BUFFER`] set in /// [`BufferInfo::usage`](super::buffer::BufferInfo::usage). pub max_texel_buffer_elements: u32, + + /// The maximum range, in bytes, for a uniform buffer binding. pub max_uniform_buffer_range: u32, + + /// The maximum range, in bytes, for a storage buffer binding. pub max_storage_buffer_range: u32, + + /// The maximum size, in bytes, of the push constant storage available to a pipeline layout. pub max_push_constants_size: u32, + + /// The maximum number of simultaneously active device memory allocations. pub max_memory_allocation_count: u32, + + /// The maximum number of sampler objects that may exist at one time. pub max_sampler_allocation_count: u32, + + /// The required granularity, in bytes, for aliasing linear and optimal resources in memory. pub buffer_image_granularity: vk::DeviceSize, + + /// The maximum size, in bytes, of the address space available for sparse resources. pub sparse_address_space_size: vk::DeviceSize, + + /// The maximum number of descriptor sets that can be bound simultaneously. pub max_bound_descriptor_sets: u32, + + /// The maximum number of sampler descriptors accessible from a single shader stage. pub max_per_stage_descriptor_samplers: u32, + + /// The maximum number of uniform buffer descriptors accessible from a single shader stage. pub max_per_stage_descriptor_uniform_buffers: u32, + + /// The maximum number of storage buffer descriptors accessible from a single shader stage. pub max_per_stage_descriptor_storage_buffers: u32, + + /// The maximum number of sampled image descriptors accessible from a single shader stage. pub max_per_stage_descriptor_sampled_images: u32, + + /// The maximum number of storage image descriptors accessible from a single shader stage. pub max_per_stage_descriptor_storage_images: u32, + + /// The maximum number of input attachment descriptors accessible from a single shader stage. pub max_per_stage_descriptor_input_attachments: u32, + + /// The maximum total number of resources accessible from a single shader stage. pub max_per_stage_resources: u32, + + /// The maximum number of sampler descriptors available across a single descriptor set. pub max_descriptor_set_samplers: u32, + + /// The maximum number of uniform buffer descriptors available across a single descriptor set. pub max_descriptor_set_uniform_buffers: u32, + + /// The maximum number of dynamic uniform buffer descriptors available across a descriptor set. pub max_descriptor_set_uniform_buffers_dynamic: u32, + + /// The maximum number of storage buffer descriptors available across a single descriptor set. pub max_descriptor_set_storage_buffers: u32, + + /// The maximum number of dynamic storage buffer descriptors available across a descriptor set. pub max_descriptor_set_storage_buffers_dynamic: u32, + + /// The maximum number of sampled image descriptors available across a single descriptor set. pub max_descriptor_set_sampled_images: u32, + + /// The maximum number of storage image descriptors available across a single descriptor set. pub max_descriptor_set_storage_images: u32, + + /// The maximum number of input attachment descriptors available across a descriptor set. pub max_descriptor_set_input_attachments: u32, + + /// The maximum number of vertex input attributes for a graphics pipeline. pub max_vertex_input_attributes: u32, + + /// The maximum number of vertex input bindings for a graphics pipeline. pub max_vertex_input_bindings: u32, + + /// The maximum offset, in bytes, allowed for a vertex input attribute. pub max_vertex_input_attribute_offset: u32, + + /// The maximum stride, in bytes, for a vertex input binding. pub max_vertex_input_binding_stride: u32, + + /// The maximum number of components output by the vertex stage. pub max_vertex_output_components: u32, + + /// The maximum tessellation generation level. pub max_tessellation_generation_level: u32, + + /// The maximum number of control points in a tessellation patch. pub max_tessellation_patch_size: u32, + + /// The maximum number of per-vertex input components to tessellation control shaders. pub max_tessellation_control_per_vertex_input_components: u32, + + /// The maximum number of per-vertex output components from tessellation control shaders. pub max_tessellation_control_per_vertex_output_components: u32, + + /// The maximum number of per-patch output components from tessellation control shaders. pub max_tessellation_control_per_patch_output_components: u32, + + /// The maximum total number of output components from tessellation control shaders. pub max_tessellation_control_total_output_components: u32, + + /// The maximum number of input components to tessellation evaluation shaders. pub max_tessellation_evaluation_input_components: u32, + + /// The maximum number of output components from tessellation evaluation shaders. pub max_tessellation_evaluation_output_components: u32, + + /// The maximum number of geometry shader invocations per primitive. pub max_geometry_shader_invocations: u32, + + /// The maximum number of input components to geometry shaders. pub max_geometry_input_components: u32, + + /// The maximum number of output components from geometry shaders. pub max_geometry_output_components: u32, + + /// The maximum number of vertices a geometry shader may emit. pub max_geometry_output_vertices: u32, + + /// The maximum total number of output components from a geometry shader invocation. pub max_geometry_total_output_components: u32, + + /// The maximum number of input components to fragment shaders. pub max_fragment_input_components: u32, + + /// The maximum number of color attachments written by a fragment shader. pub max_fragment_output_attachments: u32, + + /// The maximum number of dual-source attachments written by a fragment shader. pub max_fragment_dual_src_attachments: u32, + + /// The maximum combined number of fragment shader output resources. pub max_fragment_combined_output_resources: u32, + + /// The maximum amount of shared memory, in bytes, available to a compute workgroup. pub max_compute_shared_memory_size: u32, + + /// The maximum workgroup counts for compute dispatch in each dimension. pub max_compute_work_group_count: [u32; 3], + + /// The maximum total number of invocations in a single compute workgroup. pub max_compute_work_group_invocations: u32, + + /// The maximum workgroup size for compute dispatch in each dimension. pub max_compute_work_group_size: [u32; 3], + + /// The number of bits of sub-pixel precision in framebuffer coordinates. pub sub_pixel_precision_bits: u32, + + /// The number of bits of sub-texel precision for image sampling. pub sub_texel_precision_bits: u32, + + /// The number of bits of precision available for mipmap level selection. pub mipmap_precision_bits: u32, + + /// The maximum value of an index used with indexed drawing. pub max_draw_indexed_index_value: u32, + + /// The maximum number of draws executed by an indirect draw-count command. pub max_draw_indirect_count: u32, + + /// The maximum absolute value of sampler LOD bias. pub max_sampler_lod_bias: f32, + + /// The maximum sampler anisotropy level. pub max_sampler_anisotropy: f32, + + /// The maximum number of simultaneously active viewports. pub max_viewports: u32, + + /// The maximum viewport width and height. pub max_viewport_dimensions: [u32; 2], + + /// The minimum and maximum supported viewport bounds. pub viewport_bounds_range: [f32; 2], + + /// The number of bits of precision for viewport sub-pixel coordinates. pub viewport_sub_pixel_bits: u32, + + /// The minimum alignment, in bytes, of host pointer values passed to mapping functions. pub min_memory_map_alignment: usize, + + /// The minimum alignment, in bytes, for texel buffer offsets. pub min_texel_buffer_offset_alignment: vk::DeviceSize, + + /// The minimum alignment, in bytes, for uniform buffer offsets. pub min_uniform_buffer_offset_alignment: vk::DeviceSize, + + /// The minimum alignment, in bytes, for storage buffer offsets. pub min_storage_buffer_offset_alignment: vk::DeviceSize, + + /// The minimum offset allowed for texel fetch operations. pub min_texel_offset: i32, + + /// The maximum offset allowed for texel fetch operations. pub max_texel_offset: u32, + + /// The minimum offset allowed for texel gather operations. pub min_texel_gather_offset: i32, + + /// The maximum offset allowed for texel gather operations. pub max_texel_gather_offset: u32, + + /// The minimum interpolation offset supported by the implementation. pub min_interpolation_offset: f32, + + /// The maximum interpolation offset supported by the implementation. pub max_interpolation_offset: f32, + + /// The number of bits of sub-pixel precision for interpolation offsets. pub sub_pixel_interpolation_offset_bits: u32, + + /// The maximum framebuffer width. pub max_framebuffer_width: u32, + + /// The maximum framebuffer height. pub max_framebuffer_height: u32, + + /// The maximum number of framebuffer layers. pub max_framebuffer_layers: u32, + + /// The sample counts supported for color framebuffer attachments. pub framebuffer_color_sample_counts: vk::SampleCountFlags, + + /// The sample counts supported for depth framebuffer attachments. pub framebuffer_depth_sample_counts: vk::SampleCountFlags, + + /// The sample counts supported for stencil framebuffer attachments. pub framebuffer_stencil_sample_counts: vk::SampleCountFlags, + + /// The sample counts supported for framebuffers with no attachments. pub framebuffer_no_attachments_sample_counts: vk::SampleCountFlags, + + /// The maximum number of color attachments in a framebuffer. pub max_color_attachments: u32, + + /// The sample counts supported for sampled color images. pub sampled_image_color_sample_counts: vk::SampleCountFlags, + + /// The sample counts supported for sampled integer images. pub sampled_image_integer_sample_counts: vk::SampleCountFlags, + + /// The sample counts supported for sampled depth images. pub sampled_image_depth_sample_counts: vk::SampleCountFlags, + + /// The sample counts supported for sampled stencil images. pub sampled_image_stencil_sample_counts: vk::SampleCountFlags, + + /// The sample counts supported for storage images. pub storage_image_sample_counts: vk::SampleCountFlags, + + /// The maximum number of words in a sample mask. pub max_sample_mask_words: u32, + + /// True if timestamps are supported in both graphics and compute queues. pub timestamp_compute_and_graphics: bool, + + /// The number of nanoseconds per timestamp increment. pub timestamp_period: f32, + + /// The maximum number of clip distances. pub max_clip_distances: u32, + + /// The maximum number of cull distances. pub max_cull_distances: u32, + + /// The maximum combined number of clip and cull distances. pub max_combined_clip_and_cull_distances: u32, + + /// The number of discrete queue priorities supported by the implementation. pub discrete_queue_priorities: u32, + + /// The supported range of point sizes. pub point_size_range: [f32; 2], + + /// The supported range of line widths. pub line_width_range: [f32; 2], + + /// The granularity of point-size values. pub point_size_granularity: f32, + + /// The granularity of line-width values. pub line_width_granularity: f32, + + /// True if line rasterization follows strict diamond-exit rules. pub strict_lines: bool, + + /// True if standard sample locations are supported. pub standard_sample_locations: bool, + + /// The optimal alignment, in bytes, for buffer copy source and destination offsets. pub optimal_buffer_copy_offset_alignment: vk::DeviceSize, + + /// The optimal alignment, in bytes, for buffer copy row pitch values. pub optimal_buffer_copy_row_pitch_alignment: vk::DeviceSize, + + /// The non-coherent atom size, in bytes, used for host cache management. pub non_coherent_atom_size: vk::DeviceSize, } @@ -1380,20 +1780,18 @@ impl From for Vulkan10Limits { /// Description of Vulkan 1.0 properties. /// -/// See -/// [`VkPhysicalDeviceProperties`](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceProperties.html) -/// manual page. -#[derive(Debug)] +/// See [`VkPhysicalDeviceVulkan11Properties`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPhysicalDeviceVulkan11Properties.html). +#[derive(Clone, Debug)] pub struct Vulkan10Properties { /// The version of Vulkan supported by the device, encoded as described - /// [here](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#extendingvulkan-coreversions-versionnumbers). + /// See the [limits chapter](https://docs.vulkan.org/spec/latest/chapters/limits.html). pub api_version: u32, /// The vendor-specified version of the driver. pub driver_version: u32, /// A unique identifier for the vendor (see - /// [note](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceProperties.html#_description)) + /// See the [limits chapter](https://docs.vulkan.org/spec/latest/chapters/limits.html). /// of the physical device. pub vendor_id: u32, @@ -1401,7 +1799,7 @@ pub struct Vulkan10Properties { pub device_id: u32, /// a - /// [VkPhysicalDeviceType](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceType.html) + /// See the [limits chapter](https://docs.vulkan.org/spec/latest/chapters/limits.html). /// specifying the type of device. pub device_type: vk::PhysicalDeviceType, @@ -1438,10 +1836,8 @@ impl From for Vulkan10Properties { /// Description of Vulkan 1.1 features. /// -/// See -/// [`VkPhysicalDeviceVulkan11Features`](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceVulkan11Features.html) -/// manual page. -#[derive(Debug)] +/// See [`VkPhysicalDeviceVulkan12Features`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPhysicalDeviceVulkan12Features.html). +#[derive(Clone, Copy, Debug)] pub struct Vulkan11Features { /// Specifies whether objects in the StorageBuffer, ShaderRecordBufferKHR, or /// PhysicalStorageBuffer storage class with the Block decoration can have 16-bit integer and @@ -1463,8 +1859,8 @@ pub struct Vulkan11Features { /// Specifies whether objects in the PushConstant storage class can have 16-bit integer and /// 16-bit floating-point members. /// - /// If this feature is not enabled, 16-bit integer or floating-point members must not be used in - /// such objects. This also specifies whether shader modules can declare the + /// If this feature is not enabled, 16-bit integer or floating-point members must not be used + /// in such objects. This also specifies whether shader modules can declare the /// StoragePushConstant16 capability. pub storage_push_constant16: bool, @@ -1481,15 +1877,15 @@ pub struct Vulkan11Features { /// If this feature is not enabled, the view mask of each subpass must always be zero. pub multiview: bool, - /// Specifies whether the implementation supports multiview rendering within a render pass, with - /// geometry shaders. + /// Specifies whether the implementation supports multiview rendering within a render pass, + /// with geometry shaders. /// /// If this feature is not enabled, then a pipeline compiled against a subpass with a non-zero /// view mask must not include a geometry shader. pub multiview_geometry_shader: bool, - /// Specifies whether the implementation supports multiview rendering within a render pass, with - /// tessellation shaders. + /// Specifies whether the implementation supports multiview rendering within a render pass, + /// with tessellation shaders. /// /// If this feature is not enabled, then a pipeline compiled against a subpass with a non-zero /// view mask must not include any tessellation shaders. @@ -1547,10 +1943,8 @@ impl From> for Vulkan11Features { /// Description of Vulkan 1.1 properties. /// -/// See -/// [`VkPhysicalDeviceVulkan11Properties`](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceVulkan11Properties.html) -/// manual page. -#[derive(Debug)] +/// See [`VkPhysicalDeviceVulkan12Properties`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPhysicalDeviceVulkan12Properties.html). +#[derive(Clone, Copy, Debug)] pub struct Vulkan11Properties { /// An array of `VK_UUID_SIZE` `u8` values representing a universally unique identifier for /// the device @@ -1605,16 +1999,16 @@ pub struct Vulkan11Properties { /// recorded within a subpass of a multiview render pass instance. pub max_multiview_instance_index: u32, - /// Specifies how an implementation behaves when an application attempts to write to unprotected - /// memory in a protected queue operation, read from protected memory in an unprotected queue - /// operation, or perform a query in a protected queue operation. + /// Specifies how an implementation behaves when an application attempts to write to + /// unprotected memory in a protected queue operation, read from protected memory in an + /// unprotected queue operation, or perform a query in a protected queue operation. /// /// If this limit is `true`, such writes will be discarded or have undefined values written, /// reads and queries will return undefined values. /// /// If this limit is `false`, applications must not perform these operations. /// - /// See [memory-protected-access-rules](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceVulkan11Properties.html#memory-protected-access-rules) + /// See the [features chapter](https://docs.vulkan.org/spec/latest/chapters/features.html). /// for more information. pub protected_no_fault: bool, @@ -1656,10 +2050,8 @@ impl From> for Vulkan11Properties { /// Description of Vulkan 1.2 features. /// -/// See -/// [`VkPhysicalDeviceVulkan12Features`](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceVulkan12Features.html) -/// manual page. -#[derive(Debug)] +/// See [`VkPhysicalDeviceVulkan12Features`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPhysicalDeviceVulkan12Features.html). +#[derive(Clone, Copy, Debug)] pub struct Vulkan12Features { /// Indicates whether the implementation supports the /// `VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE` sampler address mode. @@ -1678,22 +2070,24 @@ pub struct Vulkan12Features { /// PhysicalStorageBuffer storage class with the Block decoration can have 8-bit integer /// members. /// - /// If this feature is not enabled, 8-bit integer members must not be used in such objects. This - /// also indicates whether shader modules can declare the StorageBuffer8BitAccess capability. + /// If this feature is not enabled, 8-bit integer members must not be used in such objects. + /// This also indicates whether shader modules can declare the StorageBuffer8BitAccess + /// capability. pub storage_buffer8_bit_access: bool, /// Indicates whether objects in the Uniform storage class with the Block decoration can have /// 8-bit integer members. /// - /// If this feature is not enabled, 8-bit integer members must not be used in such objects. This - /// also indicates whether shader modules can declare the UniformAndStorageBuffer8BitAccess - /// capability. + /// If this feature is not enabled, 8-bit integer members must not be used in such objects. + /// This also indicates whether shader modules can declare the + /// UniformAndStorageBuffer8BitAccess capability. pub uniform_and_storage_buffer8_bit_access: bool, /// Indicates whether objects in the PushConstant storage class can have 8-bit integer members. /// - /// If this feature is not enabled, 8-bit integer members must not be used in such objects. This - /// also indicates whether shader modules can declare the StoragePushConstant8 capability. + /// If this feature is not enabled, 8-bit integer members must not be used in such objects. + /// This also indicates whether shader modules can declare the StoragePushConstant8 + /// capability. pub storage_push_constant8: bool, /// Indicates whether shaders can perform 64-bit unsigned and signed integer atomic operations @@ -1706,12 +2100,12 @@ pub struct Vulkan12Features { /// Indicates whether 16-bit floats (halfs) are supported in shader code. /// - /// This also indicates whether shader modules can declare the Float16 capability. However, this - /// only enables a subset of the storage classes that SPIR-V allows for the Float16 SPIR-V - /// capability: Declaring and using 16-bit floats in the Private, Workgroup (for non-Block - /// variables), and Function storage classes is enabled, while declaring them in the interface - /// storage classes (e.g., UniformConstant, Uniform, StorageBuffer, Input, Output, and - /// PushConstant) is not enabled. + /// This also indicates whether shader modules can declare the Float16 capability. However, + /// this only enables a subset of the storage classes that SPIR-V allows for the Float16 + /// SPIR-V capability: Declaring and using 16-bit floats in the Private, Workgroup (for + /// non-Block variables), and Function storage classes is enabled, while declaring them in + /// the interface storage classes (e.g., UniformConstant, Uniform, StorageBuffer, Input, + /// Output, and PushConstant) is not enabled. pub shader_float16: bool, /// Indicates whether 8-bit integers (signed and unsigned) are supported in shader code. @@ -1725,12 +2119,12 @@ pub struct Vulkan12Features { pub shader_int8: bool, /// Indicates whether the implementation supports the minimum set of descriptor indexing - /// features as described in the [Feature Requirements] section. Enabling the descriptorIndexing - /// member when vkCreateDevice is called does not imply the other minimum descriptor indexing - /// features are also enabled. Those other descriptor indexing features must be enabled - /// individually as needed by the application. + /// features as described in the [Feature Requirements] section. Enabling the + /// descriptorIndexing member when vkCreateDevice is called does not imply the other + /// minimum descriptor indexing features are also enabled. Those other descriptor indexing + /// features must be enabled individually as needed by the application. /// - /// [Feature Requirements]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#features-requirements + /// See the [features chapter](https://docs.vulkan.org/spec/latest/chapters/features.html). pub descriptor_indexing: bool, /// Indicates whether arrays of input attachments can be indexed by dynamically uniform integer @@ -1770,14 +2164,14 @@ pub struct Vulkan12Features { /// capability. pub shader_uniform_buffer_array_non_uniform_indexing: bool, - /// Indicates whether arrays of samplers or sampled images can be indexed by non-uniform integer - /// expressions in shader code. + /// Indicates whether arrays of samplers or sampled images can be indexed by non-uniform + /// integer expressions in shader code. /// /// If this feature is not enabled, resources with a descriptor type of /// VK_DESCRIPTOR_TYPE_SAMPLER, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, or - /// VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE must not be indexed by non-uniform integer expressions when - /// aggregated into arrays in shader code. This also indicates whether shader modules can - /// declare the SampledImageArrayNonUniformIndexing capability. + /// VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE must not be indexed by non-uniform integer expressions + /// when aggregated into arrays in shader code. This also indicates whether shader modules + /// can declare the SampledImageArrayNonUniformIndexing capability. pub shader_sampled_image_array_non_uniform_indexing: bool, /// Indicates whether arrays of storage buffers can be indexed by non-uniform integer @@ -1790,13 +2184,13 @@ pub struct Vulkan12Features { /// capability. pub shader_storage_buffer_array_non_uniform_indexing: bool, - /// Indicates whether arrays of storage images can be indexed by non-uniform integer expressions - /// in shader code. + /// Indicates whether arrays of storage images can be indexed by non-uniform integer + /// expressions in shader code. /// /// If this feature is not enabled, resources with a descriptor type of - /// VK_DESCRIPTOR_TYPE_STORAGE_IMAGE must not be indexed by non-uniform integer expressions when - /// aggregated into arrays in shader code. This also indicates whether shader modules can - /// declare the StorageImageArrayNonUniformIndexing capability. + /// VK_DESCRIPTOR_TYPE_STORAGE_IMAGE must not be indexed by non-uniform integer expressions + /// when aggregated into arrays in shader code. This also indicates whether shader modules + /// can declare the StorageImageArrayNonUniformIndexing capability. pub shader_storage_image_array_non_uniform_indexing: bool, /// Indicates whether arrays of input attachments can be indexed by non-uniform integer @@ -1829,44 +2223,44 @@ pub struct Vulkan12Features { /// Indicates whether the implementation supports updating uniform buffer descriptors after a /// set is bound. /// - /// If this feature is not enabled, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT must not be used - /// with VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER. + /// If this feature is not enabled, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT must not be + /// used with VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER. pub descriptor_binding_uniform_buffer_update_after_bind: bool, - /// Indicates whether the implementation supports updating sampled image descriptors after a set - /// is bound. + /// Indicates whether the implementation supports updating sampled image descriptors after a + /// set is bound. /// - /// If this feature is not enabled, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT must not be used - /// with VK_DESCRIPTOR_TYPE_SAMPLER, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, or + /// If this feature is not enabled, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT must not be + /// used with VK_DESCRIPTOR_TYPE_SAMPLER, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, or /// VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE. pub descriptor_binding_sampled_image_update_after_bind: bool, - /// Indicates whether the implementation supports updating storage image descriptors after a set - /// is bound. + /// Indicates whether the implementation supports updating storage image descriptors after a + /// set is bound. /// - /// If this feature is not enabled, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT must not be used - /// with VK_DESCRIPTOR_TYPE_STORAGE_IMAGE. + /// If this feature is not enabled, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT must not be + /// used with VK_DESCRIPTOR_TYPE_STORAGE_IMAGE. pub descriptor_binding_storage_image_update_after_bind: bool, /// Indicates whether the implementation supports updating storage buffer descriptors after a /// set is bound. /// - /// If this feature is not enabled, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT must not be used - /// with VK_DESCRIPTOR_TYPE_STORAGE_BUFFER. + /// If this feature is not enabled, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT must not be + /// used with VK_DESCRIPTOR_TYPE_STORAGE_BUFFER. pub descriptor_binding_storage_buffer_update_after_bind: bool, /// Indicates whether the implementation supports updating uniform texel buffer descriptors /// after a set is bound. /// - /// If this feature is not enabled, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT must not be used - /// with VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER. + /// If this feature is not enabled, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT must not be + /// used with VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER. pub descriptor_binding_uniform_texel_buffer_update_after_bind: bool, /// Indicates whether the implementation supports updating storage texel buffer descriptors /// after a set is bound. /// - /// If this feature is not enabled, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT must not be used - /// with VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER. + /// If this feature is not enabled, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT must not be + /// used with VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER. pub descriptor_binding_storage_texel_buffer_update_after_bind: bool, /// Indicates whether the implementation supports updating descriptors while the set is in use. @@ -1881,8 +2275,8 @@ pub struct Vulkan12Features { pub descriptor_binding_partially_bound: bool, /// Indicates whether the implementation supports descriptor sets with a variable-sized last - /// binding. If this feature is not enabled, VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT - /// must not be used. + /// binding. If this feature is not enabled, + /// VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT must not be used. pub descriptor_binding_variable_descriptor_count: bool, /// Indicates whether the implementation supports the SPIR-V RuntimeDescriptorArray capability. @@ -1911,7 +2305,7 @@ pub struct Vulkan12Features { /// /// See [Standard Buffer Layout]. /// - /// [Standard Buffer Layout]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#interfaces-resources-layout + /// See the [features chapter](https://docs.vulkan.org/spec/latest/chapters/features.html). pub uniform_buffer_standard_layout: bool, /// A boolean specifying whether subgroup operations can use 8-bit integer, 16-bit integer, @@ -1922,8 +2316,8 @@ pub struct Vulkan12Features { /// Indicates whether the implementation supports a VkImageMemoryBarrier for a depth/stencil /// image with only one of VK_IMAGE_ASPECT_DEPTH_BIT or VK_IMAGE_ASPECT_STENCIL_BIT set, and /// whether VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL, - /// VK_IMAGE_LAYOUT_STENCIL_ATTACHMENT_OPTIMAL, or VK_IMAGE_LAYOUT_STENCIL_READ_ONLY_OPTIMAL can - /// be used. + /// VK_IMAGE_LAYOUT_STENCIL_ATTACHMENT_OPTIMAL, or VK_IMAGE_LAYOUT_STENCIL_READ_ONLY_OPTIMAL + /// can be used. pub separate_depth_stencil_layouts: bool, /// Indicates that the implementation supports resetting queries from the host with @@ -1953,7 +2347,7 @@ pub struct Vulkan12Features { /// /// This also indicates whether shader modules can declare the VulkanMemoryModel capability. /// - /// [Vulkan Memory Model]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#memory-model + /// See the [features chapter](https://docs.vulkan.org/spec/latest/chapters/features.html). pub vulkan_memory_model: bool, /// Indicates whether the [Vulkan Memory Model] can use Device scope synchronization. @@ -1961,13 +2355,13 @@ pub struct Vulkan12Features { /// This also indicates whether shader modules can declare the VulkanMemoryModelDeviceScope /// capability. /// - /// [Vulkan Memory Model]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#memory-model + /// See the [features chapter](https://docs.vulkan.org/spec/latest/chapters/features.html). pub vulkan_memory_model_device_scope: bool, /// Indicates whether the [Vulkan Memory Model] can use availability and visibility chains with /// more than one element. /// - /// [Vulkan Memory Model]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#memory-model + /// See the [features chapter](https://docs.vulkan.org/spec/latest/chapters/features.html). pub vulkan_memory_model_availability_visibility_chains: bool, /// Indicates whether the implementation supports the ShaderViewportIndex SPIR-V capability @@ -1979,11 +2373,11 @@ pub struct Vulkan12Features { pub shader_output_viewport_index: bool, /// Indicates whether the implementation supports the ShaderLayer SPIR-V capability enabling - /// variables decorated with the Layer built-in to be exported from mesh, vertex or tessellation - /// evaluation shaders. + /// variables decorated with the Layer built-in to be exported from mesh, vertex or + /// tessellation evaluation shaders. /// - /// If this feature is not enabled, the Layer built-in decoration must not be used on outputs in - /// mesh, vertex or tessellation evaluation shaders. + /// If this feature is not enabled, the Layer built-in decoration must not be used on outputs + /// in mesh, vertex or tessellation evaluation shaders. pub shader_output_layer: bool, /// If `true`, the “Id” operand of OpGroupNonUniformBroadcast can be dynamically uniform within @@ -2092,10 +2486,8 @@ impl From> for Vulkan12Features { /// Description of Vulkan 1.2 properties. /// -/// See -/// [`VkPhysicalDeviceVulkan12Properties`](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceVulkan12Properties.html) -/// manual page. -#[derive(Debug)] +/// See [`VkPhysicalDeviceVulkan12Properties`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPhysicalDeviceVulkan12Properties.html). +#[derive(Clone, Debug)] pub struct Vulkan12Properties { /// A unique identifier for the driver of the physical device. pub driver_id: vk::DriverId, @@ -2109,7 +2501,7 @@ pub struct Vulkan12Properties { pub driver_info: String, /// The version of the Vulkan conformance test this driver is conformant against (see - /// [`VkConformanceVersion`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkConformanceVersion.html)). + /// See the [limits chapter](https://docs.vulkan.org/spec/latest/chapters/limits.html). pub conformance_version: vk::ConformanceVersion, /// A `vk::ShaderFloatControlsIndependence` value indicating whether, and how, denorm behavior @@ -2123,22 +2515,22 @@ pub struct Vulkan12Properties { /// A `bool` value indicating whether sign of a zero, Nans and ±∞ can be preserved in 16-bit /// floating-point computations. /// - /// It also indicates whether the SignedZeroInfNanPreserve execution mode can be used for 16-bit - /// floating-point types. + /// It also indicates whether the SignedZeroInfNanPreserve execution mode can be used for + /// 16-bit floating-point types. pub shader_signed_zero_inf_nan_preserve_float16: bool, /// A `bool` value indicating whether sign of a zero, Nans and ±∞ can be preserved in 32-bit /// floating-point computations. /// - /// It also indicates whether the SignedZeroInfNanPreserve execution mode can be used for 32-bit - /// floating-point types. + /// It also indicates whether the SignedZeroInfNanPreserve execution mode can be used for + /// 32-bit floating-point types. pub shader_signed_zero_inf_nan_preserve_float32: bool, /// A `bool` value indicating whether sign of a zero, Nans and ±∞ can be preserved in 64-bit /// floating-point computations. /// - /// It also indicates whether the SignedZeroInfNanPreserve execution mode can be used for 64-bit - /// floating-point types. + /// It also indicates whether the SignedZeroInfNanPreserve execution mode can be used for + /// 64-bit floating-point types. pub shader_signed_zero_inf_nan_preserve_float64: bool, /// A `bool` value indicating whether denormals can be preserved in 16-bit floating-point @@ -2204,22 +2596,22 @@ pub struct Vulkan12Properties { /// floating-point types. pub shader_rounding_mode_rte_float64: bool, - /// A `bool` value indicating whether an implementation supports the round-towards-zero rounding - /// mode for 16-bit floating-point arithmetic and conversion instructions. + /// A `bool` value indicating whether an implementation supports the round-towards-zero + /// rounding mode for 16-bit floating-point arithmetic and conversion instructions. /// /// It also indicates whether the RoundingModeRTZ execution mode can be used for 16-bit /// floating-point types. pub shader_rounding_mode_rtz_float16: bool, - /// A `bool` value indicating whether an implementation supports the round-towards-zero rounding - /// mode for 32-bit floating-point arithmetic and conversion instructions. + /// A `bool` value indicating whether an implementation supports the round-towards-zero + /// rounding mode for 32-bit floating-point arithmetic and conversion instructions. /// /// It also indicates whether the RoundingModeRTZ execution mode can be used for 32-bit /// floating-point types. pub shader_rounding_mode_rtz_float32: bool, - /// A `bool` value indicating whether an implementation supports the round-towards-zero rounding - /// mode for 64-bit floating-point arithmetic and conversion instructions. + /// A `bool` value indicating whether an implementation supports the round-towards-zero + /// rounding mode for 64-bit floating-point arithmetic and conversion instructions. /// /// It also indicates whether the RoundingModeRTZ execution mode can be used for 64-bit /// floating-point types. @@ -2245,8 +2637,8 @@ pub struct Vulkan12Properties { /// indexing. /// /// If this is `false`, then a single dynamic instance of an instruction that nonuniformly - /// indexes an array of samplers or images may execute multiple times in order to access all the - /// descriptors. + /// indexes an array of samplers or images may execute multiple times in order to access all + /// the descriptors. pub shader_sampled_image_array_non_uniform_indexing_native: bool, /// A `bool` value indicating whether storage buffer descriptors natively support nonuniform @@ -2283,36 +2675,36 @@ pub struct Vulkan12Properties { /// update-after-bind features must be disabled. pub robust_buffer_access_update_after_bind: bool, - /// A `bool` value indicating whether implicit level of detail calculations for image operations - /// have well-defined results when the image and/or sampler objects used for the instruction are - /// not uniform within a quad. + /// A `bool` value indicating whether implicit level of detail calculations for image + /// operations have well-defined results when the image and/or sampler objects used for the + /// instruction are not uniform within a quad. /// - /// See [Derivative Image Operations](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPhysicalDeviceVulkan12Properties.html#textures-derivative-image-operations). + /// See the [features chapter](https://docs.vulkan.org/spec/latest/chapters/features.html). pub quad_divergent_implicit_lod: bool, /// Similar to `maxPerStageDescriptorSamplers` but counts descriptors from descriptor sets - /// created with or without the `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` bit - /// set. + /// created with or without the `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` + /// bit set. pub max_per_stage_descriptor_update_after_bind_samplers: u32, - /// Similar to `maxPerStageDescriptorUniformBuffers` but counts descriptors from descriptor sets - /// created with or without the `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` bit - /// set. + /// Similar to `maxPerStageDescriptorUniformBuffers` but counts descriptors from descriptor + /// sets created with or without the + /// `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` bit set. pub max_per_stage_descriptor_update_after_bind_uniform_buffers: u32, - /// Similar to `maxPerStageDescriptorStorageBuffers` but counts descriptors from descriptor sets - /// created with or without the `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` bit - /// set. + /// Similar to `maxPerStageDescriptorStorageBuffers` but counts descriptors from descriptor + /// sets created with or without the + /// `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` bit set. pub max_per_stage_descriptor_update_after_bind_storage_buffers: u32, /// Similar to `maxPerStageDescriptorSampledImages` but counts descriptors from descriptor sets - /// created with or without the `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` bit - /// set. + /// created with or without the `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` + /// bit set. pub max_per_stage_descriptor_update_after_bind_sampled_images: u32, /// Similar to `maxPerStageDescriptorStorageImages` but counts descriptors from descriptor sets - /// created with or without the `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` bit - /// set. + /// created with or without the `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` + /// bit set. pub max_per_stage_descriptor_update_after_bind_storage_images: u32, /// Similar to `maxPerStageDescriptorInputAttachments` but counts descriptors from descriptor @@ -2329,8 +2721,8 @@ pub struct Vulkan12Properties { pub max_descriptor_set_update_after_bind_samplers: u32, /// Similar to `maxDescriptorSetUniformBuffers` but counts descriptors from descriptor sets - /// created with or without the `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` bit - /// set. + /// created with or without the `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` + /// bit set. pub max_descriptor_set_update_after_bind_uniform_buffers: u32, /// Similar to `maxDescriptorSetUniformBuffersDynamic` but counts descriptors from descriptor @@ -2344,8 +2736,8 @@ pub struct Vulkan12Properties { pub max_descriptor_set_update_after_bind_uniform_buffers_dynamic: u32, /// Similar to `maxDescriptorSetStorageBuffers` but counts descriptors from descriptor sets - /// created with or without the `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` bit - /// set. + /// created with or without the `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` + /// bit set. pub max_descriptor_set_update_after_bind_storage_buffers: u32, /// Similar to `maxDescriptorSetStorageBuffersDynamic` but counts descriptors from descriptor @@ -2359,18 +2751,18 @@ pub struct Vulkan12Properties { pub max_descriptor_set_update_after_bind_storage_buffers_dynamic: u32, /// Similar to `maxDescriptorSetSampledImages` but counts descriptors from descriptor sets - /// created with or without the `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` bit - /// set. + /// created with or without the `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` + /// bit set. pub max_descriptor_set_update_after_bind_sampled_images: u32, /// Similar to `maxDescriptorSetStorageImages` but counts descriptors from descriptor sets - /// created with or without the `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` bit - /// set. + /// created with or without the `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` + /// bit set. pub max_descriptor_set_update_after_bind_storage_images: u32, /// Similar to `maxDescriptorSetInputAttachments` but counts descriptors from descriptor sets - /// created with or without the `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` bit - /// set. + /// created with or without the `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` + /// bit set. pub max_descriptor_set_update_after_bind_input_attachments: u32, /// A bitmask of `vk::ResolveModeFlagBits` indicating the set of supported depth resolve modes. @@ -2404,16 +2796,16 @@ pub struct Vulkan12Properties { /// filtering. pub filter_minmax_single_component_formats: bool, - /// A `bool` value indicating whether the implementation supports non-identity component mapping - /// of the image when doing min/max filtering. + /// A `bool` value indicating whether the implementation supports non-identity component + /// mapping of the image when doing min/max filtering. pub filter_minmax_image_component_mapping: bool, /// Indicates the maximum difference allowed by the implementation between the current value of /// a timeline semaphore and any pending signal or wait operations. pub max_timeline_semaphore_value_difference: u64, - /// A bitmask of `vk::SampleCountFlagBits` indicating the color sample counts that are supported - /// for all framebuffer color attachments with integer formats. + /// A bitmask of `vk::SampleCountFlagBits` indicating the color sample counts that are + /// supported for all framebuffer color attachments with integer formats. pub framebuffer_integer_color_sample_counts: vk::SampleCountFlags, } diff --git a/src/driver/ray_trace.rs b/src/driver/ray_trace.rs index 9c309780..6308541f 100644 --- a/src/driver/ray_trace.rs +++ b/src/driver/ray_trace.rs @@ -6,46 +6,26 @@ use { device::Device, merge_push_constant_ranges, physical_device::RayTraceProperties, - shader::{DescriptorBindingMap, PipelineDescriptorInfo, Shader, align_spriv}, + shader::{DescriptorBindingMap, PipelineDescriptorInfo, Shader}, }, ash::vk, derive_builder::{Builder, UninitializedFieldError}, log::warn, - std::{ffi::CString, ops::Deref, sync::Arc, thread::panicking}, + std::{ + ffi::CString, + hash::{Hash, Hasher}, + sync::{Arc, OnceLock}, + thread::panicking, + }, }; -/// Smart pointer handle to a [pipeline] object. +/// Smart pointer handle of a pipeline object. /// /// Also contains information about the object. -/// -/// ## `Deref` behavior -/// -/// `RayTracePipeline` automatically dereferences to [`vk::Pipeline`] (via the [`Deref`] -/// trait), so you can call `vk::Pipeline`'s methods on a value of type `RayTracePipeline`. To avoid -/// name clashes with `vk::Pipeline`'s methods, the methods of `RayTracePipeline` itself are -/// associated functions, called using [fully qualified syntax]: -/// -/// [pipeline]: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPipeline.html -/// [deref]: core::ops::Deref -/// [fully qualified syntax]: https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#fully-qualified-syntax-for-disambiguation-calling-methods-with-the-same-name -#[derive(Debug)] +#[derive(Clone, Debug)] +#[read_only::cast] pub struct RayTracePipeline { - pub(crate) descriptor_bindings: DescriptorBindingMap, - pub(crate) descriptor_info: PipelineDescriptorInfo, - device: Arc, - - /// Information used to create this object. - pub info: RayTracePipelineInfo, - - pub(crate) layout: vk::PipelineLayout, - - /// A descriptive name used in debugging messages. - pub name: Option, - - pub(crate) push_constants: Vec, - pipeline: vk::Pipeline, - shader_modules: Vec, - shader_group_handles: Vec, + pub(crate) inner: Arc, } impl RayTracePipeline { @@ -68,12 +48,16 @@ impl RayTracePipeline { /// ```no_run /// # use std::sync::Arc; /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::ray_trace::{RayTracePipeline, RayTracePipelineInfo, RayTraceShaderGroup}; - /// # use screen_13::driver::shader::Shader; + /// # use vk_graph::driver::DriverError; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::ray_trace::{ + /// # RayTracePipeline, + /// # RayTracePipelineInfo, + /// # RayTraceShaderGroup, + /// # }; + /// # use vk_graph::driver::shader::Shader; /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); + /// # let device = Device::new(DeviceInfo::default())?; /// # let my_rgen_code = [0u8; 1]; /// # let my_chit_code = [0u8; 1]; /// # let my_miss_code = [0u8; 1]; @@ -97,13 +81,13 @@ impl RayTracePipeline { /// ], /// )?; /// - /// assert_ne!(*pipeline, vk::Pipeline::null()); - /// assert_eq!(pipeline.info.max_ray_recursion_depth, 1); + /// assert_ne!(pipeline.handle(), vk::Pipeline::null()); + /// assert_eq!(pipeline.info().max_ray_recursion_depth, 1); /// # Ok(()) } /// ``` #[profiling::function] pub fn create( - device: &Arc, + device: &Device, info: impl Into, shaders: impl IntoIterator, shader_groups: impl IntoIterator, @@ -111,6 +95,12 @@ impl RayTracePipeline { where S: Into, { + if device.physical_device.ray_trace_properties.is_none() { + warn!("unsupported ray trace pipeline creation: missing ray trace properties"); + + return Err(DriverError::Unsupported); + } + let info = info.into(); let shader_groups = shader_groups .into_iter() @@ -139,22 +129,22 @@ impl RayTracePipeline { } let descriptor_info = PipelineDescriptorInfo::create(device, &descriptor_bindings)?; - let descriptor_set_layout_handles = descriptor_info + let layouts = descriptor_info .layouts .values() - .map(|descriptor_set_layout| **descriptor_set_layout) - .collect::>(); + .map(|layout| layout.handle) + .collect::>(); unsafe { let layout = device .create_pipeline_layout( &vk::PipelineLayoutCreateInfo::default() - .set_layouts(&descriptor_set_layout_handles) + .set_layouts(&layouts) .push_constant_ranges(&push_constants), None, ) .map_err(|err| { - warn!("{err}"); + warn!("unable to create ray trace pipeline layout: {err}"); DriverError::Unsupported })?; @@ -163,19 +153,13 @@ impl RayTracePipeline { .map(|shader| CString::new(shader.entry_name.as_str())) .collect::>() .map_err(|err| { - warn!("{err}"); + warn!("invalid ray trace shader entry name: {err}"); DriverError::InvalidData })?; let specialization_infos: Box<[Option]> = shaders .iter() - .map(|shader| { - shader.specialization_info.as_ref().map(|info| { - vk::SpecializationInfo::default() - .data(&info.data) - .map_entries(&info.map_entries) - }) - }) + .map(|shader| shader.specialization.as_ref().map(Into::into)) .collect(); let mut shader_stages: Vec = Vec::with_capacity(shaders.len()); @@ -183,11 +167,11 @@ impl RayTracePipeline { for (idx, shader) in shaders.iter().enumerate() { let module = device .create_shader_module( - &vk::ShaderModuleCreateInfo::default().code(align_spriv(&shader.spirv)?), + &vk::ShaderModuleCreateInfo::default().code(shader.spirv.words()), None, ) .map_err(|err| { - warn!("{err}"); + warn!("unable to create ray trace shader module: {err}"); device.destroy_pipeline_layout(layout, None); @@ -218,118 +202,111 @@ impl RayTracePipeline { dynamic_states.push(vk::DynamicState::RAY_TRACING_PIPELINE_STACK_SIZE_KHR); } - let ray_trace_ext = device - .ray_trace_ext - .as_ref() - .ok_or(DriverError::Unsupported)?; - let pipeline = ray_trace_ext - .create_ray_tracing_pipelines( + let ray_trace_ext = Device::expect_ray_trace_ext(device); + let handle = + ray_trace_ext.create_ray_tracing_pipelines( vk::DeferredOperationKHR::null(), Device::pipeline_cache(device), &[vk::RayTracingPipelineCreateInfoKHR::default() .stages(&shader_stages) .groups(&shader_groups) - .max_pipeline_ray_recursion_depth( - info.max_ray_recursion_depth.min( - device - .physical_device - .ray_trace_properties - .as_ref() - .unwrap() - .max_ray_recursion_depth, - ), - ) + .max_pipeline_ray_recursion_depth(info.max_ray_recursion_depth.min( + Device::expect_ray_trace_properties(device).max_ray_recursion_depth, + )) .layout(layout) .dynamic_state( &vk::PipelineDynamicStateCreateInfo::default() .dynamic_states(&dynamic_states), )], None, - ) - .map_err(|(pipelines, err)| { - warn!("{err}"); + ); - for pipeline in pipelines { - device.destroy_pipeline(pipeline, None); - } + for shader_module in shader_modules.iter().copied() { + device.destroy_shader_module(shader_module, None); + } + + let handle = handle.map_err(|(pipelines, err)| { + warn!("unable to create ray trace pipeline: {err}"); - device.destroy_pipeline_layout(layout, None); + for pipeline in pipelines { + device.destroy_pipeline(pipeline, None); + } - for shader_module in shader_modules.iter().copied() { - device.destroy_shader_module(shader_module, None); - } + device.destroy_pipeline_layout(layout, None); - DriverError::Unsupported - })?[0]; - let device = Arc::clone(device); + DriverError::Unsupported + })?[0]; let &RayTraceProperties { shader_group_handle_size, .. - } = device - .physical_device - .ray_trace_properties - .as_ref() - .unwrap(); + } = Device::expect_ray_trace_properties(device); - let push_constants = merge_push_constant_ranges(&push_constants); + let push_constants = merge_push_constant_ranges(&push_constants).into_boxed_slice(); // SAFETY: - // According to [vulkan spec](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/vkGetRayTracingShaderGroupHandlesKHR.html) + // See [`vkGetRayTracingShaderGroupHandlesKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkGetRayTracingShaderGroupHandlesKHR.html). // Valid usage of this function requires: // 1. pipeline must be raytracing pipeline. // 2. first_group must be less than the number of shader groups in the pipeline. - // 3. the sum of first group and group_count must be less or equal to the number of shader - // modules in the pipeline. + // 3. the sum of first group and group_count must be less or equal to the number of + // shader modules in the pipeline. // 4. data_size must be at least shader_group_handle_size * group_count. // 5. pipeline must not have been created with VK_PIPELINE_CREATE_LIBRARY_BIT_KHR. // let shader_group_handles = { ray_trace_ext.get_ray_tracing_shader_group_handles( - pipeline, + handle, 0, group_count as u32, group_count * shader_group_handle_size as usize, ) } - .map_err(|_| DriverError::InvalidData)?; + .map_err(|_| DriverError::InvalidData)? + .into_boxed_slice(); Ok(Self { - descriptor_bindings, - descriptor_info, - device, - info, - layout, - name: None, - push_constants, - pipeline, - shader_modules, - shader_group_handles, + inner: Arc::new(RayTracePipelineInner { + descriptor_bindings, + descriptor_info, + device: device.clone(), + handle, + info, + layout, + name: Default::default(), + push_constants, + shader_group_handles, + }), }) } } + /// Gets the debugging name assigned to this pipeline, if one has been set. + pub fn debug_name(&self) -> Option<&str> { + self.inner.name.get().map(String::as_str) + } + + /// The device which owns this ray trace pipeline. + pub fn device(&self) -> &Device { + &self.inner.device + } + /// Function returning a handle to a shader group of this pipeline. /// This can be used to construct a sbt. /// /// # Examples /// /// See - /// [ray_trace.rs](https://github.com/attackgoat/screen-13/blob/master/examples/ray_trace.rs) + /// [ray_trace.rs](https://github.com/attackgoat/vk-graph/blob/master/examples/ray_trace.rs) /// for a detail example which constructs a shader binding table buffer using this function. - pub fn group_handle(this: &Self, idx: usize) -> Result<&[u8], DriverError> { + pub fn group_handle(&self, idx: usize) -> &[u8] { let &RayTraceProperties { shader_group_handle_size, .. - } = this - .device - .physical_device - .ray_trace_properties - .as_ref() - .ok_or(DriverError::Unsupported)?; + } = Device::expect_ray_trace_properties(&self.inner.device); let start = idx * shader_group_handle_size as usize; let end = start + shader_group_handle_size as usize; - Ok(&this.shader_group_handles[start..end]) + &self.inner.shader_group_handles[start..end] } /// Query ray trace pipeline shader group shader stack size. @@ -338,67 +315,77 @@ impl RayTracePipeline { /// called from the specified shader group. #[profiling::function] pub fn group_stack_size( - this: &Self, + &self, group: u32, group_shader: vk::ShaderGroupShaderKHR, ) -> vk::DeviceSize { unsafe { // Safely use unchecked because ray_trace_ext is checked during pipeline creation - this.device - .ray_trace_ext - .as_ref() - .unwrap_unchecked() - .get_ray_tracing_shader_group_stack_size(this.pipeline, group, group_shader) + Device::expect_ray_trace_ext(&self.inner.device) + .get_ray_tracing_shader_group_stack_size(self.handle(), group, group_shader) } } - /// Sets the debugging name assigned to this pipeline. - pub fn with_name(mut this: Self, name: impl Into) -> Self { - this.name = Some(name.into()); - this + /// The native Vulkan pipeline handle of this ray trace pipeline. + pub fn handle(&self) -> vk::Pipeline { + self.inner.handle } -} -impl Deref for RayTracePipeline { - type Target = vk::Pipeline; + /// Gets the information used to create this object. + pub fn info(&self) -> RayTracePipelineInfo { + self.inner.info + } - fn deref(&self) -> &Self::Target { - &self.pipeline + /// Gets the debugging name assigned to this pipeline, if one has been set. + pub fn name(&self) -> Option<&str> { + self.inner.name.get().map(String::as_str) } -} -impl Drop for RayTracePipeline { - #[profiling::function] - fn drop(&mut self) { - if panicking() { + /// Sets the debugging name assigned to this pipeline. + /// + /// _Note:_ The pipeline name may only be assigned once. Subsequent calls will not update the + /// previously set name value. + pub fn set_debug_name(&mut self, name: impl Into) { + if !self.inner.device.physical_device.instance.info.debug { return; } - unsafe { - self.device.destroy_pipeline(self.pipeline, None); - self.device.destroy_pipeline_layout(self.layout, None); - } + // Both Ok and Err are valid conditions + let _ = self.inner.name.set(name.into()); + } - for shader_module in self.shader_modules.drain(..) { - unsafe { - self.device.destroy_shader_module(shader_module, None); - } - } + /// Sets the debugging name assigned to this pipeline. + /// + /// _Note:_ The pipeline name may only be assigned once. Subsequent calls will not update the + /// previously set name value. + pub fn with_debug_name(mut self, name: impl Into) -> Self { + self.set_debug_name(name); + + self + } +} + +impl Eq for RayTracePipeline {} + +impl Hash for RayTracePipeline { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.inner).hash(state); + } +} + +impl PartialEq for RayTracePipeline { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.inner, &other.inner) } } /// Information used to create a [`RayTracePipeline`] instance. #[derive(Builder, Clone, Copy, Debug, Eq, Hash, PartialEq)] #[builder( - build_fn( - private, - name = "fallible_build", - error = "RayTracePipelineInfoBuilderError" - ), + build_fn(private, name = "fallible_build", error = "UninitializedFieldError"), derive(Clone, Copy, Debug), pattern = "owned" )] -#[non_exhaustive] pub struct RayTracePipelineInfo { /// The number of descriptors to allocate for a given binding when using bindless (unbounded) /// syntax. @@ -410,17 +397,17 @@ pub struct RayTracePipelineInfo { /// Basic usage (GLSL): /// /// ``` - /// # inline_spirv::inline_spirv!(r#" + /// # vk_shader_macros::glsl!(target: vulkan1_2, r#" /// #version 460 core /// #extension GL_EXT_nonuniform_qualifier : require + /// #pragma shader_stage(closest) /// /// layout(set = 0, binding = 0, rgba8) readonly uniform image2D my_binding[]; /// - /// void main() - /// { + /// void main() { /// // my_binding will have space for 8,192 images by default /// } - /// # "#, rchit, vulkan1_2); + /// # "#); /// ``` #[builder(default = "8192")] pub bindless_descriptor_count: u32, @@ -430,7 +417,7 @@ pub struct RayTracePipelineInfo { /// When set, you must manually set the stack size during ray trace passes using /// [`RayTrace::set_stack_size`](crate::graph::pass_ref::RayTrace::set_stack_size). /// - /// [setting the stack size dynamically]: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/vkCmdSetRayTracingPipelineStackSizeKHR.html + /// See [`vkCmdSetRayTracingPipelineStackSizeKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/vkCmdSetRayTracingPipelineStackSizeKHR.html). #[builder(default)] pub dynamic_stack_size: bool, @@ -438,7 +425,7 @@ pub struct RayTracePipelineInfo { /// /// The default is `16`. /// - /// [maximum recursion depth]: https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#ray-tracing-recursion-depth + /// See [`VkRayTracingPipelineCreateInfoKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkRayTracingPipelineCreateInfoKHR.html). #[builder(default = "16")] pub max_ray_recursion_depth: u32, } @@ -450,14 +437,19 @@ impl RayTracePipelineInfo { } /// Converts a `RayTracePipelineInfo` into a `RayTracePipelineInfoBuilder`. - #[inline(always)] - pub fn to_builder(self) -> RayTracePipelineInfoBuilder { + pub fn into_builder(self) -> RayTracePipelineInfoBuilder { RayTracePipelineInfoBuilder { bindless_descriptor_count: Some(self.bindless_descriptor_count), dynamic_stack_size: Some(self.dynamic_stack_size), max_ray_recursion_depth: Some(self.max_ray_recursion_depth), } } + + #[deprecated = "use into_builder function"] + #[doc(hidden)] + pub fn to_builder(self) -> RayTracePipelineInfoBuilder { + self.into_builder() + } } impl Default for RayTracePipelineInfo { @@ -480,32 +472,42 @@ impl RayTracePipelineInfoBuilder { /// Builds a new `RayTracePipelineInfo`. #[inline(always)] pub fn build(self) -> RayTracePipelineInfo { - let res = self.fallible_build(); - - #[cfg(test)] - let res = res.unwrap(); - - #[cfg(not(test))] - let res = unsafe { res.unwrap_unchecked() }; - - res + self.fallible_build() + .expect("invalid ray trace pipeline info") } } #[derive(Debug)] -struct RayTracePipelineInfoBuilderError; +pub(crate) struct RayTracePipelineInner { + pub descriptor_bindings: DescriptorBindingMap, + pub descriptor_info: PipelineDescriptorInfo, + pub device: Device, + pub handle: vk::Pipeline, + pub info: RayTracePipelineInfo, + pub layout: vk::PipelineLayout, + pub name: OnceLock, + pub push_constants: Box<[vk::PushConstantRange]>, + pub shader_group_handles: Box<[u8]>, +} + +impl Drop for RayTracePipelineInner { + #[profiling::function] + fn drop(&mut self) { + if panicking() { + return; + } -impl From for RayTracePipelineInfoBuilderError { - fn from(_: UninitializedFieldError) -> Self { - Self + unsafe { + self.device.destroy_pipeline(self.handle, None); + self.device.destroy_pipeline_layout(self.layout, None); + } } } /// Describes the set of the shader stages to be included in each shader group in the ray trace /// pipeline. /// -/// See -/// [VkRayTracingShaderGroupCreateInfoKHR](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#VkRayTracingShaderGroupCreateInfoKHR). +/// See [`VkRayTracingShaderGroupCreateInfoKHR`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkRayTracingShaderGroupCreateInfoKHR.html). #[derive(Clone, Copy, Debug)] pub struct RayTraceShaderGroup { /// The optional index of the any-hit shader in the group if the shader group has type of @@ -579,8 +581,8 @@ impl RayTraceShaderGroup { ) } - /// Creates a new triangles-type shader group with the given closest-hit shader and optional any-hit - /// shader. + /// Creates a new triangles-type shader group with the given closest-hit shader and optional + /// any-hit shader. pub fn new_triangles(closest_hit_shader: u32, any_hit_shader: impl Into>) -> Self { Self::new( RayTraceShaderGroupType::TrianglesHitGroup, @@ -639,8 +641,20 @@ impl From for vk::RayTracingShaderGroupTypeKHR { } } +mod deprecated { + use crate::driver::ray_trace::RayTracePipeline; + + impl RayTracePipeline { + #[deprecated = "use with_debug_name function"] + #[doc(hidden)] + pub fn with_name(this: Self, name: impl Into) -> Self { + this.with_debug_name(name) + } + } +} + #[cfg(test)] -mod tests { +mod test { use super::*; type Info = RayTracePipelineInfo; @@ -649,7 +663,7 @@ mod tests { #[test] pub fn ray_trace_pipeline_info() { let info = Info::default(); - let builder = info.to_builder().build(); + let builder = info.into_builder().build(); assert_eq!(info, builder); } diff --git a/src/driver/render_pass.rs b/src/driver/render_pass.rs index 239026df..29f76f8b 100644 --- a/src/driver/render_pass.rs +++ b/src/driver/render_pass.rs @@ -1,13 +1,17 @@ //! Render pass related types. use { - super::{DepthStencilMode, DriverError, GraphicPipeline, SampleCount, device::Device}, + super::{ + DriverError, + device::Device, + graphic::{DepthStencilInfo, GraphicPipeline}, + image::SampleCount, + }, ash::vk, log::{trace, warn}, std::{ collections::{HashMap, hash_map::Entry}, - ops::Deref, - sync::Arc, + slice, thread::panicking, }, }; @@ -89,41 +93,49 @@ pub(crate) struct FramebufferInfo { #[derive(Debug, Eq, Hash, PartialEq)] struct GraphicPipelineKey { - depth_stencil: Option, + depth_stencil: Option, layout: vk::PipelineLayout, - shader_modules: Vec, subpass_idx: u32, } -#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] -pub(crate) struct RenderPassInfo { - pub attachments: Vec, - pub subpasses: Vec, - pub dependencies: Vec, -} - +/// Vulkan render pass state and cached framebuffer/pipeline objects for compatible attachments. #[derive(Debug)] -pub(crate) struct RenderPass { - device: Arc, +#[read_only::cast] +pub struct RenderPass { + /// The device which owns this render pass resource. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub device: Device, + framebuffers: HashMap, graphic_pipelines: HashMap, + + /// The native Vulkan resource handle of this render pass. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub handle: vk::RenderPass, + + /// Information used to create this render pass resource. + /// + /// _Note:_ This field is read-only. + #[readonly] pub info: RenderPassInfo, - render_pass: vk::RenderPass, } impl RenderPass { #[profiling::function] - pub fn create(device: &Arc, info: RenderPassInfo) -> Result { - //trace!("create: \n{:#?}", &info); + pub(crate) fn create(device: &Device, info: RenderPassInfo) -> Result { trace!("create"); - let device = Arc::clone(device); + let device = device.clone(); let attachments = info .attachments .iter() .copied() .map(Into::into) - .collect::>(); + .collect::>(); let correlated_view_masks = if info.subpasses.iter().any(|subpass| subpass.view_mask != 0) { { info.subpasses @@ -139,7 +151,7 @@ impl RenderPass { .iter() .copied() .map(Into::into) - .collect::>(); + .collect::>(); let subpass_attachments = info .subpasses @@ -169,7 +181,6 @@ impl RenderPass { subpass.depth_stencil_resolve_attachment.map( |(_, depth_resolve_mode, stencil_resolve_mode)| { vk::SubpassDescriptionDepthStencilResolve::default() - .depth_stencil_resolve_attachment(subpass_attachments.last().unwrap()) .depth_resolve_mode( depth_resolve_mode.map(Into::into).unwrap_or_default(), ) @@ -210,6 +221,9 @@ impl RenderPass { } if let Some(depth_stencil_resolve) = depth_stencil_resolve { + *depth_stencil_resolve = depth_stencil_resolve.depth_stencil_resolve_attachment( + &subpass_attachments[depth_stencil_resolve_idx], + ); desc = desc.push_next(depth_stencil_resolve); } @@ -222,40 +236,39 @@ impl RenderPass { ); } - let render_pass = unsafe { - device - .create_render_pass2( - &vk::RenderPassCreateInfo2::default() - .attachments(&attachments) - .correlated_view_masks(&correlated_view_masks) - .dependencies(&dependencies) - .subpasses(&subpasses), - None, - ) - .map_err(|err| { - warn!("{err}"); + let handle = unsafe { + device.create_render_pass2( + &vk::RenderPassCreateInfo2::default() + .attachments(&attachments) + .correlated_view_masks(&correlated_view_masks) + .dependencies(&dependencies) + .subpasses(&subpasses), + None, + ) + } + .map_err(|err| { + warn!("unable to create render pass: {err}"); - DriverError::Unsupported - })? - }; + DriverError::Unsupported + })?; Ok(Self { - info, device, framebuffers: Default::default(), graphic_pipelines: Default::default(), - render_pass, + handle, + info, }) } #[profiling::function] - pub fn framebuffer( - this: &mut Self, + pub(crate) fn framebuffer( + &mut self, info: FramebufferInfo, ) -> Result { debug_assert!(!info.attachments.is_empty()); - let entry = this.framebuffers.entry(info); + let entry = self.framebuffers.entry(info); if let Entry::Occupied(entry) = entry { return Ok(*entry.get()); } @@ -284,27 +297,24 @@ impl RenderPass { .usage(attachment.usage) .view_formats(&attachment.view_formats) }) - .collect::>(); + .collect::>(); let mut imageless_info = vk::FramebufferAttachmentsCreateInfoKHR::default().attachment_image_infos(&attachments); let mut create_info = vk::FramebufferCreateInfo::default() .flags(vk::FramebufferCreateFlags::IMAGELESS) - .render_pass(this.render_pass) + .render_pass(self.handle) .width(attachments[0].width) .height(attachments[0].height) .layers(layers) .push_next(&mut imageless_info); - create_info.attachment_count = this.info.attachments.len() as _; + create_info.attachment_count = self.info.attachments.len() as _; - let framebuffer = unsafe { - this.device - .create_framebuffer(&create_info, None) - .map_err(|err| { - warn!("{err}"); + let framebuffer = + unsafe { self.device.create_framebuffer(&create_info, None) }.map_err(|err| { + warn!("unable to create framebuffer: {err}"); - DriverError::Unsupported - })? - }; + DriverError::Unsupported + })?; entry.insert(framebuffer); @@ -312,18 +322,15 @@ impl RenderPass { } #[profiling::function] - pub fn graphic_pipeline( - this: &mut Self, - pipeline: &Arc, - depth_stencil: Option, + pub(crate) fn pipeline_handle( + &mut self, + pipeline: &GraphicPipeline, + depth_stencil: Option, subpass_idx: u32, ) -> Result { - use std::slice::from_ref; - - let entry = this.graphic_pipelines.entry(GraphicPipelineKey { + let entry = self.graphic_pipelines.entry(GraphicPipelineKey { depth_stencil, - layout: pipeline.layout, - shader_modules: pipeline.shader_modules.clone(), + layout: pipeline.inner.layout, subpass_idx, }); if let Entry::Occupied(entry) = entry { @@ -335,42 +342,32 @@ impl RenderPass { _ => unreachable!(), }; - let color_blend_attachment_states = this.info.subpasses[subpass_idx as usize] + let color_blend_attachment_states = self.info.subpasses[subpass_idx as usize] .color_attachments .iter() - .map(|_| pipeline.info.blend.into()) - .collect::>(); + .map(|_| pipeline.inner.info.blend.into()) + .collect::>(); let color_blend_state = vk::PipelineColorBlendStateCreateInfo::default() .attachments(&color_blend_attachment_states); - let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR]; - let dynamic_state = - vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states); + let dynamic_state = vk::PipelineDynamicStateCreateInfo::default() + .dynamic_states(&[vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR]); let multisample_state = vk::PipelineMultisampleStateCreateInfo::default() - .alpha_to_coverage_enable(pipeline.state.multisample.alpha_to_coverage_enable) - .alpha_to_one_enable(pipeline.state.multisample.alpha_to_one_enable) - .flags(pipeline.state.multisample.flags) - .min_sample_shading(pipeline.state.multisample.min_sample_shading) - .rasterization_samples(pipeline.state.multisample.rasterization_samples.into()) - .sample_shading_enable(pipeline.state.multisample.sample_shading_enable) - .sample_mask(&pipeline.state.multisample.sample_mask); + .alpha_to_coverage_enable(pipeline.inner.multisample.alpha_to_coverage_enable) + .alpha_to_one_enable(pipeline.inner.multisample.alpha_to_one_enable) + .flags(pipeline.inner.multisample.flags) + .min_sample_shading(pipeline.inner.multisample.min_sample_shading) + .rasterization_samples(pipeline.inner.multisample.rasterization_samples.into()) + .sample_shading_enable(pipeline.inner.multisample.sample_shading_enable) + .sample_mask(&pipeline.inner.multisample.sample_mask); let specializations = pipeline - .state - .stages + .inner + .shader_stages .iter() - .map(|stage| { - stage - .specialization_info - .as_ref() - .map(|specialization_info| { - vk::SpecializationInfo::default() - .map_entries(&specialization_info.map_entries) - .data(&specialization_info.data) - }) - }) + .map(|stage| stage.specialization.as_ref().map(Into::into)) .collect::>(); let stages = pipeline - .state - .stages + .inner + .shader_stages .iter() .zip(specializations.iter()) .map(|(stage, specialization)| { @@ -385,53 +382,50 @@ impl RenderPass { info }) - .collect::>(); + .collect::>(); let vertex_input_state = vk::PipelineVertexInputStateCreateInfo::default() .vertex_attribute_descriptions( - &pipeline.state.vertex_input.vertex_attribute_descriptions, + &pipeline.inner.vertex_input.vertex_attribute_descriptions, ) - .vertex_binding_descriptions(&pipeline.state.vertex_input.vertex_binding_descriptions); + .vertex_binding_descriptions(&pipeline.inner.vertex_input.vertex_binding_descriptions); let viewport_state = vk::PipelineViewportStateCreateInfo::default() .viewport_count(1) .scissor_count(1); let input_assembly_state = vk::PipelineInputAssemblyStateCreateInfo { - topology: pipeline.info.topology, + topology: pipeline.inner.info.topology, ..Default::default() }; let depth_stencil = depth_stencil.map(Into::into).unwrap_or_default(); let rasterization_state = vk::PipelineRasterizationStateCreateInfo { - front_face: pipeline.info.front_face, + front_face: pipeline.inner.info.front_face, line_width: 1.0, - polygon_mode: pipeline.info.polygon_mode, - cull_mode: pipeline.info.cull_mode, + polygon_mode: pipeline.inner.info.polygon_mode, + cull_mode: pipeline.inner.info.cull_mode, ..Default::default() }; - let graphic_pipeline_info = vk::GraphicsPipelineCreateInfo::default() + let create_info = vk::GraphicsPipelineCreateInfo::default() .color_blend_state(&color_blend_state) .depth_stencil_state(&depth_stencil) .dynamic_state(&dynamic_state) .input_assembly_state(&input_assembly_state) - .layout(pipeline.state.layout) + .layout(pipeline.inner.layout) .multisample_state(&multisample_state) .rasterization_state(&rasterization_state) - .render_pass(this.render_pass) + .render_pass(self.handle) .stages(&stages) .subpass(subpass_idx) .vertex_input_state(&vertex_input_state) .viewport_state(&viewport_state); let pipeline = unsafe { - this.device.create_graphics_pipelines( - Device::pipeline_cache(&this.device), - from_ref(&graphic_pipeline_info), + self.device.create_graphics_pipelines( + Device::pipeline_cache(&self.device), + slice::from_ref(&create_info), None, ) } .map_err(|(_, err)| { - warn!( - "create_graphics_pipelines: {err}\n{:#?}", - graphic_pipeline_info - ); + warn!("create_graphics_pipelines: {err}\n{:#?}", create_info); DriverError::Unsupported })?[0]; @@ -442,14 +436,6 @@ impl RenderPass { } } -impl Deref for RenderPass { - type Target = vk::RenderPass; - - fn deref(&self) -> &Self::Target { - &self.render_pass - } -} - impl Drop for RenderPass { #[profiling::function] fn drop(&mut self) { @@ -457,20 +443,32 @@ impl Drop for RenderPass { return; } - unsafe { - for (_, framebuffer) in self.framebuffers.drain() { + for (_, framebuffer) in self.framebuffers.drain() { + unsafe { self.device.destroy_framebuffer(framebuffer, None); } + } - for (_, pipeline) in self.graphic_pipelines.drain() { + for (_, pipeline) in self.graphic_pipelines.drain() { + unsafe { self.device.destroy_pipeline(pipeline, None); } + } - self.device.destroy_render_pass(self.render_pass, None); + unsafe { + self.device.destroy_render_pass(self.handle, None); } } } +/// Attachment, subpass, and dependency information used to create a [`RenderPass`]. +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +pub struct RenderPassInfo { + pub(crate) attachments: Vec, + pub(crate) subpasses: Vec, + pub(crate) dependencies: Vec, +} + /// Specifying depth and stencil resolve modes. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum ResolveMode { diff --git a/src/driver/shader.rs b/src/driver/shader.rs index 30decda5..51e00019 100644 --- a/src/driver/shader.rs +++ b/src/driver/shader.rs @@ -9,6 +9,8 @@ use { spirq::{ ReflectConfig, entry_point::EntryPoint, + parse::SpirvBinary, + spirv::ExecutionModel, ty::{DescriptorType, ScalarType, Type, VectorType}, var::Variable, }, @@ -16,26 +18,17 @@ use { collections::{BTreeMap, HashMap}, fmt::{Debug, Formatter}, iter::repeat_n, - mem::size_of_val, ops::Deref, - sync::Arc, thread::panicking, }, }; -pub(crate) type DescriptorBindingMap = HashMap; - -pub(crate) fn align_spriv(code: &[u8]) -> Result<&[u32], DriverError> { - let (prefix, code, suffix) = unsafe { code.align_to() }; - - if prefix.len() + suffix.len() == 0 { - Ok(code) - } else { - warn!("Invalid SPIR-V code"); +#[allow(deprecated)] +#[deprecated = "use SpecializationMap struct"] +#[doc(hidden)] +pub type SpecializationInfo = self::deprecated::SpecializationInfo; - Err(DriverError::InvalidData) - } -} +pub(crate) type DescriptorBindingMap = HashMap; #[profiling::function] fn guess_immutable_sampler(binding_name: &str) -> SamplerInfo { @@ -198,7 +191,7 @@ pub(crate) struct PipelineDescriptorInfo { impl PipelineDescriptorInfo { #[profiling::function] pub fn create( - device: &Arc, + device: &Device, descriptor_bindings: &DescriptorBindingMap, ) -> Result { let descriptor_set_count = descriptor_bindings @@ -295,7 +288,7 @@ impl PipelineDescriptorInfo { let mut create_info = vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings); // The bindless flags have to be created for every descriptor set layout binding. - // [vulkan spec](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkDescriptorSetLayoutBindingFlagsCreateInfo.html) + // See [`VkDescriptorSetLayoutBindingFlagsCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkDescriptorSetLayoutBindingFlagsCreateInfo.html). // Maybe using one vector and updating it would be more efficient. let bindless_flags = vec![vk::DescriptorBindingFlags::PARTIALLY_BOUND; bindings.len()]; let mut bindless_flags = if device @@ -337,14 +330,14 @@ impl PipelineDescriptorInfo { } pub(crate) struct Sampler { - device: Arc, + device: Device, sampler: vk::Sampler, } impl Sampler { #[profiling::function] - pub fn create(device: &Arc, info: impl Into) -> Result { - let device = Arc::clone(device); + pub fn create(device: &Device, info: impl Into) -> Result { + let device = device.clone(); let info = info.into(); let sampler = unsafe { @@ -373,13 +366,15 @@ impl Sampler { ), None, ) - .map_err(|err| { - warn!("{err}"); - - match err { - vk::Result::ERROR_OUT_OF_HOST_MEMORY - | vk::Result::ERROR_OUT_OF_DEVICE_MEMORY => DriverError::OutOfMemory, - _ => DriverError::Unsupported, + .map_err(|err| match err { + vk::Result::ERROR_OUT_OF_HOST_MEMORY + | vk::Result::ERROR_OUT_OF_DEVICE_MEMORY => { + warn!("unable to create sampler: {err}"); + DriverError::OutOfMemory + } + _ => { + warn!("unsupported sampler creation: {err}"); + DriverError::Unsupported } })? }; @@ -422,7 +417,6 @@ impl Drop for Sampler { derive(Clone, Copy, Debug), pattern = "owned" )] -#[non_exhaustive] pub struct SamplerInfo { /// Bitmask specifying additional parameters of a sampler. #[builder(default)] @@ -464,15 +458,15 @@ pub struct SamplerInfo { #[builder(default)] pub address_mode_w: vk::SamplerAddressMode, - /// The bias to be added to mipmap LOD calculation and bias provided by image sampling functions - /// in SPIR-V, as described in the - /// [LOD Operation](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#textures-level-of-detail-operation) + /// The bias to be added to mipmap LOD calculation and bias provided by image sampling + /// functions in SPIR-V, as described in the + /// See [`VkSamplerCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkSamplerCreateInfo.html). /// section. #[builder(default, setter(into))] pub mip_lod_bias: OrderedFloat, /// Enables anisotropic filtering, as described in the - /// [Texel Anisotropic Filtering](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#textures-texel-anisotropic-filtering) + /// See [`VkSamplerCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkSamplerCreateInfo.html). /// section #[builder(default)] pub anisotropy_enable: bool, @@ -489,18 +483,18 @@ pub struct SamplerInfo { /// Specifies the comparison operator to apply to fetched data before filtering as described in /// the - /// [Depth Compare Operation](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#textures-depth-compare-operation) + /// See [`VkSamplerCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkSamplerCreateInfo.html). /// section. #[builder(default)] pub compare_op: vk::CompareOp, /// Used to clamp the - /// [minimum of the computed LOD value](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#textures-level-of-detail-operation). + /// See [`VkSamplerCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkSamplerCreateInfo.html). #[builder(default, setter(into))] pub min_lod: OrderedFloat, /// Used to clamp the - /// [maximum of the computed LOD value](https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#textures-level-of-detail-operation). + /// See [`VkSamplerCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkSamplerCreateInfo.html). /// /// To avoid clamping the maximum value, set maxLod to the constant `vk::LOD_CLAMP_NONE`. #[builder(default, setter(into))] @@ -521,7 +515,7 @@ pub struct SamplerInfo { /// When set to `false` the range of image coordinates is zero to one. /// /// See - /// [requirements](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkSamplerCreateInfo.html). + /// See [`VkSamplerCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkSamplerCreateInfo.html). #[builder(default)] pub unnormalized_coordinates: bool, @@ -533,7 +527,7 @@ pub struct SamplerInfo { /// The default value is [`vk::SamplerReductionMode::WEIGHTED_AVERAGE`] /// /// See - /// [requirements](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkSamplerCreateInfo.html). + /// See [`VkSamplerCreateInfo`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkSamplerCreateInfo.html). #[builder(default)] pub reduction_mode: vk::SamplerReductionMode, } @@ -587,12 +581,16 @@ impl SamplerInfo { #[deprecated = "Use SamplerInfo::default()"] #[doc(hidden)] pub fn new() -> SamplerInfoBuilder { - Self::default().to_builder() + Self::default().into_builder() + } + + /// Creates a default `SamplerInfoBuilder`. + pub fn builder() -> SamplerInfoBuilder { + Default::default() } /// Converts a `SamplerInfo` into a `SamplerInfoBuilder`. - #[inline(always)] - pub fn to_builder(self) -> SamplerInfoBuilder { + pub fn into_builder(self) -> SamplerInfoBuilder { SamplerInfoBuilder { flags: Some(self.flags), mag_filter: Some(self.mag_filter), @@ -613,6 +611,12 @@ impl SamplerInfo { reduction_mode: Some(self.reduction_mode), } } + + #[deprecated = "use into_builder function"] + #[doc(hidden)] + pub fn to_builder(self) -> SamplerInfoBuilder { + self.into_builder() + } } impl Default for SamplerInfo { @@ -643,15 +647,7 @@ impl SamplerInfoBuilder { /// Builds a new `SamplerInfo`. #[inline(always)] pub fn build(self) -> SamplerInfo { - let res = self.fallible_build(); - - #[cfg(test)] - let res = res.unwrap(); - - #[cfg(not(test))] - let res = unsafe { res.unwrap_unchecked() }; - - res + self.fallible_build().expect("invalid sampler info") } } @@ -682,7 +678,7 @@ pub struct Shader { /// The name of the entry point which will be executed by this shader. /// /// The default value is `main`. - #[builder(default = "\"main\".to_owned()")] + #[builder(default = "\"main\".to_owned()", setter(into))] pub entry_name: String, /// Data about Vulkan specialization constants. @@ -692,10 +688,11 @@ pub struct Shader { /// Basic usage (GLSL): /// /// ``` - /// # inline_spirv::inline_spirv!(r#" + /// # vk_shader_macros::glsl!(r#" /// #version 460 core + /// #pragma shader_stage(compute) /// - /// // Defaults to 6 if not set using Shader specialization_info! + /// // Defaults to 6 if not set using Shader specialization! /// layout(constant_id = 0) const uint MY_COUNT = 6; /// /// layout(set = 0, binding = 0) uniform sampler2D my_samplers[MY_COUNT]; @@ -704,39 +701,36 @@ pub struct Shader { /// { /// // Code uses MY_COUNT number of my_samplers here /// } - /// # "#, comp); + /// # "#); /// ``` /// /// ```no_run /// # use std::sync::Arc; /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::shader::{Shader, SpecializationInfo}; + /// # use vk_graph::driver::DriverError; + /// # use vk_graph::driver::device::{Device, DeviceInfo}; + /// # use vk_graph::driver::shader::{Shader, SpecializationMap}; /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); + /// # let device = Device::new(DeviceInfo::default())?; /// # let my_shader_code = [0u8; 1]; /// // We instead specify 42 for MY_COUNT: /// let shader = Shader::new_fragment(my_shader_code.as_slice()) - /// .specialization_info(SpecializationInfo::new( - /// [vk::SpecializationMapEntry { - /// constant_id: 0, - /// offset: 0, - /// size: 4, - /// }], - /// 42u32.to_ne_bytes() - /// )); + /// .specialization( + /// SpecializationMap::new(42u32.to_ne_bytes()) + /// .constant(0, 0, 4) + /// ); /// # Ok(()) } /// ``` #[builder(default, setter(strip_option))] - pub specialization_info: Option, + pub specialization: Option, /// Shader code. /// /// Although SPIR-V code is specified as `u32` values, this field uses `u8` in order to make /// loading from file simpler. You should always have a SPIR-V code length which is a multiple /// of four bytes, or an error will be returned during pipeline creation. - pub spirv: Vec, + #[builder(setter(into))] + pub spirv: SpirvBinary, /// The shader stage this structure applies to. pub stage: vk::ShaderStageFlags, @@ -752,12 +746,10 @@ pub struct Shader { } impl Shader { - /// Specifies a shader with the given `stage` and shader code values. + /// Specifies a shader with the given `stage` and shader code. #[allow(clippy::new_ret_no_self)] - pub fn new(stage: vk::ShaderStageFlags, spirv: impl ShaderCode) -> ShaderBuilder { - ShaderBuilder::default() - .spirv(spirv.into_vec()) - .stage(stage) + pub fn new(stage: vk::ShaderStageFlags, spirv: impl Into) -> ShaderBuilder { + ShaderBuilder::default().spirv(spirv).stage(stage) } /// Creates a new ray trace shader. @@ -765,7 +757,7 @@ impl Shader { /// # Panics /// /// If the shader code is invalid or not a multiple of four bytes in length. - pub fn new_any_hit(spirv: impl ShaderCode) -> ShaderBuilder { + pub fn new_any_hit(spirv: impl Into) -> ShaderBuilder { Self::new(vk::ShaderStageFlags::ANY_HIT_KHR, spirv) } @@ -774,7 +766,7 @@ impl Shader { /// # Panics /// /// If the shader code is invalid or not a multiple of four bytes in length. - pub fn new_callable(spirv: impl ShaderCode) -> ShaderBuilder { + pub fn new_callable(spirv: impl Into) -> ShaderBuilder { Self::new(vk::ShaderStageFlags::CALLABLE_KHR, spirv) } @@ -783,7 +775,7 @@ impl Shader { /// # Panics /// /// If the shader code is invalid or not a multiple of four bytes in length. - pub fn new_closest_hit(spirv: impl ShaderCode) -> ShaderBuilder { + pub fn new_closest_hit(spirv: impl Into) -> ShaderBuilder { Self::new(vk::ShaderStageFlags::CLOSEST_HIT_KHR, spirv) } @@ -792,7 +784,7 @@ impl Shader { /// # Panics /// /// If the shader code is invalid or not a multiple of four bytes in length. - pub fn new_compute(spirv: impl ShaderCode) -> ShaderBuilder { + pub fn new_compute(spirv: impl Into) -> ShaderBuilder { Self::new(vk::ShaderStageFlags::COMPUTE, spirv) } @@ -801,7 +793,7 @@ impl Shader { /// # Panics /// /// If the shader code is invalid or not a multiple of four bytes in length. - pub fn new_fragment(spirv: impl ShaderCode) -> ShaderBuilder { + pub fn new_fragment(spirv: impl Into) -> ShaderBuilder { Self::new(vk::ShaderStageFlags::FRAGMENT, spirv) } @@ -810,7 +802,7 @@ impl Shader { /// # Panics /// /// If the shader code is invalid or not a multiple of four bytes in length. - pub fn new_geometry(spirv: impl ShaderCode) -> ShaderBuilder { + pub fn new_geometry(spirv: impl Into) -> ShaderBuilder { Self::new(vk::ShaderStageFlags::GEOMETRY, spirv) } @@ -819,7 +811,7 @@ impl Shader { /// # Panics /// /// If the shader code is invalid or not a multiple of four bytes in length. - pub fn new_intersection(spirv: impl ShaderCode) -> ShaderBuilder { + pub fn new_intersection(spirv: impl Into) -> ShaderBuilder { Self::new(vk::ShaderStageFlags::INTERSECTION_KHR, spirv) } @@ -828,7 +820,7 @@ impl Shader { /// # Panics /// /// If the shader code is invalid. - pub fn new_mesh(spirv: impl ShaderCode) -> ShaderBuilder { + pub fn new_mesh(spirv: impl Into) -> ShaderBuilder { Self::new(vk::ShaderStageFlags::MESH_EXT, spirv) } @@ -837,7 +829,7 @@ impl Shader { /// # Panics /// /// If the shader code is invalid or not a multiple of four bytes in length. - pub fn new_miss(spirv: impl ShaderCode) -> ShaderBuilder { + pub fn new_miss(spirv: impl Into) -> ShaderBuilder { Self::new(vk::ShaderStageFlags::MISS_KHR, spirv) } @@ -846,7 +838,7 @@ impl Shader { /// # Panics /// /// If the shader code is invalid or not a multiple of four bytes in length. - pub fn new_ray_gen(spirv: impl ShaderCode) -> ShaderBuilder { + pub fn new_ray_gen(spirv: impl Into) -> ShaderBuilder { Self::new(vk::ShaderStageFlags::RAYGEN_KHR, spirv) } @@ -855,34 +847,46 @@ impl Shader { /// # Panics /// /// If the shader code is invalid. - pub fn new_task(spirv: impl ShaderCode) -> ShaderBuilder { + pub fn new_task(spirv: impl Into) -> ShaderBuilder { Self::new(vk::ShaderStageFlags::TASK_EXT, spirv) } - /// Creates a new tesselation control shader. + /// Creates a new tessellation control shader. /// /// # Panics /// /// If the shader code is invalid or not a multiple of four bytes in length. - pub fn new_tesselation_ctrl(spirv: impl ShaderCode) -> ShaderBuilder { + pub fn new_tessellation_ctrl(spirv: impl Into) -> ShaderBuilder { Self::new(vk::ShaderStageFlags::TESSELLATION_CONTROL, spirv) } - /// Creates a new tesselation evaluation shader. + #[deprecated = "use new_tessellation_ctrl function"] + #[doc(hidden)] + pub fn new_tesselation_ctrl(spirv: impl Into) -> ShaderBuilder { + Self::new_tessellation_ctrl(spirv) + } + + /// Creates a new tessellation evaluation shader. /// /// # Panics /// /// If the shader code is invalid or not a multiple of four bytes in length. - pub fn new_tesselation_eval(spirv: impl ShaderCode) -> ShaderBuilder { + pub fn new_tessellation_eval(spirv: impl Into) -> ShaderBuilder { Self::new(vk::ShaderStageFlags::TESSELLATION_EVALUATION, spirv) } + #[deprecated = "use new_tessellation_eval function"] + #[doc(hidden)] + pub fn new_tesselation_eval(spirv: impl Into) -> ShaderBuilder { + Self::new_tessellation_eval(spirv) + } + /// Creates a new vertex shader. /// /// # Panics /// /// If the shader code is invalid or not a multiple of four bytes in length. - pub fn new_vertex(spirv: impl ShaderCode) -> ShaderBuilder { + pub fn new_vertex(spirv: impl Into) -> ShaderBuilder { Self::new(vk::ShaderStageFlags::VERTEX, spirv) } @@ -909,6 +913,11 @@ impl Shader { ) } + /// Creates a default `ShaderBuilder`. + pub fn builder() -> ShaderBuilder { + Default::default() + } + #[profiling::function] pub(super) fn descriptor_bindings(&self) -> DescriptorBindingMap { let mut res = DescriptorBindingMap::default(); @@ -986,6 +995,11 @@ impl Shader { res } + /// Specifies a shader with the given shader code. + pub fn from_spirv(spirv: impl Into) -> ShaderBuilder { + ShaderBuilder::default().spirv(spirv) + } + fn image_sampler(&self, descriptor: Descriptor, name: &str) -> (SamplerInfo, bool) { self.image_samplers .get(&descriptor) @@ -1158,23 +1172,28 @@ impl Shader { #[profiling::function] fn reflect_entry_point( entry_name: &str, - spirv: &[u8], - specialization_info: Option<&SpecializationInfo>, + spirv: impl Into, + specialization: Option<&SpecializationMap>, ) -> Result { let mut config = ReflectConfig::new(); config.ref_all_rscs(true).spv(spirv); - if let Some(spec_info) = specialization_info { - for spec in &spec_info.map_entries { + if let Some(specialization) = specialization { + for &vk::SpecializationMapEntry { + constant_id, + offset, + size, + } in &specialization.entries + { config.specialize( - spec.constant_id, - spec_info.data[spec.offset as usize..spec.offset as usize + spec.size].into(), + constant_id, + specialization.data[offset as usize..offset as usize + size].into(), ); } } let entry_points = config.reflect().map_err(|err| { - error!("Unable to reflect spirv: {err}"); + error!("invalid spirv reflection data: {err}"); DriverError::InvalidData })?; @@ -1182,7 +1201,7 @@ impl Shader { .into_iter() .find(|entry_point| entry_point.name == entry_name) .ok_or_else(|| { - error!("Entry point not found"); + error!("invalid shader entry point: not found"); DriverError::InvalidData })?; @@ -1191,64 +1210,64 @@ impl Shader { } #[profiling::function] - pub(super) fn vertex_input(&self) -> VertexInputState { + pub(super) fn try_vertex_input(&self) -> Result { // Check for manually-specified vertex layout descriptions if let Some(vertex_input) = &self.vertex_input_state { - return vertex_input.clone(); + return Ok(vertex_input.clone()); } - fn scalar_format(ty: &ScalarType) -> vk::Format { + fn scalar_format(ty: &ScalarType) -> Option { match *ty { ScalarType::Float { bits } => match bits { - u8::BITS => vk::Format::R8_SNORM, - u16::BITS => vk::Format::R16_SFLOAT, - u32::BITS => vk::Format::R32_SFLOAT, - u64::BITS => vk::Format::R64_SFLOAT, - _ => unimplemented!("{bits}-bit float"), + u8::BITS => Some(vk::Format::R8_SNORM), + u16::BITS => Some(vk::Format::R16_SFLOAT), + u32::BITS => Some(vk::Format::R32_SFLOAT), + u64::BITS => Some(vk::Format::R64_SFLOAT), + _ => None, }, ScalarType::Integer { bits, is_signed: false, } => match bits { - u8::BITS => vk::Format::R8_UINT, - u16::BITS => vk::Format::R16_UINT, - u32::BITS => vk::Format::R32_UINT, - u64::BITS => vk::Format::R64_UINT, - _ => unimplemented!("{bits}-bit unsigned integer"), + u8::BITS => Some(vk::Format::R8_UINT), + u16::BITS => Some(vk::Format::R16_UINT), + u32::BITS => Some(vk::Format::R32_UINT), + u64::BITS => Some(vk::Format::R64_UINT), + _ => None, }, ScalarType::Integer { bits, is_signed: true, } => match bits { - u8::BITS => vk::Format::R8_SINT, - u16::BITS => vk::Format::R16_SINT, - u32::BITS => vk::Format::R32_SINT, - u64::BITS => vk::Format::R64_SINT, - _ => unimplemented!("{bits}-bit signed integer"), + u8::BITS => Some(vk::Format::R8_SINT), + u16::BITS => Some(vk::Format::R16_SINT), + u32::BITS => Some(vk::Format::R32_SINT), + u64::BITS => Some(vk::Format::R64_SINT), + _ => None, }, - _ => unimplemented!("{ty:?}"), + _ => None, } } - fn vector_format(ty: &VectorType) -> vk::Format { + fn vector_format(ty: &VectorType) -> Option { match *ty { VectorType { scalar_ty: ScalarType::Float { bits }, nscalar, } => match (bits, nscalar) { - (u8::BITS, 2) => vk::Format::R8G8_SNORM, - (u8::BITS, 3) => vk::Format::R8G8B8_SNORM, - (u8::BITS, 4) => vk::Format::R8G8B8A8_SNORM, - (u16::BITS, 2) => vk::Format::R16G16_SFLOAT, - (u16::BITS, 3) => vk::Format::R16G16B16_SFLOAT, - (u16::BITS, 4) => vk::Format::R16G16B16A16_SFLOAT, - (u32::BITS, 2) => vk::Format::R32G32_SFLOAT, - (u32::BITS, 3) => vk::Format::R32G32B32_SFLOAT, - (u32::BITS, 4) => vk::Format::R32G32B32A32_SFLOAT, - (u64::BITS, 2) => vk::Format::R64G64_SFLOAT, - (u64::BITS, 3) => vk::Format::R64G64B64_SFLOAT, - (u64::BITS, 4) => vk::Format::R64G64B64A64_SFLOAT, - _ => unimplemented!("{bits}-bit vec{nscalar} float"), + (u8::BITS, 2) => Some(vk::Format::R8G8_SNORM), + (u8::BITS, 3) => Some(vk::Format::R8G8B8_SNORM), + (u8::BITS, 4) => Some(vk::Format::R8G8B8A8_SNORM), + (u16::BITS, 2) => Some(vk::Format::R16G16_SFLOAT), + (u16::BITS, 3) => Some(vk::Format::R16G16B16_SFLOAT), + (u16::BITS, 4) => Some(vk::Format::R16G16B16A16_SFLOAT), + (u32::BITS, 2) => Some(vk::Format::R32G32_SFLOAT), + (u32::BITS, 3) => Some(vk::Format::R32G32B32_SFLOAT), + (u32::BITS, 4) => Some(vk::Format::R32G32B32A32_SFLOAT), + (u64::BITS, 2) => Some(vk::Format::R64G64_SFLOAT), + (u64::BITS, 3) => Some(vk::Format::R64G64B64_SFLOAT), + (u64::BITS, 4) => Some(vk::Format::R64G64B64A64_SFLOAT), + _ => None, }, VectorType { scalar_ty: @@ -1258,19 +1277,19 @@ impl Shader { }, nscalar, } => match (bits, nscalar) { - (u8::BITS, 2) => vk::Format::R8G8_UINT, - (u8::BITS, 3) => vk::Format::R8G8B8_UINT, - (u8::BITS, 4) => vk::Format::R8G8B8A8_UINT, - (u16::BITS, 2) => vk::Format::R16G16_UINT, - (u16::BITS, 3) => vk::Format::R16G16B16_UINT, - (u16::BITS, 4) => vk::Format::R16G16B16A16_UINT, - (u32::BITS, 2) => vk::Format::R32G32_UINT, - (u32::BITS, 3) => vk::Format::R32G32B32_UINT, - (u32::BITS, 4) => vk::Format::R32G32B32A32_UINT, - (u64::BITS, 2) => vk::Format::R64G64_UINT, - (u64::BITS, 3) => vk::Format::R64G64B64_UINT, - (u64::BITS, 4) => vk::Format::R64G64B64A64_UINT, - _ => unimplemented!("{bits}-bit vec{nscalar} unsigned integer"), + (u8::BITS, 2) => Some(vk::Format::R8G8_UINT), + (u8::BITS, 3) => Some(vk::Format::R8G8B8_UINT), + (u8::BITS, 4) => Some(vk::Format::R8G8B8A8_UINT), + (u16::BITS, 2) => Some(vk::Format::R16G16_UINT), + (u16::BITS, 3) => Some(vk::Format::R16G16B16_UINT), + (u16::BITS, 4) => Some(vk::Format::R16G16B16A16_UINT), + (u32::BITS, 2) => Some(vk::Format::R32G32_UINT), + (u32::BITS, 3) => Some(vk::Format::R32G32B32_UINT), + (u32::BITS, 4) => Some(vk::Format::R32G32B32A32_UINT), + (u64::BITS, 2) => Some(vk::Format::R64G64_UINT), + (u64::BITS, 3) => Some(vk::Format::R64G64B64_UINT), + (u64::BITS, 4) => Some(vk::Format::R64G64B64A64_UINT), + _ => None, }, VectorType { scalar_ty: @@ -1280,21 +1299,21 @@ impl Shader { }, nscalar, } => match (bits, nscalar) { - (u8::BITS, 2) => vk::Format::R8G8_SINT, - (u8::BITS, 3) => vk::Format::R8G8B8_SINT, - (u8::BITS, 4) => vk::Format::R8G8B8A8_SINT, - (u16::BITS, 2) => vk::Format::R16G16_SINT, - (u16::BITS, 3) => vk::Format::R16G16B16_SINT, - (u16::BITS, 4) => vk::Format::R16G16B16A16_SINT, - (u32::BITS, 2) => vk::Format::R32G32_SINT, - (u32::BITS, 3) => vk::Format::R32G32B32_SINT, - (u32::BITS, 4) => vk::Format::R32G32B32A32_SINT, - (u64::BITS, 2) => vk::Format::R64G64_SINT, - (u64::BITS, 3) => vk::Format::R64G64B64_SINT, - (u64::BITS, 4) => vk::Format::R64G64B64A64_SINT, - _ => unimplemented!("{bits}-bit vec{nscalar} signed integer"), + (u8::BITS, 2) => Some(vk::Format::R8G8_SINT), + (u8::BITS, 3) => Some(vk::Format::R8G8B8_SINT), + (u8::BITS, 4) => Some(vk::Format::R8G8B8A8_SINT), + (u16::BITS, 2) => Some(vk::Format::R16G16_SINT), + (u16::BITS, 3) => Some(vk::Format::R16G16B16_SINT), + (u16::BITS, 4) => Some(vk::Format::R16G16B16A16_SINT), + (u32::BITS, 2) => Some(vk::Format::R32G32_SINT), + (u32::BITS, 3) => Some(vk::Format::R32G32B32_SINT), + (u32::BITS, 4) => Some(vk::Format::R32G32B32A32_SINT), + (u64::BITS, 2) => Some(vk::Format::R64G64_SINT), + (u64::BITS, 3) => Some(vk::Format::R64G64B64_SINT), + (u64::BITS, 4) => Some(vk::Format::R64G64B64A64_SINT), + _ => None, }, - _ => unimplemented!("{ty:?}"), + _ => None, } } @@ -1309,7 +1328,7 @@ impl Shader { .as_ref() .filter(|name| name.contains("_ibind") || name.contains("_vbind")) .map(|name| { - let binding = name[name.rfind("bind").unwrap()..] + let binding = name[name.rfind("bind").expect("missing bind suffix")..] .parse() .unwrap_or_default(); let rate = if name.contains("_ibind") { @@ -1333,14 +1352,21 @@ impl Shader { //trace!("{location} {:?} is {byte_stride} bytes", name); + let format = match ty { + Type::Scalar(ty) => scalar_format(ty), + Type::Vector(ty) => vector_format(ty), + _ => None, + } + .ok_or_else(|| { + warn!("unsupported reflected vertex input type: {ty:?}"); + + DriverError::Unsupported + })?; + vertex_attribute_descriptions.push(vk::VertexInputAttributeDescription { location, binding, - format: match ty { - Type::Scalar(ty) => scalar_format(ty), - Type::Vector(ty) => vector_format(ty), - _ => unimplemented!("{:?}", ty), - }, + format, offset: byte_stride, // Figured out below - this data is iter'd in an unknown order }); } @@ -1385,10 +1411,16 @@ impl Shader { }); } - VertexInputState { + Ok(VertexInputState { vertex_attribute_descriptions, vertex_binding_descriptions, - } + }) + } + + #[profiling::function] + pub(super) fn vertex_input(&self) -> VertexInputState { + self.try_vertex_input() + .expect("unsupported reflected vertex input layout") } } @@ -1406,6 +1438,15 @@ impl From for Shader { } } +impl From for Shader +where + T: Into, +{ + fn from(spirv: T) -> Self { + Shader::from_spirv(spirv).build() + } +} + // HACK: https://github.com/colin-kiegel/rust-derive-builder/issues/56 impl ShaderBuilder { /// Specifies a shader with the given `stage` and shader code values. @@ -1414,22 +1455,12 @@ impl ShaderBuilder { } /// Builds a new `Shader`. - pub fn build(mut self) -> Shader { - let entry_name = self.entry_name.as_deref().unwrap_or("main"); - self.entry_point = Some( - Shader::reflect_entry_point( - entry_name, - self.spirv.as_deref().unwrap(), - self.specialization_info - .as_ref() - .map(|opt| opt.as_ref()) - .unwrap_or_default(), - ) - .unwrap_or_else(|_| panic!("invalid shader code for entry name \'{entry_name}\'")), - ); + pub fn build(self) -> Shader { + let entry_name = self.entry_name.clone().unwrap_or_else(|| "main".to_owned()); - self.fallible_build() - .expect("All required fields set at initialization") + self.try_build().unwrap_or_else(|_| { + panic!("invalid or unsupported shader code for entry name '{entry_name}'") + }) } /// Specifies a manually-defined image sampler. @@ -1466,12 +1497,69 @@ impl ShaderBuilder { self.image_samplers .as_mut() - .unwrap() + .expect("missing image samplers") .insert(descriptor, info); self } + /// Attempts to build a new `Shader`. + pub fn try_build(mut self) -> Result { + let entry_name = self.entry_name.as_deref().unwrap_or("main"); + let entry_point = Shader::reflect_entry_point( + entry_name, + self.spirv + .as_ref() + .map(|spirv| spirv.words()) + .expect("missing spirv code"), + self.specialization + .as_ref() + .map(|opt| opt.as_ref()) + .unwrap_or_default(), + ) + .map_err(|err| { + warn!("invalid shader reflection entry point: {err}"); + + DriverError::InvalidData + })?; + + if self.stage.unwrap_or_default().is_empty() { + self.stage = Some(match entry_point.exec_model { + ExecutionModel::Vertex => vk::ShaderStageFlags::VERTEX, + ExecutionModel::TessellationControl => vk::ShaderStageFlags::TESSELLATION_CONTROL, + ExecutionModel::TessellationEvaluation => { + vk::ShaderStageFlags::TESSELLATION_EVALUATION + } + ExecutionModel::Geometry => vk::ShaderStageFlags::GEOMETRY, + ExecutionModel::Fragment => vk::ShaderStageFlags::FRAGMENT, + ExecutionModel::GLCompute => vk::ShaderStageFlags::COMPUTE, + ExecutionModel::Kernel => { + warn!("unsupported shader execution model: kernel"); + + return Err(DriverError::Unsupported); + } + ExecutionModel::TaskNV => vk::ShaderStageFlags::TASK_EXT, + ExecutionModel::MeshNV => vk::ShaderStageFlags::MESH_EXT, + ExecutionModel::RayGenerationNV => vk::ShaderStageFlags::RAYGEN_KHR, + ExecutionModel::IntersectionNV => vk::ShaderStageFlags::INTERSECTION_KHR, + ExecutionModel::AnyHitNV => vk::ShaderStageFlags::ANY_HIT_KHR, + ExecutionModel::ClosestHitNV => vk::ShaderStageFlags::CLOSEST_HIT_KHR, + ExecutionModel::MissNV => vk::ShaderStageFlags::MISS_KHR, + ExecutionModel::CallableNV => vk::ShaderStageFlags::CALLABLE_KHR, + ExecutionModel::TaskEXT => vk::ShaderStageFlags::TASK_EXT, + ExecutionModel::MeshEXT => vk::ShaderStageFlags::MESH_EXT, + }) + } + + self.entry_point = Some(entry_point); + + self.fallible_build().map_err(|err| { + warn!("invalid shader builder state: {err:?}"); + + DriverError::InvalidData + }) + } + /// Specifies a manually-defined vertex input layout. /// /// The vertex input layout, by default, uses reflection to automatically define vertex binding @@ -1506,74 +1594,91 @@ impl From for ShaderBuilderError { } } -/// Trait for types which can be converted into shader code. -pub trait ShaderCode { - /// Converts the instance into SPIR-V shader code specified as a byte array. - fn into_vec(self) -> Vec; -} +/// Describes specialized constant values. +#[derive(Clone, Debug, Default)] +pub struct SpecializationMap { + /// A buffer of data which holds the constant values. + pub data: Vec, -impl ShaderCode for &[u8] { - fn into_vec(self) -> Vec { - debug_assert_eq!(self.len() % 4, 0, "invalid spir-v code"); + /// Mapping of locations within the constant value data which describe each individual + /// constant. + pub entries: Vec, +} - self.to_vec() +impl SpecializationMap { + /// Constructs a new `SpecializationMap`. + pub fn new(data: impl Into>) -> Self { + Self { + data: data.into(), + entries: Default::default(), + } } -} -impl ShaderCode for &[u32] { - fn into_vec(self) -> Vec { - pub fn into_u8_slice(t: &[T]) -> &[u8] - where - T: Sized, - { - use std::slice::from_raw_parts; + /// Adds a single constant offset and size to the map and returns the map for further building. + pub fn constant(mut self, constant_id: u32, offset: u32, size: usize) -> Self { + self.set_constant(constant_id, offset, size); + self + } - unsafe { from_raw_parts(t.as_ptr() as *const _, size_of_val(t)) } - } + /// Adds a single constant offset and size to the map and returns the map for further building. + pub fn set_constant(&mut self, constant_id: u32, offset: u32, size: usize) { + self.entries.push(vk::SpecializationMapEntry { + constant_id, + offset, + size, + }); + } +} - into_u8_slice(self).into_vec() +impl<'a> From<&'a SpecializationMap> for vk::SpecializationInfo<'a> { + fn from(value: &'a SpecializationMap) -> Self { + vk::SpecializationInfo::default() + .map_entries(&value.entries) + .data(&value.data) } } -impl ShaderCode for Vec { - fn into_vec(self) -> Vec { - debug_assert_eq!(self.len() % 4, 0, "invalid spir-v code"); +mod deprecated { + use { + crate::driver::shader::{ShaderBuilder, SpecializationMap}, + ash::vk, + }; - self + #[derive(Clone, Debug)] + pub struct SpecializationInfo { + pub data: Vec, + pub map_entries: Vec, } -} -impl ShaderCode for Vec { - fn into_vec(self) -> Vec { - self.as_slice().into_vec() + impl SpecializationInfo { + pub fn new( + map_entries: impl Into>, + data: impl Into>, + ) -> Self { + Self { + data: data.into(), + map_entries: map_entries.into(), + } + } } -} -/// Describes specialized constant values. -#[derive(Clone, Debug)] -pub struct SpecializationInfo { - /// A buffer of data which holds the constant values. - pub data: Vec, + impl ShaderBuilder { + #[deprecated = "use specialization function"] + #[doc(hidden)] + pub fn specialization_info(self, info: SpecializationInfo) -> Self { + let mut specialization = SpecializationMap::new(info.data); - /// Mapping of locations within the constant value data which describe each individual constant. - pub map_entries: Vec, -} + for entry in &info.map_entries { + specialization.set_constant(entry.constant_id, entry.offset, entry.size); + } -impl SpecializationInfo { - /// Constructs a new `SpecializationInfo`. - pub fn new( - map_entries: impl Into>, - data: impl Into>, - ) -> Self { - Self { - data: data.into(), - map_entries: map_entries.into(), + self.specialization(specialization) } } } #[cfg(test)] -mod tests { +mod test { use super::*; type Info = SamplerInfo; @@ -1582,7 +1687,7 @@ mod tests { #[test] pub fn sampler_info() { let info = Info::default(); - let builder = info.to_builder().build(); + let builder = info.into_builder().build(); assert_eq!(info, builder); } diff --git a/src/driver/surface.rs b/src/driver/surface.rs index e5e4a7ef..8b9ba05e 100644 --- a/src/driver/surface.rs +++ b/src/driver/surface.rs @@ -1,94 +1,105 @@ //! Native platform window surface types. use { - super::{DriverError, Instance, device::Device}, + super::{DriverError, device::Device, instance::Instance}, ash::vk, ash_window::create_surface, log::warn, raw_window_handle::{HasDisplayHandle, HasWindowHandle}, std::{ fmt::{Debug, Formatter}, - ops::Deref, - sync::Arc, thread::panicking, }, }; /// Smart pointer handle to a [`vk::SurfaceKHR`] object. +#[read_only::cast] pub struct Surface { - device: Arc, - surface: vk::SurfaceKHR, + /// The device which owns this surface resource. + /// + /// _Note:_ This field is read-only. + pub device: Device, + + /// The native Vulkan resource handle of this surface. + /// + /// _Note:_ This field is read-only. + pub handle: vk::SurfaceKHR, } impl Surface { /// Query surface capabilities - pub fn capabilities(this: &Self) -> Result { - let surface_ext = Device::expect_surface_ext(&this.device); + pub fn capabilities(&self) -> Result { + let surface_ext = Device::expect_surface_ext(&self.device); unsafe { surface_ext.get_physical_device_surface_capabilities( - *this.device.physical_device, - this.surface, + self.device.physical_device.handle, + self.handle, ) } - .inspect_err(|err| warn!("unable to get surface capabilities: {err}")) - .or(Err(DriverError::Unsupported)) + .map_err(|err| { + warn!("unable to get surface capabilities: {err}"); + + DriverError::Unsupported + }) } /// Create a surface from a raw window display handle. /// - /// `device` must have been created with platform specific surface extensions enabled, acquired - /// through [`Device::create_display_window`]. + /// `device` must have been created with platform specific surface extensions enabled. #[profiling::function] pub fn create( - device: &Arc, - window: &(impl HasDisplayHandle + HasWindowHandle), + device: &Device, + display: impl HasDisplayHandle, + window: impl HasWindowHandle, ) -> Result { - let device = Arc::clone(device); - let instance = Device::instance(&device); - let display_handle = window.display_handle().map_err(|err| { - warn!("{err}"); + let device = device.clone(); + + let display_handle = display.display_handle().map_err(|err| { + warn!("unable to get display handle: {err}"); DriverError::Unsupported })?; let window_handle = window.window_handle().map_err(|err| { - warn!("{err}"); + warn!("unable to get window handle: {err}"); DriverError::Unsupported })?; - let surface = unsafe { + + let handle = unsafe { create_surface( - Instance::entry(instance), - instance, + Instance::entry(&device.physical_device.instance), + &device.physical_device.instance, display_handle.as_raw(), window_handle.as_raw(), None, ) } .map_err(|err| { - warn!("Unable to create surface: {err}"); + warn!("unable to create surface: {err}"); DriverError::Unsupported })?; - Ok(Self { device, surface }) + Ok(Self { device, handle }) } /// Lists the supported surface formats. #[profiling::function] - pub fn formats(this: &Self) -> Result, DriverError> { + pub fn formats(&self) -> Result, DriverError> { + let surface_ext = Device::expect_surface_ext(&self.device); + unsafe { - this.device - .surface_ext - .as_ref() - .unwrap() - .get_physical_device_surface_formats(*this.device.physical_device, this.surface) - .map_err(|err| { - warn!("Unable to get surface formats: {err}"); - - DriverError::Unsupported - }) + surface_ext.get_physical_device_surface_formats( + self.device.physical_device.handle, + self.handle, + ) } + .map_err(|err| { + warn!("unable to get surface formats: {err}"); + + DriverError::Unsupported + }) } /// Helper function to automatically select the best UNORM format, if one is available. @@ -114,18 +125,45 @@ impl Surface { Self::linear(formats).unwrap_or_else(|| formats.first().copied().unwrap_or_default()) } + /// Returns `true` if the given queue family supports presentation on this surface. + pub fn physical_device_support(&self, queue_family_index: u32) -> Result { + let surface_ext = Device::expect_surface_ext(&self.device); + + unsafe { + surface_ext.get_physical_device_surface_support( + self.device.physical_device.handle, + queue_family_index, + self.handle, + ) + } + .map_err(|err| { + warn!("unable to get physical device support: {err}"); + + match err { + vk::Result::ERROR_OUT_OF_DEVICE_MEMORY | vk::Result::ERROR_OUT_OF_HOST_MEMORY => { + DriverError::OutOfMemory + } + vk::Result::ERROR_SURFACE_LOST_KHR => DriverError::InvalidData, + _ => DriverError::Unsupported, + } + }) + } + /// Query supported presentation modes. - pub fn present_modes(this: &Self) -> Result, DriverError> { - let surface_ext = Device::expect_surface_ext(&this.device); + pub fn present_modes(&self) -> Result, DriverError> { + let surface_ext = Device::expect_surface_ext(&self.device); unsafe { surface_ext.get_physical_device_surface_present_modes( - *this.device.physical_device, - this.surface, + self.device.physical_device.handle, + self.handle, ) } - .inspect_err(|err| warn!("unable to get surface present modes: {err}")) - .or(Err(DriverError::Unsupported)) + .map_err(|err| { + warn!("unable to get present modes: {err}"); + + DriverError::Unsupported + }) } /// Helper function to automatically select the best sRGB format, if one is available. @@ -164,14 +202,6 @@ impl Debug for Surface { } } -impl Deref for Surface { - type Target = vk::SurfaceKHR; - - fn deref(&self) -> &Self::Target { - &self.surface - } -} - impl Drop for Surface { #[profiling::function] fn drop(&mut self) { @@ -182,7 +212,85 @@ impl Drop for Surface { let surface_ext = Device::expect_surface_ext(&self.device); unsafe { - surface_ext.destroy_surface(self.surface, None); + surface_ext.destroy_surface(self.handle, None); } } } + +impl Eq for Surface {} + +impl PartialEq for Surface { + fn eq(&self, other: &Self) -> bool { + self.handle == other.handle + } +} + +#[cfg(test)] +mod test { + use super::Surface; + use ash::vk; + + #[test] + fn linear_prefers_known_unorm_formats() { + let formats = [ + vk::SurfaceFormatKHR { + format: vk::Format::R8G8B8A8_SRGB, + ..Default::default() + }, + vk::SurfaceFormatKHR { + format: vk::Format::R8G8B8A8_UNORM, + ..Default::default() + }, + ]; + + assert_eq!( + Surface::linear(&formats).unwrap().format, + vk::Format::R8G8B8A8_UNORM + ); + } + + #[test] + fn linear_or_default_falls_back_to_first_format() { + let formats = [vk::SurfaceFormatKHR { + format: vk::Format::R16G16B16A16_SFLOAT, + ..Default::default() + }]; + + assert_eq!( + Surface::linear_or_default(&formats).format, + vk::Format::R16G16B16A16_SFLOAT + ); + } + + #[test] + fn srgb_prefers_known_srgb_formats() { + let formats = [ + vk::SurfaceFormatKHR { + color_space: vk::ColorSpaceKHR::DISPLAY_P3_NONLINEAR_EXT, + format: vk::Format::B8G8R8A8_SRGB, + }, + vk::SurfaceFormatKHR { + color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR, + format: vk::Format::R8G8B8A8_SRGB, + }, + ]; + + assert_eq!( + Surface::srgb(&formats).unwrap().format, + vk::Format::R8G8B8A8_SRGB + ); + } + + #[test] + fn srgb_or_default_falls_back_to_first_format() { + let formats = [vk::SurfaceFormatKHR { + color_space: vk::ColorSpaceKHR::DISPLAY_P3_NONLINEAR_EXT, + format: vk::Format::R16G16B16A16_SFLOAT, + }]; + + assert_eq!( + Surface::srgb_or_default(&formats).format, + vk::Format::R16G16B16A16_SFLOAT + ); + } +} diff --git a/src/driver/swapchain.rs b/src/driver/swapchain.rs index 528514e0..ffc6c608 100644 --- a/src/driver/swapchain.rs +++ b/src/driver/swapchain.rs @@ -6,59 +6,188 @@ use { device::Device, image::{Image, ImageInfo}, }, - ash::vk, + ash::vk::{self, Handle}, derive_builder::{Builder, UninitializedFieldError}, log::{debug, info, trace, warn}, - std::{mem::replace, ops::Deref, slice, sync::Arc, thread::panicking}, + std::{mem::replace, ops::Deref, slice, thread::panicking}, }; -// TODO: This needs to track completed command buffers and not constantly create semaphores +#[derive(Debug)] +struct QueueFamilySignal { + cmd_pool: vk::CommandPool, + queue_signals: Box<[QueueSignal]>, +} + +#[derive(Debug)] +struct QueueSignal { + cmd: vk::CommandBuffer, + fence: vk::Fence, + queued: bool, +} + +#[derive(Debug, Default)] +struct QueueSwapchain { + handle: vk::SwapchainKHR, + queue_family_index: u32, + queue_index: u32, +} /// Provides the ability to present rendering results to a [`Surface`]. #[derive(Debug)] +#[read_only::cast] pub struct Swapchain { - device: Arc, + /// The native Vulkan resource handle of this swapchain. + /// + /// _Note:_ This field is read-only. + pub handle: vk::SwapchainKHR, + images: Box<[SwapchainImage]>, - info: SwapchainInfo, - old_swapchain: vk::SwapchainKHR, + + /// Information used to create this resource. + /// + /// _Note:_ This field is read-only. + pub info: SwapchainInfo, + + prev_queue: (u32, u32), + prev_swapchain: QueueSwapchain, + queue_family_signals: Box<[QueueFamilySignal]>, suboptimal: bool, - surface: Surface, - swapchain: vk::SwapchainKHR, + + /// The surface which supports this swapchain. + /// + /// _Note:_ This field is read-only. + pub surface: Surface, } impl Swapchain { /// Prepares a [`vk::SwapchainKHR`] object which is lazily created after calling /// [`acquire_next_image`][Self::acquire_next_image]. #[profiling::function] - pub fn new( - device: &Arc, - surface: Surface, - info: impl Into, - ) -> Result { - let device = Arc::clone(device); + pub fn new(surface: Surface, info: impl Into) -> Result { let info = info.into(); + let device = &surface.device; + let queue_families = surface.device.physical_device.queue_families.iter(); + let mut queue_family_signals = Vec::with_capacity(queue_families.len()); + + for (idx, queue_family) in queue_families.enumerate() { + let cmd_pool_info = vk::CommandPoolCreateInfo::default().queue_family_index(idx as _); + if !surface.physical_device_support(cmd_pool_info.queue_family_index)? { + continue; + } + + let cmd_pool = unsafe { + device + .create_command_pool(&cmd_pool_info, None) + .map_err(|err| { + warn!("unable to create command pool: {err}"); + + match err { + vk::Result::ERROR_OUT_OF_DEVICE_MEMORY + | vk::Result::ERROR_OUT_OF_HOST_MEMORY => DriverError::OutOfMemory, + _ => DriverError::Unsupported, + } + })? + }; + let cmd_bufs = unsafe { + device + .allocate_command_buffers( + &vk::CommandBufferAllocateInfo::default() + .command_buffer_count(queue_family.queue_count) + .command_pool(cmd_pool) + .level(vk::CommandBufferLevel::PRIMARY), + ) + .map_err(|err| { + warn!("unable to allocate command buffer: {err}"); + + match err { + vk::Result::ERROR_OUT_OF_DEVICE_MEMORY + | vk::Result::ERROR_OUT_OF_HOST_MEMORY => DriverError::OutOfMemory, + _ => DriverError::Unsupported, + } + })? + }; + + let mut queue_signals = Vec::with_capacity(queue_family.queue_count as _); + + for cmd in cmd_bufs { + Device::begin_command_buffer( + device, + cmd, + &vk::CommandBufferBeginInfo::default() + .flags(vk::CommandBufferUsageFlags::SIMULTANEOUS_USE), + )?; + Device::end_command_buffer(device, cmd)?; + + let queued = false; + let fence = Device::create_fence(device, queued)?; + queue_signals.push(QueueSignal { cmd, fence, queued }); + } + + queue_family_signals.push(QueueFamilySignal { + cmd_pool, + queue_signals: queue_signals.into_boxed_slice(), + }); + } + Ok(Swapchain { - device, + handle: Default::default(), images: Default::default(), info, - old_swapchain: vk::SwapchainKHR::null(), + prev_queue: Default::default(), + prev_swapchain: Default::default(), + queue_family_signals: queue_family_signals.into_boxed_slice(), suboptimal: true, surface, - swapchain: vk::SwapchainKHR::null(), }) } - /// Gets the next available swapchain image which should be rendered to and then presented using - /// [`present_image`][Self::present_image]. + /// Acquires the next available swapchain image for rendering. + /// + /// The returned [`SwapchainImage`] should be rendered to and then presented using + /// [`present_image`][Self::present_image]. Each call returns a unique image from the + /// swapchain's image ring buffer; the caller must not render to the same image concurrently. + /// + /// # Parameters + /// + /// * `timeout` — Maximum time to wait in nanoseconds before the operation times out. Pass + /// [`u64::MAX`] to wait indefinitely. A short timeout may return + /// [`SwapchainError::Suboptimal`] without acquiring an image. + /// * `acquired` — A semaphore that will be signaled when the acquired image is ready for use. + /// The caller must wait on this semaphore before submitting commands that write to the + /// returned image. + /// + /// # Errors + /// + /// Returns [`SwapchainError::Suboptimal`] if the swapchain no longer matches the + /// surface properties (e.g. after a window resize). The caller should typically + /// recreate any framebuffer-sized resources and try again next frame. + /// + /// Returns [`SwapchainError::SurfaceLost`] if the underlying surface has been + /// destroyed. The swapchain and surface must be recreated. + /// + /// Returns [`SwapchainError::DeviceLost`] if the Vulkan device has been lost. + /// The application must destroy and recreate the device. + /// + /// # Panics + /// + /// Panics if the acquired image index is out of bounds of the internal image array + /// (this indicates a driver or swapchain consistency bug). + /// + /// # Retry behavior + /// + /// Internally this method retries once on transient errors + /// (`TIMEOUT`, `NOT_READY`, `OUT_OF_DATE_KHR`, `FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT`). + /// Between retries the swapchain is recreated if the current state is suboptimal. #[profiling::function] pub fn acquire_next_image( &mut self, + timeout: u64, acquired: vk::Semaphore, ) -> Result { for _ in 0..2 { if self.suboptimal { - self.recreate_swapchain().map_err(|err| { + self.recreate().map_err(|err| { if matches!(err, DriverError::Unsupported) { SwapchainError::Suboptimal } else { @@ -67,15 +196,10 @@ impl Swapchain { })?; } - let swapchain_ext = Device::expect_swapchain_ext(&self.device); + let swapchain_ext = Device::expect_swapchain_ext(&self.surface.device); let image_idx = unsafe { - swapchain_ext.acquire_next_image( - self.swapchain, - u64::MAX, - acquired, - vk::Fence::null(), - ) + swapchain_ext.acquire_next_image(self.handle, timeout, acquired, vk::Fence::null()) } .map(|(idx, suboptimal)| { if suboptimal { @@ -93,8 +217,7 @@ impl Swapchain { assert!(image_idx < self.images.len()); - let image = unsafe { self.images.get_unchecked(image_idx) }; - let image = SwapchainImage::clone_swapchain(image); + let image = unsafe { self.images.get_unchecked(image_idx) }.clone(); return Ok(replace( unsafe { self.images.get_unchecked_mut(image_idx) }, @@ -146,42 +269,36 @@ impl Swapchain { Err(SwapchainError::Suboptimal) } - fn clamp_desired_image_count( - desired_image_count: u32, - surface_capabilities: vk::SurfaceCapabilitiesKHR, - ) -> u32 { - let mut desired_image_count = desired_image_count.max(surface_capabilities.min_image_count); + fn clamp_min_image_count(min_image_count: u32, surface: vk::SurfaceCapabilitiesKHR) -> u32 { + let min_image_count = min_image_count.max(surface.min_image_count); - if surface_capabilities.max_image_count != 0 { - desired_image_count = desired_image_count.min(surface_capabilities.max_image_count); + if surface.max_image_count == 0 { + return min_image_count; } - desired_image_count.min(u8::MAX as u32) + min_image_count.min(surface.max_image_count) } #[profiling::function] - fn destroy_swapchain(device: &Device, swapchain: &mut vk::SwapchainKHR) { - if *swapchain != vk::SwapchainKHR::null() { - // wait for device to be finished with swapchain before destroying it. - // This avoid crashes when resizing windows - #[cfg(target_os = "macos")] - if let Err(err) = unsafe { device.device_wait_idle() } { - warn!("device_wait_idle() failed: {err}"); - } + fn destroy(device: &Device, handle: &mut vk::SwapchainKHR) { + if *handle == vk::SwapchainKHR::null() { + return; + } - let swapchain_ext = Device::expect_swapchain_ext(device); + // wait for device to be finished with swapchain before destroying it. + // This avoid crashes when resizing windows + #[cfg(target_os = "macos")] + if let Err(err) = unsafe { device.device_wait_idle() } { + warn!("device_wait_idle() failed: {err}"); + } - unsafe { - swapchain_ext.destroy_swapchain(*swapchain, None); - } + let swapchain_ext = Device::expect_swapchain_ext(device); - *swapchain = vk::SwapchainKHR::null(); + unsafe { + swapchain_ext.destroy_swapchain(*handle, None); } - } - /// Gets information about this swapchain. - pub fn info(&self) -> SwapchainInfo { - self.info.clone() + *handle = vk::SwapchainKHR::null(); } /// Presents an image which has been previously acquired using @@ -193,71 +310,92 @@ impl Swapchain { wait_semaphores: &[vk::Semaphore], queue_family_index: u32, queue_index: u32, - ) { - let queue_family_index = queue_family_index as usize; - let queue_index = queue_index as usize; + ) -> Result<(), DriverError> { + let device = &self.surface.device; + let queue_signal = &mut self.queue_family_signals[queue_family_index as usize] + .queue_signals[queue_index as usize]; - debug_assert!( - queue_family_index < self.device.physical_device.queue_families.len(), - "Queue family index must be within the range of the available queues created by the device." - ); - debug_assert!( - queue_index - < self.device.physical_device.queue_families[queue_family_index].queue_count - as usize, - "Queue index must be within the range of the available queues created by the device." - ); + let swapchain_ext = Device::expect_swapchain_ext(device); - let present_info = vk::PresentInfoKHR::default() - .wait_semaphores(wait_semaphores) - .swapchains(slice::from_ref(&self.swapchain)) - .image_indices(slice::from_ref(&image.image_idx)); + Device::with_queue(device, queue_family_index, queue_index, |queue| { + unsafe { + match swapchain_ext.queue_present( + queue, + &vk::PresentInfoKHR::default() + .wait_semaphores(wait_semaphores) + .swapchains(slice::from_ref(&self.handle)) + .image_indices(slice::from_ref(&image.index)), + ) { + Ok(suboptimal) => self.suboptimal = suboptimal, + Err(err) + if err == vk::Result::ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT + || err == vk::Result::ERROR_OUT_OF_DATE_KHR + || err == vk::Result::SUBOPTIMAL_KHR => + { + self.suboptimal = true + } + Err(err) + if err == vk::Result::ERROR_DEVICE_LOST + || err == vk::Result::ERROR_SURFACE_LOST_KHR => + { + info!("skipping present: {err}"); - let swapchain_ext = Device::expect_swapchain_ext(&self.device); + self.suboptimal = true; + } + Err(err) => { + // Probably: + // VK_ERROR_OUT_OF_HOST_MEMORY + // VK_ERROR_OUT_OF_DEVICE_MEMORY + warn!("failed to present: {err}"); - unsafe { - match swapchain_ext.queue_present( - self.device.queues[queue_family_index][queue_index], - &present_info, - ) { - Ok(_) => { - Self::destroy_swapchain(&self.device, &mut self.old_swapchain); - } - Err(err) - if err == vk::Result::ERROR_DEVICE_LOST - || err == vk::Result::ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT - || err == vk::Result::ERROR_OUT_OF_DATE_KHR - || err == vk::Result::ERROR_SURFACE_LOST_KHR - || err == vk::Result::SUBOPTIMAL_KHR => - { - // Handled in the next frame - self.suboptimal = true; - } - Err(err) => { - // Probably: - // VK_ERROR_OUT_OF_HOST_MEMORY - // VK_ERROR_OUT_OF_DEVICE_MEMORY - warn!("{err}"); + self.suboptimal = true; + } } } + + if queue_signal.queued { + Device::wait_for_fence(device, &queue_signal.fence)?; + Device::reset_fences(device, slice::from_ref(&queue_signal.fence))?; + } + + Device::queue_submit( + device, + queue, + &[vk::SubmitInfo::default().command_buffers(slice::from_ref(&queue_signal.cmd))], + queue_signal.fence, + )?; + queue_signal.queued = true; + + Ok(()) + })?; + + if !self.prev_swapchain.handle.is_null() { + let QueueSignal { fence, queued, .. } = &mut self.queue_family_signals + [self.prev_swapchain.queue_family_index as usize] + .queue_signals[self.prev_swapchain.queue_index as usize]; + Device::wait_for_fence(device, &*fence)?; + Device::reset_fences(device, slice::from_ref(&*fence))?; + *queued = false; + + Self::destroy(device, &mut self.prev_swapchain.handle); } - let image_idx = image.image_idx as usize; + let image_idx = image.index as usize; self.images[image_idx] = image; + + self.prev_queue = (queue_family_index, queue_index); + + Ok(()) } #[profiling::function] - fn recreate_swapchain(&mut self) -> Result<(), DriverError> { - Self::destroy_swapchain(&self.device, &mut self.old_swapchain); + fn recreate(&mut self) -> Result<(), DriverError> { + self.wait_for_all_signals()?; + Self::destroy(&self.surface.device, &mut self.prev_swapchain.handle); let surface_caps = Surface::capabilities(&self.surface)?; - let present_modes = Surface::present_modes(&self.surface)?; - - let desired_image_count = - Self::clamp_desired_image_count(self.info.desired_image_count, surface_caps); - + let min_image_count = Self::clamp_min_image_count(self.info.min_image_count, surface_caps); let image_usage = self.supported_surface_usage(surface_caps.supported_usage_flags)?; - let (surface_width, surface_height) = match surface_caps.current_extent.width { std::u32::MAX => ( // TODO: Maybe handle this case with aspect-correct clamping? @@ -277,17 +415,14 @@ impl Swapchain { }; if surface_width * surface_height == 0 { + warn!( + "invalid surface extent: computed {}x{}", + surface_width, surface_height + ); + return Err(DriverError::Unsupported); } - let present_mode = self - .info - .present_modes - .iter() - .copied() - .find(|mode| present_modes.contains(mode)) - .unwrap_or(vk::PresentModeKHR::FIFO); - let pre_transform = if surface_caps .supported_transforms .contains(vk::SurfaceTransformFlagsKHR::IDENTITY) @@ -297,10 +432,10 @@ impl Swapchain { surface_caps.current_transform }; - let swapchain_ext = Device::expect_swapchain_ext(&self.device); + let swapchain_ext = Device::expect_swapchain_ext(&self.surface.device); let swapchain_create_info = vk::SwapchainCreateInfoKHR::default() - .surface(*self.surface) - .min_image_count(desired_image_count) + .surface(self.surface.handle) + .min_image_count(min_image_count) .image_color_space(self.info.surface.color_space) .image_format(self.info.surface.format) .image_extent(vk::Extent2D { @@ -311,31 +446,41 @@ impl Swapchain { .image_sharing_mode(vk::SharingMode::EXCLUSIVE) .pre_transform(pre_transform) .composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE) - .present_mode(present_mode) + .present_mode(self.info.present_mode) .clipped(true) - .old_swapchain(self.swapchain) + .old_swapchain(self.handle) .image_array_layers(1); let swapchain = unsafe { swapchain_ext.create_swapchain(&swapchain_create_info, None) } .map_err(|err| { - warn!("{err}"); + warn!("unable to create swapchain: {err}"); DriverError::Unsupported })?; let images = unsafe { swapchain_ext.get_swapchain_images(swapchain) }.map_err(|err| match err { - vk::Result::INCOMPLETE => DriverError::InvalidData, + vk::Result::INCOMPLETE => { + warn!("invalid swapchain image enumeration: incomplete"); + + DriverError::InvalidData + } vk::Result::ERROR_OUT_OF_DEVICE_MEMORY | vk::Result::ERROR_OUT_OF_HOST_MEMORY => { + warn!("unable to get swapchain images: {err}"); + DriverError::OutOfMemory } - _ => DriverError::Unsupported, + _ => { + warn!("unable to get swapchain images: {err}"); + + DriverError::Unsupported + } })?; let images = images .into_iter() .enumerate() .map(|(image_idx, image)| { let mut image = Image::from_raw( - &self.device, + &self.surface.device, image, ImageInfo::image_2d( surface_width, @@ -349,9 +494,10 @@ impl Swapchain { image.name = Some(format!("swapchain{image_idx}")); Ok(SwapchainImage { - exec_idx: 0, - image, - image_idx, + read_only: ReadOnlySwapchainImage { + image, + index: image_idx, + }, }) }) .collect::, _>>()?; @@ -359,14 +505,17 @@ impl Swapchain { self.info.height = surface_height; self.info.width = surface_width; self.images = images; - self.old_swapchain = self.swapchain; - self.swapchain = swapchain; + self.prev_swapchain.handle = self.handle; + self.prev_swapchain.queue_family_index = self.prev_queue.0; + self.prev_swapchain.queue_index = self.prev_queue.1; + self.handle = swapchain; self.suboptimal = false; info!( - "swapchain {}x{} {present_mode:?}x{} {:?} {image_usage:#?}", + "swapchain {}x{} {:?}x{} {:?} {image_usage:#?}", self.info.width, self.info.height, + self.info.present_mode, self.images.len(), self.info.surface.format, ); @@ -374,7 +523,7 @@ impl Swapchain { Ok(()) } - /// Sets information about this swapchain. + /// Updates the information which controls this swapchain. /// /// Previously acquired swapchain images should be discarded after calling this function. pub fn set_info(&mut self, info: impl Into) { @@ -383,7 +532,7 @@ impl Swapchain { if self.info != info { // attempt to reducing flickering when resizing windows on mac #[cfg(target_os = "macos")] - if let Err(err) = unsafe { self.device.device_wait_idle() } { + if let Err(err) = unsafe { self.surface.device.device_wait_idle() } { warn!("device_wait_idle() failed: {err}"); } @@ -407,21 +556,24 @@ impl Swapchain { continue; } - if Device::image_format_properties( - &self.device, - self.info.surface.format, - vk::ImageType::TYPE_2D, - vk::ImageTiling::OPTIMAL, - usage, - vk::ImageCreateFlags::empty(), - ) - .inspect_err(|err| { - warn!( - "unable to get image format properties: {:?} {:?} {err}", - self.info.surface.format, usage + if self + .surface + .device + .physical_device + .image_format_properties( + self.info.surface.format, + vk::ImageType::TYPE_2D, + vk::ImageTiling::OPTIMAL, + usage, + vk::ImageCreateFlags::empty(), ) - })? - .is_none() + .inspect_err(|err| { + warn!( + "unable to get image format properties: {:?} {:?} {err}", + self.info.surface.format, usage + ) + })? + .is_none() { continue; } @@ -435,6 +587,36 @@ impl Swapchain { Ok(res) } + + /// Waits for all tracked per-queue fences to become signaled and resets their state. + fn wait_for_all_signals(&mut self) -> Result<(), DriverError> { + let fences = self + .queue_family_signals + .iter() + .flat_map(|queue_family| { + queue_family + .queue_signals + .iter() + .filter_map(|queue| queue.queued.then_some(queue.fence)) + }) + .collect::>(); + if fences.is_empty() { + return Ok(()); + } + + let device = &self.surface.device; + + Device::wait_for_fences(device, &fences)?; + Device::reset_fences(device, &fences)?; + + for queue_family in &mut self.queue_family_signals { + for queue in &mut queue_family.queue_signals { + queue.queued = false; + } + } + + Ok(()) + } } impl Drop for Swapchain { @@ -444,8 +626,38 @@ impl Drop for Swapchain { return; } - Self::destroy_swapchain(&self.device, &mut self.old_swapchain); - Self::destroy_swapchain(&self.device, &mut self.swapchain); + if let Err(err) = self.wait_for_all_signals() { + warn!("unable to wait for swapchain signals: {err}"); + } + + let device = &self.surface.device; + + Self::destroy(device, &mut self.handle); + Self::destroy(device, &mut self.prev_swapchain.handle); + + for queue_family in &self.queue_family_signals { + let mut cmd_bufs = Vec::with_capacity(queue_family.queue_signals.len()); + for queue in &queue_family.queue_signals { + cmd_bufs.push(queue.cmd); + + unsafe { + device.destroy_fence(queue.fence, None); + } + } + + unsafe { + device.free_command_buffers(queue_family.cmd_pool, &cmd_bufs); + device.destroy_command_pool(queue_family.cmd_pool, None); + } + } + } +} + +impl Eq for Swapchain {} + +impl PartialEq for Swapchain { + fn eq(&self, other: &Self) -> bool { + self.handle == other.handle } } @@ -464,29 +676,33 @@ pub enum SwapchainError { /// An opaque type representing a swapchain image. #[derive(Debug)] +#[read_only::embed] pub struct SwapchainImage { - pub(crate) exec_idx: usize, + /// The underlying image resource. + /// + /// _Note:_ This field is read-only. + #[readonly] image: Image, - image_idx: u32, -} -impl SwapchainImage { - pub(crate) fn clone_swapchain(this: &Self) -> Self { - let Self { - exec_idx, - image, - image_idx, - } = this; + /// The swapchain image index. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub index: u32, +} +impl Clone for SwapchainImage { + fn clone(&self) -> Self { Self { - exec_idx: *exec_idx, - image: Image::clone_swapchain(image), - image_idx: *image_idx, + read_only: ReadOnlySwapchainImage { + image: unsafe { self.image.to_detached() }, + index: self.index, + }, } } } -impl Deref for SwapchainImage { +impl Deref for ReadOnlySwapchainImage { type Target = Image; fn deref(&self) -> &Self::Target { @@ -495,71 +711,82 @@ impl Deref for SwapchainImage { } /// Information used to create a [`Swapchain`] instance. -#[derive(Builder, Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Builder, Clone, Copy, Debug, Eq, Hash, PartialEq)] #[builder( build_fn(private, name = "fallible_build", error = "SwapchainInfoBuilderError"), - derive(Clone, Debug), + derive(Clone, Copy, Debug), pattern = "owned" )] -#[non_exhaustive] pub struct SwapchainInfo { - /// The desired, but not guaranteed, number of images that will be in the created swapchain. - /// - /// More images introduces more display lag, but smoother animation. - #[builder(default = "3")] - pub desired_image_count: u32, - /// The initial height of the surface. pub height: u32, - /// The format and color space of the surface. - pub surface: vk::SurfaceFormatKHR, + /// The minimum number of presentable images that the application needs. The implementation + /// will either create the swapchain with at least that many images, or it will fail to + /// create the swapchain. + /// + /// More images introduce more display lag, but smoother animation. + #[builder(default = "2")] + pub min_image_count: u32, - /// `vk::PresentModeKHR` Determines timing and queueing with which frames are actually displayed to the user. - /// `present_modes` is a set of these modes ordered by preference. If the first mode is not available it will fall - /// back to the next, etc... + /// `vk::PresentModeKHR` determines timing and queueing with which frames are actually + /// displayed to the user. /// - /// `vk::PresentModeKHR::FIFO` - Presentation frames are kept in a First-In-First-Out queue approximately 3 frames - /// long. Every vertical blanking period, the presentation engine will pop a frame off the queue to display. If - /// there is no frame to display, it will present the same frame again until the next vblank. + /// `vk::PresentModeKHR::FIFO` - Presentation frames are kept in a First-In-First-Out queue + /// approximately 3 frames long. Every vertical blanking period, the presentation engine + /// will pop a frame off the queue to display. If there is no frame to display, it will + /// present the same frame again until the next vblank. /// /// When a present command is executed on the GPU, the presented image is added on the queue. /// /// * **Tearing:** No tearing will be observed. /// * **Also known as**: "Vsync On" /// - /// `vk::PresentModeKHR::FIFO_RELAXED` - Presentation frames are kept in a First-In-First-Out queue approximately 3 - /// frames long. Every vertical blanking period, the presentation engine will pop a frame off the queue to display. - /// If there is no frame to display, it will present the same frame until there is a frame in the queue. The moment - /// there is a frame in the queue, it will immediately pop the frame off the queue. + /// `vk::PresentModeKHR::FIFO_RELAXED` - Presentation frames are kept in a First-In-First-Out + /// queue approximately 3 frames long. Every vertical blanking period, the presentation + /// engine will pop a frame off the queue to display. If there is no frame to display, it + /// will present the same frame until there is a frame in the queue. The moment there is a + /// frame in the queue, it will immediately pop the frame off the queue. /// /// When a present command is executed on the GPU, the presented image is added on the queue. /// - /// * **Tearing**: - /// Tearing will be observed if frames last more than one vblank as the front buffer. + /// * **Tearing**: Tearing will be observed if frames last more than one vblank as the front + /// buffer. /// * **Also known as**: "Adaptive Vsync" /// - /// `vk::PresentModeKHR::IMMEDIATE` - Presentation frames are not queued at all. The moment a present command is - /// executed on the GPU, the presented image is swapped onto the front buffer immediately. + /// `vk::PresentModeKHR::IMMEDIATE` - Presentation frames are not queued at all. The moment a + /// present command is executed on the GPU, the presented image is swapped onto the front + /// buffer immediately. /// /// * **Tearing**: Tearing can be observed. /// * **Also known as**: "Vsync Off" /// - /// `vk::PresentModeKHR::MAILBOX` - Presentation frames are kept in a single-frame queue. Every vertical blanking - /// period, the presentation engine will pop a frame from the queue. If there is no frame to display, it will - /// present the same frame again until the next vblank. + /// `vk::PresentModeKHR::MAILBOX` - Presentation frames are kept in a single-frame queue. Every + /// vertical blanking period, the presentation engine will pop a frame from the queue. If + /// there is no frame to display, it will present the same frame again until the next + /// vblank. /// /// When a present command is executed on the GPU, the frame will be put into the queue. - /// If there was already a frame in the queue, the new frame will _replace_ the old frame + /// If there was already a frame in the queue, the new frame will _replace_ the old frame. /// on the queue. /// /// * **Tearing**: No tearing will be observed. /// * **Also known as**: "Fast Vsync" - #[builder(default = vec![vk::PresentModeKHR::FIFO_RELAXED, vk::PresentModeKHR::FIFO])] - pub present_modes: Vec, + #[builder(default = vk::PresentModeKHR::IMMEDIATE)] + pub present_mode: vk::PresentModeKHR, + + /// The format and color space of the surface. + pub surface: vk::SurfaceFormatKHR, /// The initial width of the surface. pub width: u32, + + /// NOTE: This field does not do anything, use the new one. + #[builder(default)] + #[builder_field_attr(deprecated = "use min_image_count field")] + #[builder_setter_attr(deprecated = "use min_image_count field")] + #[deprecated = "use min_image_count field"] + pub desired_image_count: u32, } impl SwapchainInfo { @@ -567,25 +794,39 @@ impl SwapchainInfo { #[inline(always)] pub fn new(width: u32, height: u32, surface: vk::SurfaceFormatKHR) -> SwapchainInfo { Self { - width, + #[allow(deprecated)] + desired_image_count: 0, height, + min_image_count: 2, + present_mode: vk::PresentModeKHR::IMMEDIATE, surface, - desired_image_count: 3, - present_modes: vec![vk::PresentModeKHR::FIFO_RELAXED, vk::PresentModeKHR::FIFO], + width, } } + /// Creates a default `SwapchainInfoBuilder`. + pub fn builder() -> SwapchainInfoBuilder { + Default::default() + } + /// Converts a `SwapchainInfo` into a `SwapchainInfoBuilder`. - #[inline(always)] - pub fn to_builder(self) -> SwapchainInfoBuilder { + pub fn into_builder(self) -> SwapchainInfoBuilder { SwapchainInfoBuilder { + #[allow(deprecated)] desired_image_count: Some(self.desired_image_count), height: Some(self.height), + min_image_count: Some(self.min_image_count), + present_mode: Some(self.present_mode), surface: Some(self.surface), - present_modes: Some(self.present_modes), width: Some(self.width), } } + + #[deprecated = "use into_builder function"] + #[doc(hidden)] + pub fn to_builder(self) -> SwapchainInfoBuilder { + self.into_builder() + } } impl From for SwapchainInfo { @@ -599,7 +840,7 @@ impl SwapchainInfoBuilder { /// /// # Panics /// - /// If any of the following values have not been set this function will panic: + /// If any of the following values have not been set this function will panic. /// /// * `width` /// * `height` @@ -622,8 +863,22 @@ impl From for SwapchainInfoBuilderError { } } +mod deprecated { + use ash::vk; + + use crate::driver::swapchain::SwapchainInfoBuilder; + + impl SwapchainInfoBuilder { + #[deprecated = "use present_mode function"] + #[doc(hidden)] + pub fn present_modes(self, modes: impl Into>) -> Self { + self.present_mode(modes.into()[0]) + } + } +} + #[cfg(test)] -mod tests { +mod test { use super::*; type Info = SwapchainInfo; @@ -632,7 +887,7 @@ mod tests { #[test] pub fn swapchain_info() { let info = Info::new(20, 24, vk::SurfaceFormatKHR::default()); - let builder = info.clone().to_builder().build(); + let builder = info.into_builder().build(); assert_eq!(info, builder); } diff --git a/src/graph/binding.rs b/src/graph/binding.rs deleted file mode 100644 index 6aa42981..00000000 --- a/src/graph/binding.rs +++ /dev/null @@ -1,297 +0,0 @@ -use { - super::{ - AccelerationStructureLeaseNode, AccelerationStructureNode, BufferLeaseNode, BufferNode, - ImageLeaseNode, ImageNode, RenderGraph, - }, - crate::{ - driver::{ - accel_struct::AccelerationStructure, buffer::Buffer, image::Image, - swapchain::SwapchainImage, - }, - pool::Lease, - }, - std::{fmt::Debug, sync::Arc}, -}; - -// #[derive(Debug)] -// pub enum AnyBufferBinding<'a> { -// Buffer(&'a Arc), -// BufferLease(&'a Lease), -// } - -// impl<'a> From<&'a Arc> for AnyBufferBinding<'a> { -// fn from(buffer: &'a Arc) -> Self { -// Self::Buffer(buffer) -// } -// } - -// impl<'a> From<&'a Lease> for AnyBufferBinding<'a> { -// fn from(buffer: &'a Lease) -> Self { -// Self::BufferLease(buffer) -// } -// } - -// #[derive(Debug)] -// pub enum AnyImageBinding<'a> { -// Image(&'a Arc), -// ImageLease(&'a Lease), -// SwapchainImage(&'a SwapchainImage), -// } - -// impl<'a> From<&'a Arc> for AnyImageBinding<'a> { -// fn from(image: &'a Arc) -> Self { -// Self::Image(image) -// } -// } - -// impl<'a> From<&'a Lease> for AnyImageBinding<'a> { -// fn from(image: &'a Lease) -> Self { -// Self::ImageLease(image) -// } -// } - -// impl<'a> From<&'a SwapchainImage> for AnyImageBinding<'a> { -// fn from(image: &'a SwapchainImage) -> Self { -// Self::SwapchainImage(image) -// } -// } - -/// A trait for resources which may be bound to a `RenderGraph`. -/// -/// See [`RenderGraph::bind_node`] and -/// [`PassRef::bind_pipeline`](super::pass_ref::PassRef::bind_pipeline) for details. -pub trait Bind { - /// Binds the resource to a graph-like object. - /// - /// Returns a reference Node object. - fn bind(self, graph: Graph) -> Node; -} - -#[derive(Debug)] -pub enum Binding { - AccelerationStructure(Arc, bool), - AccelerationStructureLease(Arc>, bool), - Buffer(Arc, bool), - BufferLease(Arc>, bool), - Image(Arc, bool), - ImageLease(Arc>, bool), - SwapchainImage(Box, bool), -} - -impl Binding { - pub(super) fn as_driver_acceleration_structure(&self) -> Option<&AccelerationStructure> { - Some(match self { - Self::AccelerationStructure(binding, _) => binding, - Self::AccelerationStructureLease(binding, _) => binding, - _ => return None, - }) - } - - pub(super) fn as_driver_buffer(&self) -> Option<&Buffer> { - Some(match self { - Self::Buffer(binding, _) => binding, - Self::BufferLease(binding, _) => binding, - _ => return None, - }) - } - - pub(super) fn as_driver_image(&self) -> Option<&Image> { - Some(match self { - Self::Image(binding, _) => binding, - Self::ImageLease(binding, _) => binding, - Self::SwapchainImage(binding, _) => binding, - _ => return None, - }) - } - - pub(super) fn is_bound(&self) -> bool { - match self { - Self::AccelerationStructure(_, is_bound) => *is_bound, - Self::AccelerationStructureLease(_, is_bound) => *is_bound, - Self::Buffer(_, is_bound) => *is_bound, - Self::BufferLease(_, is_bound) => *is_bound, - Self::Image(_, is_bound) => *is_bound, - Self::ImageLease(_, is_bound) => *is_bound, - Self::SwapchainImage(_, is_bound) => *is_bound, - } - } - - pub(super) fn unbind(&mut self) { - *match self { - Self::AccelerationStructure(_, is_bound) => is_bound, - Self::AccelerationStructureLease(_, is_bound) => is_bound, - Self::Buffer(_, is_bound) => is_bound, - Self::BufferLease(_, is_bound) => is_bound, - Self::Image(_, is_bound) => is_bound, - Self::ImageLease(_, is_bound) => is_bound, - Self::SwapchainImage(_, is_bound) => is_bound, - } = false; - } -} - -macro_rules! bind { - ($name:ident) => { - paste::paste! { - impl Bind<&mut RenderGraph, [<$name Node>]> for $name { - #[profiling::function] - fn bind(self, graph: &mut RenderGraph) -> [<$name Node>] { - // In this function we are binding a new item (Image or Buffer or etc) - - // We will return a new node - let res = [<$name Node>]::new(graph.bindings.len()); - let binding = Binding::$name(Arc::new(self), true); - graph.bindings.push(binding); - - res - } - } - - impl<'a> Bind<&mut RenderGraph, [<$name Node>]> for &'a Arc<$name> { - fn bind(self, graph: &mut RenderGraph) -> [<$name Node>] { - // In this function we are binding a borrowed binding (&Arc or - // &Arc or etc) - - Arc::clone(self).bind(graph) - } - } - - impl Bind<&mut RenderGraph, [<$name Node>]> for Arc<$name> { - #[profiling::function] - fn bind(self, graph: &mut RenderGraph) -> [<$name Node>] { - // In this function we are binding an existing binding (Arc or - // Arc or etc) - - // We will return an existing node, if possible - // TODO: Could store a sorted list of these shared pointers to avoid the O(N) - for (idx, existing_binding) in graph.bindings.iter_mut().enumerate() { - if let Some((existing_binding, is_bound)) = existing_binding.[]() { - if Arc::ptr_eq(existing_binding, &self) { - *is_bound = true; - - return [<$name Node>]::new(idx); - } - } - } - - // Return a new node - let res = [<$name Node>]::new(graph.bindings.len()); - let binding = Binding::$name(self, true); - graph.bindings.push(binding); - - res - } - } - - impl Binding { - pub(super) fn [](&self) -> Option<&Arc<$name>> { - if let Self::$name(binding, _) = self { - Some(&binding) - } else { - None - } - } - - pub(super) fn [](&mut self) -> Option<(&mut Arc<$name>, &mut bool)> { - if let Self::$name(binding, is_bound) = self { - Some((binding, is_bound)) - } else { - None - } - } - } - } - }; -} - -bind!(AccelerationStructure); -bind!(Image); -bind!(Buffer); - -macro_rules! bind_lease { - ($name:ident) => { - paste::paste! { - impl Bind<&mut RenderGraph, [<$name LeaseNode>]> for Lease<$name> { - #[profiling::function] - fn bind(self, graph: &mut RenderGraph) -> [<$name LeaseNode>] { - // In this function we are binding a new lease (Lease or Lease or - // etc) - - // We will return a new node - let res = [<$name LeaseNode>]::new(graph.bindings.len()); - let binding = Binding::[<$name Lease>](Arc::new(self), true); - graph.bindings.push(binding); - - res - } - } - - impl<'a> Bind<&mut RenderGraph, [<$name LeaseNode>]> for &'a Arc> { - fn bind(self, graph: &mut RenderGraph) -> [<$name LeaseNode>] { - // In this function we are binding a borrowed binding (&Arc> or - // &Arc> or etc) - - Arc::clone(self).bind(graph) - } - } - - impl Bind<&mut RenderGraph, [<$name LeaseNode>]> for Arc> { - #[profiling::function] - fn bind(self, graph: &mut RenderGraph) -> [<$name LeaseNode>] { - // In this function we are binding an existing lease binding - // (Arc> or Arc> or etc) - - // We will return an existing node, if possible - // TODO: Could store a sorted list of these shared pointers to avoid the O(N) - for (idx, existing_binding) in graph.bindings.iter_mut().enumerate() { - if let Some((existing_binding, is_bound)) = existing_binding.[]() { - if Arc::ptr_eq(existing_binding, &self) { - *is_bound = true; - - return [<$name LeaseNode>]::new(idx); - } - } - } - - // We will return a new node - let res = [<$name LeaseNode>]::new(graph.bindings.len()); - let binding = Binding::[<$name Lease>](self, true); - graph.bindings.push(binding); - - res - } - } - - impl Binding { - pub(super) fn [](&self) -> Option<&Arc>> { - if let Self::[<$name Lease>](binding, _) = self { - Some(binding) - } else { - None - } - } - - pub(super) fn [](&mut self) -> Option<(&Arc>, &mut bool)> { - if let Self::[<$name Lease>](binding, is_bound) = self { - Some((binding, is_bound)) - } else { - None - } - } - } - } - } -} - -bind_lease!(AccelerationStructure); -bind_lease!(Image); -bind_lease!(Buffer); - -/// A trait for resources which may be unbound from a `RenderGraph`. -/// -/// See [`RenderGraph::unbind_node`] for details. -pub trait Unbind { - /// Unbinds the resource from a graph-like object. - /// - /// Returns the original Binding object. - fn unbind(self, graph: &mut Graph) -> Binding; -} diff --git a/src/graph/edge.rs b/src/graph/edge.rs deleted file mode 100644 index 69e17fe9..00000000 --- a/src/graph/edge.rs +++ /dev/null @@ -1,108 +0,0 @@ -use { - super::{ - AccelerationStructureLeaseNode, AccelerationStructureNode, BufferLeaseNode, BufferNode, - ImageLeaseNode, ImageNode, RenderGraph, Resolver, SwapchainImageNode, - pass_ref::{PassRef, PipelinePassRef}, - }, - crate::{ - driver::{ - accel_struct::AccelerationStructure, buffer::Buffer, compute::ComputePipeline, - graphic::GraphicPipeline, image::Image, ray_trace::RayTracePipeline, - swapchain::SwapchainImage, - }, - pool::Lease, - }, - std::sync::Arc, -}; - -/// A marker trait that says some graph object can transition into a different -/// graph object; it is a one-way transition unless the other direction has -/// been implemented too. -pub trait Edge { - type Result; -} - -macro_rules! graph_edge { - ($src:ty => $dst:ty) => { - impl Edge for $src { - type Result = $dst; - } - }; -} - -// Edges that can be bound as nodes to the render graph: -// Ex: RenderGraph::bind_node(&mut self, binding: X) -> Y -graph_edge!(AccelerationStructure => AccelerationStructureNode); -graph_edge!(Arc => AccelerationStructureNode); -graph_edge!(Lease => AccelerationStructureLeaseNode); -graph_edge!(Arc> => AccelerationStructureLeaseNode); -graph_edge!(Buffer => BufferNode); -graph_edge!(Arc => BufferNode); -graph_edge!(Lease => BufferLeaseNode); -graph_edge!(Arc> => BufferLeaseNode); -graph_edge!(Image => ImageNode); -graph_edge!(Arc => ImageNode); -graph_edge!(Lease => ImageLeaseNode); -graph_edge!(Arc> => ImageLeaseNode); -graph_edge!(SwapchainImage => SwapchainImageNode); - -// Edges that can be unbound from the render graph: -// Ex: RenderGraph::unbind_node(&mut self, node: X) -> Y -graph_edge!(AccelerationStructureNode => Arc); -graph_edge!(AccelerationStructureLeaseNode => Arc>); -graph_edge!(BufferNode => Arc); -graph_edge!(BufferLeaseNode => Arc>); -graph_edge!(ImageNode => Arc); -graph_edge!(ImageLeaseNode => Arc>); -graph_edge!(SwapchainImageNode => SwapchainImage); - -macro_rules! graph_edge_borrow { - ($src:ty => $dst:ty) => { - impl<'a> Edge for &'a $src { - type Result = $dst; - } - }; -} - -graph_edge_borrow!(Arc => AccelerationStructureNode); -graph_edge_borrow!(Arc> => AccelerationStructureLeaseNode); -graph_edge_borrow!(Arc => BufferNode); -graph_edge_borrow!(Arc> => BufferLeaseNode); -graph_edge_borrow!(Arc => ImageNode); -graph_edge_borrow!(Arc> => ImageLeaseNode); - -// Specialized edges for pipelines added to a pass: -// Ex: PassRef::bind_pipeline(&mut self, pipeline: X) -> PipelinePassRef -macro_rules! pipeline_edge { - ($name:ident) => { - paste::paste! { - impl<'a> Edge> for &'a Arc<[<$name Pipeline>]> { - type Result = PipelinePassRef<'a, [<$name Pipeline>]>; - } - - impl<'a> Edge> for Arc<[<$name Pipeline>]> { - type Result = PipelinePassRef<'a, [<$name Pipeline>]>; - } - - impl<'a> Edge> for [<$name Pipeline>] { - type Result = PipelinePassRef<'a, [<$name Pipeline>]>; - } - } - }; -} - -pipeline_edge!(Compute); -pipeline_edge!(Graphic); -pipeline_edge!(RayTrace); - -macro_rules! resolver_edge { - ($src:ident -> $dst:ident) => { - impl Edge for $src { - type Result = $dst; - } - }; -} - -// Edges that can be unbound from a resolved render graph: -// (You get the full real actual swapchain image woo hoo!) -resolver_edge!(SwapchainImageNode -> SwapchainImage); diff --git a/src/graph/info.rs b/src/graph/info.rs deleted file mode 100644 index 834337f2..00000000 --- a/src/graph/info.rs +++ /dev/null @@ -1,37 +0,0 @@ -use { - super::{ - AccelerationStructureLeaseNode, AccelerationStructureNode, BufferLeaseNode, BufferNode, - ImageLeaseNode, ImageNode, RenderGraph, SwapchainImageNode, - }, - crate::driver::{ - accel_struct::AccelerationStructureInfo, buffer::BufferInfo, image::ImageInfo, - }, -}; - -pub trait Information { - type Info; - - fn get(self, graph: &RenderGraph) -> Self::Info; -} - -macro_rules! information { - ($name:ident: $src:ident -> $dst:ident) => { - paste::paste! { - impl Information for $src { - type Info = $dst; - - fn get(self, graph: &RenderGraph) -> $dst { - graph.bindings[self.idx].[]().unwrap().info - } - } - } - }; -} - -information!(acceleration_structure: AccelerationStructureNode -> AccelerationStructureInfo); -information!(acceleration_structure_lease: AccelerationStructureLeaseNode -> AccelerationStructureInfo); -information!(buffer: BufferNode -> BufferInfo); -information!(buffer_lease: BufferLeaseNode -> BufferInfo); -information!(image: ImageNode -> ImageInfo); -information!(image_lease: ImageLeaseNode -> ImageInfo); -information!(swapchain_image: SwapchainImageNode -> ImageInfo); diff --git a/src/graph/mod.rs b/src/graph/mod.rs deleted file mode 100644 index 7d9e8809..00000000 --- a/src/graph/mod.rs +++ /dev/null @@ -1,1035 +0,0 @@ -//! Rendering operations and command submission. -//! -//! - -pub mod node; -pub mod pass_ref; - -mod binding; -mod edge; -mod info; -mod resolver; -mod swapchain; - -pub use self::{ - binding::{Bind, Unbind}, - resolver::Resolver, -}; - -use { - self::{ - binding::Binding, - edge::Edge, - info::Information, - node::Node, - node::{ - AccelerationStructureLeaseNode, AccelerationStructureNode, - AnyAccelerationStructureNode, AnyBufferNode, AnyImageNode, BufferLeaseNode, BufferNode, - ImageLeaseNode, ImageNode, SwapchainImageNode, - }, - pass_ref::{AttachmentIndex, Bindings, Descriptor, PassRef, SubresourceAccess, ViewType}, - }, - crate::driver::{ - DescriptorBindingMap, - buffer::Buffer, - compute::ComputePipeline, - device::Device, - format_aspect_mask, format_texel_block_extent, format_texel_block_size, - graphic::{DepthStencilMode, GraphicPipeline}, - image::{ImageInfo, ImageViewInfo, SampleCount}, - image_subresource_range_from_layers, - ray_trace::RayTracePipeline, - render_pass::ResolveMode, - shader::PipelineDescriptorInfo, - }, - ash::vk, - std::{ - cmp::Ord, - collections::{BTreeMap, HashMap}, - fmt::{Debug, Formatter}, - ops::Range, - sync::Arc, - }, - vk_sync::AccessType, -}; - -type ExecFn = Box) + Send>; -type NodeIndex = usize; - -#[derive(Clone, Copy, Debug)] -struct Area { - height: u32, - width: u32, - x: i32, - y: i32, -} - -#[derive(Clone, Copy, Debug)] -struct Attachment { - array_layer_count: u32, - aspect_mask: vk::ImageAspectFlags, - base_array_layer: u32, - base_mip_level: u32, - format: vk::Format, - mip_level_count: u32, - sample_count: SampleCount, - target: NodeIndex, -} - -impl Attachment { - fn new(image_view_info: ImageViewInfo, sample_count: SampleCount, target: NodeIndex) -> Self { - Self { - array_layer_count: image_view_info.array_layer_count, - aspect_mask: image_view_info.aspect_mask, - base_array_layer: image_view_info.base_array_layer, - base_mip_level: image_view_info.base_mip_level, - format: image_view_info.fmt, - mip_level_count: image_view_info.mip_level_count, - sample_count, - target, - } - } - - fn are_compatible(lhs: Option, rhs: Option) -> bool { - // Two attachment references are compatible if they have matching format and sample - // count, or are both VK_ATTACHMENT_UNUSED or the pointer that would contain the - // reference is NULL. - if lhs.is_none() || rhs.is_none() { - return true; - } - - Self::are_identical(lhs.unwrap(), rhs.unwrap()) - } - - fn are_identical(lhs: Self, rhs: Self) -> bool { - lhs.array_layer_count == rhs.array_layer_count - && lhs.base_array_layer == rhs.base_array_layer - && lhs.base_mip_level == rhs.base_mip_level - && lhs.format == rhs.format - && lhs.mip_level_count == rhs.mip_level_count - && lhs.sample_count == rhs.sample_count - && lhs.target == rhs.target - } - - fn image_view_info(self, image_info: ImageInfo) -> ImageViewInfo { - image_info - .to_builder() - .array_layer_count(self.array_layer_count) - .mip_level_count(self.mip_level_count) - .fmt(self.format) - .build() - .default_view_info() - .to_builder() - .aspect_mask(self.aspect_mask) - .base_array_layer(self.base_array_layer) - .base_mip_level(self.base_mip_level) - .build() - } -} - -/// Specifies a color attachment clear value which can be used to initliaze an image. -#[derive(Clone, Copy, Debug)] -pub struct ClearColorValue(pub [f32; 4]); - -impl From<[f32; 3]> for ClearColorValue { - fn from(color: [f32; 3]) -> Self { - [color[0], color[1], color[2], 1.0].into() - } -} - -impl From<[f32; 4]> for ClearColorValue { - fn from(color: [f32; 4]) -> Self { - Self(color) - } -} - -impl From<[u8; 3]> for ClearColorValue { - fn from(color: [u8; 3]) -> Self { - [color[0], color[1], color[2], u8::MAX].into() - } -} - -impl From<[u8; 4]> for ClearColorValue { - fn from(color: [u8; 4]) -> Self { - [ - color[0] as f32 / u8::MAX as f32, - color[1] as f32 / u8::MAX as f32, - color[2] as f32 / u8::MAX as f32, - color[3] as f32 / u8::MAX as f32, - ] - .into() - } -} - -#[derive(Default)] -struct Execution { - accesses: HashMap>, - bindings: BTreeMap)>, - - correlated_view_mask: u32, - depth_stencil: Option, - render_area: Option, - view_mask: u32, - - color_attachments: HashMap, - color_clears: HashMap, - color_loads: HashMap, - color_resolves: HashMap, - color_stores: HashMap, - depth_stencil_attachment: Option, - depth_stencil_clear: Option<(Attachment, vk::ClearDepthStencilValue)>, - depth_stencil_load: Option, - depth_stencil_resolve: Option<( - Attachment, - AttachmentIndex, - Option, - Option, - )>, - depth_stencil_store: Option, - - func: Option, - pipeline: Option, -} - -impl Debug for Execution { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - // The only field missing is func which cannot easily be implemented because it is a - // FnOnce. - f.debug_struct("Execution") - .field("accesses", &self.accesses) - .field("bindings", &self.bindings) - .field("depth_stencil", &self.depth_stencil) - .field("color_attachments", &self.color_attachments) - .field("color_clears", &self.color_clears) - .field("color_loads", &self.color_loads) - .field("color_resolves", &self.color_resolves) - .field("color_stores", &self.color_stores) - .field("depth_stencil_attachment", &self.depth_stencil_attachment) - .field("depth_stencil_clear", &self.depth_stencil_clear) - .field("depth_stencil_load", &self.depth_stencil_load) - .field("depth_stencil_resolve", &self.depth_stencil_resolve) - .field("depth_stencil_store", &self.depth_stencil_store) - .field("pipeline", &self.pipeline) - .finish() - } -} - -struct ExecutionFunction(ExecFn); - -#[derive(Debug)] -enum ExecutionPipeline { - Compute(Arc), - Graphic(Arc), - RayTrace(Arc), -} - -impl ExecutionPipeline { - fn as_graphic(&self) -> Option<&GraphicPipeline> { - if let Self::Graphic(pipeline) = self { - Some(pipeline) - } else { - None - } - } - - fn bind_point(&self) -> vk::PipelineBindPoint { - match self { - ExecutionPipeline::Compute(_) => vk::PipelineBindPoint::COMPUTE, - ExecutionPipeline::Graphic(_) => vk::PipelineBindPoint::GRAPHICS, - ExecutionPipeline::RayTrace(_) => vk::PipelineBindPoint::RAY_TRACING_KHR, - } - } - - fn descriptor_bindings(&self) -> &DescriptorBindingMap { - match self { - ExecutionPipeline::Compute(pipeline) => &pipeline.descriptor_bindings, - ExecutionPipeline::Graphic(pipeline) => &pipeline.descriptor_bindings, - ExecutionPipeline::RayTrace(pipeline) => &pipeline.descriptor_bindings, - } - } - - fn descriptor_info(&self) -> &PipelineDescriptorInfo { - match self { - ExecutionPipeline::Compute(pipeline) => &pipeline.descriptor_info, - ExecutionPipeline::Graphic(pipeline) => &pipeline.descriptor_info, - ExecutionPipeline::RayTrace(pipeline) => &pipeline.descriptor_info, - } - } - - fn layout(&self) -> vk::PipelineLayout { - match self { - ExecutionPipeline::Compute(pipeline) => pipeline.layout, - ExecutionPipeline::Graphic(pipeline) => pipeline.layout, - ExecutionPipeline::RayTrace(pipeline) => pipeline.layout, - } - } - - fn stage(&self) -> vk::PipelineStageFlags { - match self { - ExecutionPipeline::Compute(_) => vk::PipelineStageFlags::COMPUTE_SHADER, - ExecutionPipeline::Graphic(_) => vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, - ExecutionPipeline::RayTrace(_) => vk::PipelineStageFlags::RAY_TRACING_SHADER_KHR, - } - } -} - -impl Clone for ExecutionPipeline { - fn clone(&self) -> Self { - match self { - Self::Compute(pipeline) => Self::Compute(Arc::clone(pipeline)), - Self::Graphic(pipeline) => Self::Graphic(Arc::clone(pipeline)), - Self::RayTrace(pipeline) => Self::RayTrace(Arc::clone(pipeline)), - } - } -} - -#[derive(Debug)] -struct Pass { - execs: Vec, - name: String, -} - -impl Pass { - fn descriptor_pools_sizes( - &self, - ) -> impl Iterator>> { - self.execs - .iter() - .flat_map(|exec| exec.pipeline.as_ref()) - .map(|pipeline| &pipeline.descriptor_info().pool_sizes) - } -} - -/// A composable graph of render pass operations. -/// -/// `RenderGraph` instances are are intended for one-time use. -/// -/// The design of this code originated with a combination of -/// [`PassBuilder`](https://github.com/EmbarkStudios/kajiya/blob/main/crates/lib/kajiya-rg/src/pass_builder.rs) -/// and -/// [`render_graph.cpp`](https://github.com/Themaister/Granite/blob/master/renderer/render_graph.cpp). -#[derive(Debug)] -pub struct RenderGraph { - bindings: Vec, - passes: Vec, - - /// Set to true (when in debug mode) in order to get a breakpoint hit where you want. - #[cfg(debug_assertions)] - pub debug: bool, -} - -impl RenderGraph { - /// Constructs a new `RenderGraph`. - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - let bindings = vec![]; - let passes = vec![]; - - #[cfg(debug_assertions)] - let debug = false; - - Self { - bindings, - passes, - #[cfg(debug_assertions)] - debug, - } - } - - /// Begins a new pass. - pub fn begin_pass(&mut self, name: impl AsRef) -> PassRef<'_> { - PassRef::new(self, name.as_ref().to_string()) - } - - /// Binds a Vulkan acceleration structure, buffer, or image to this graph. - /// - /// Bound nodes may be used in passes for pipeline and shader operations. - pub fn bind_node<'a, B>(&'a mut self, binding: B) -> >::Result - where - B: Edge, - B: Bind<&'a mut Self, >::Result>, - { - binding.bind(self) - } - - /// Copy an image, potentially performing format conversion. - pub fn blit_image( - &mut self, - src_node: impl Into, - dst_node: impl Into, - filter: vk::Filter, - ) -> &mut Self { - let src_node = src_node.into(); - let dst_node = dst_node.into(); - - let src_info = self.node_info(src_node); - let dst_info = self.node_info(dst_node); - - self.blit_image_region( - src_node, - dst_node, - filter, - vk::ImageBlit { - src_subresource: vk::ImageSubresourceLayers { - aspect_mask: format_aspect_mask(src_info.fmt), - mip_level: 0, - base_array_layer: 0, - layer_count: 1, - }, - src_offsets: [ - vk::Offset3D { x: 0, y: 0, z: 0 }, - vk::Offset3D { - x: src_info.width as _, - y: src_info.height as _, - z: src_info.depth as _, - }, - ], - dst_subresource: vk::ImageSubresourceLayers { - aspect_mask: format_aspect_mask(dst_info.fmt), - mip_level: 0, - base_array_layer: 0, - layer_count: 1, - }, - dst_offsets: [ - vk::Offset3D { x: 0, y: 0, z: 0 }, - vk::Offset3D { - x: dst_info.width as _, - y: dst_info.height as _, - z: dst_info.depth as _, - }, - ], - }, - ) - } - - /// Copy a region of an image, potentially performing format conversion. - pub fn blit_image_region( - &mut self, - src_node: impl Into, - dst_node: impl Into, - filter: vk::Filter, - region: vk::ImageBlit, - ) -> &mut Self { - self.blit_image_regions(src_node, dst_node, filter, [region]) - } - - /// Copy regions of an image, potentially performing format conversion. - #[profiling::function] - pub fn blit_image_regions( - &mut self, - src_node: impl Into, - dst_node: impl Into, - filter: vk::Filter, - regions: impl AsRef<[vk::ImageBlit]> + 'static + Send, - ) -> &mut Self { - let src_node = src_node.into(); - let dst_node = dst_node.into(); - - let mut pass = self.begin_pass("blit image"); - - for region in regions.as_ref() { - pass = pass - .access_node_subrange( - src_node, - AccessType::TransferRead, - image_subresource_range_from_layers(region.src_subresource), - ) - .access_node_subrange( - dst_node, - AccessType::TransferWrite, - image_subresource_range_from_layers(region.dst_subresource), - ); - } - - pass.record_cmd_buf(move |device, cmd_buf, bindings| { - let src_image = *bindings[src_node]; - let dst_image = *bindings[dst_node]; - - unsafe { - device.cmd_blit_image( - cmd_buf, - src_image, - vk::ImageLayout::TRANSFER_SRC_OPTIMAL, - dst_image, - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - regions.as_ref(), - filter, - ); - } - }) - .submit_pass() - } - - /// Clear a color image. - pub fn clear_color_image(&mut self, image_node: impl Into) -> &mut Self { - self.clear_color_image_value(image_node, [0, 0, 0, 0]) - } - - /// Clear a color image. - #[profiling::function] - pub fn clear_color_image_value( - &mut self, - image_node: impl Into, - color_value: impl Into, - ) -> &mut Self { - let color_value = color_value.into(); - let image_node = image_node.into(); - let image_info = self.node_info(image_node); - let image_view_info = image_info.default_view_info(); - - self.begin_pass("clear color") - .access_node_subrange(image_node, AccessType::TransferWrite, image_view_info) - .record_cmd_buf(move |device, cmd_buf, bindings| unsafe { - device.cmd_clear_color_image( - cmd_buf, - *bindings[image_node], - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - &vk::ClearColorValue { - float32: color_value.0, - }, - &[image_view_info.into()], - ); - }) - .submit_pass() - } - - /// Clears a depth/stencil image. - pub fn clear_depth_stencil_image(&mut self, image_node: impl Into) -> &mut Self { - self.clear_depth_stencil_image_value(image_node, 1.0, 0) - } - - /// Clears a depth/stencil image. - #[profiling::function] - pub fn clear_depth_stencil_image_value( - &mut self, - image_node: impl Into, - depth: f32, - stencil: u32, - ) -> &mut Self { - let image_node = image_node.into(); - let image_info = self.node_info(image_node); - let image_view_info = image_info.default_view_info(); - - self.begin_pass("clear depth/stencil") - .access_node_subrange(image_node, AccessType::TransferWrite, image_view_info) - .record_cmd_buf(move |device, cmd_buf, bindings| unsafe { - device.cmd_clear_depth_stencil_image( - cmd_buf, - *bindings[image_node], - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - &vk::ClearDepthStencilValue { depth, stencil }, - &[image_view_info.into()], - ); - }) - .submit_pass() - } - - /// Copy data between buffers - pub fn copy_buffer( - &mut self, - src_node: impl Into, - dst_node: impl Into, - ) -> &mut Self { - let src_node = src_node.into(); - let dst_node = dst_node.into(); - let src_info = self.node_info(src_node); - let dst_info = self.node_info(dst_node); - - self.copy_buffer_region( - src_node, - dst_node, - vk::BufferCopy { - src_offset: 0, - dst_offset: 0, - size: src_info.size.min(dst_info.size), - }, - ) - } - - /// Copy data between buffer regions. - pub fn copy_buffer_region( - &mut self, - src_node: impl Into, - dst_node: impl Into, - region: vk::BufferCopy, - ) -> &mut Self { - self.copy_buffer_regions(src_node, dst_node, [region]) - } - - /// Copy data between buffer regions. - #[profiling::function] - pub fn copy_buffer_regions( - &mut self, - src_node: impl Into, - dst_node: impl Into, - regions: impl AsRef<[vk::BufferCopy]> + 'static + Send, - ) -> &mut Self { - let src_node = src_node.into(); - let dst_node = dst_node.into(); - - #[cfg(debug_assertions)] - let (src_size, dst_size) = (self.node_info(src_node).size, self.node_info(dst_node).size); - - let mut pass = self.begin_pass("copy buffer"); - - for region in regions.as_ref() { - #[cfg(debug_assertions)] - { - assert!( - region.src_offset + region.size <= src_size, - "source range end ({}) exceeds source size ({src_size})", - region.src_offset + region.size - ); - assert!( - region.dst_offset + region.size <= dst_size, - "destination range end ({}) exceeds destination size ({dst_size})", - region.dst_offset + region.size - ); - }; - - pass = pass - .access_node_subrange( - src_node, - AccessType::TransferRead, - region.src_offset..region.src_offset + region.size, - ) - .access_node_subrange( - dst_node, - AccessType::TransferWrite, - region.dst_offset..region.dst_offset + region.size, - ); - } - - pass.record_cmd_buf(move |device, cmd_buf, bindings| { - let src_buf = *bindings[src_node]; - let dst_buf = *bindings[dst_node]; - - unsafe { - device.cmd_copy_buffer(cmd_buf, src_buf, dst_buf, regions.as_ref()); - } - }) - .submit_pass() - } - - /// Copy data from a buffer into an image. - pub fn copy_buffer_to_image( - &mut self, - src_node: impl Into, - dst_node: impl Into, - ) -> &mut Self { - let dst_node = dst_node.into(); - let dst_info = self.node_info(dst_node); - - self.copy_buffer_to_image_region( - src_node, - dst_node, - vk::BufferImageCopy { - buffer_offset: 0, - buffer_row_length: dst_info.width, - buffer_image_height: dst_info.height, - image_subresource: vk::ImageSubresourceLayers { - aspect_mask: format_aspect_mask(dst_info.fmt), - mip_level: 0, - base_array_layer: 0, - layer_count: 1, - }, - image_offset: Default::default(), - image_extent: vk::Extent3D { - depth: dst_info.depth, - height: dst_info.height, - width: dst_info.width, - }, - }, - ) - } - - /// Copy data from a buffer into an image. - pub fn copy_buffer_to_image_region( - &mut self, - src_node: impl Into, - dst_node: impl Into, - region: vk::BufferImageCopy, - ) -> &mut Self { - self.copy_buffer_to_image_regions(src_node, dst_node, [region]) - } - - /// Copy data from a buffer into an image. - #[profiling::function] - pub fn copy_buffer_to_image_regions( - &mut self, - src_node: impl Into, - dst_node: impl Into, - regions: impl AsRef<[vk::BufferImageCopy]> + 'static + Send, - ) -> &mut Self { - let src_node = src_node.into(); - let dst_node = dst_node.into(); - let dst_info = self.node_info(dst_node); - - let mut pass = self.begin_pass("copy buffer to image"); - - for region in regions.as_ref() { - let block_bytes_size = format_texel_block_size(dst_info.fmt); - let (block_height, block_width) = format_texel_block_extent(dst_info.fmt); - let data_size = block_bytes_size - * (region.buffer_row_length / block_width) - * (region.buffer_image_height / block_height); - - pass = pass - .access_node_subrange( - src_node, - AccessType::TransferRead, - region.buffer_offset..region.buffer_offset + data_size as vk::DeviceSize, - ) - .access_node_subrange( - dst_node, - AccessType::TransferWrite, - image_subresource_range_from_layers(region.image_subresource), - ); - } - - pass.record_cmd_buf(move |device, cmd_buf, bindings| { - let src_buf = *bindings[src_node]; - let dst_image = *bindings[dst_node]; - - unsafe { - device.cmd_copy_buffer_to_image( - cmd_buf, - src_buf, - dst_image, - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - regions.as_ref(), - ); - } - }) - .submit_pass() - } - - /// Copy all layers of a source image to a destination image. - pub fn copy_image( - &mut self, - src_node: impl Into, - dst_node: impl Into, - ) -> &mut Self { - let src_node = src_node.into(); - let src_info = self.node_info(src_node); - - let dst_node = dst_node.into(); - let dst_info = self.node_info(dst_node); - - self.copy_image_region( - src_node, - dst_node, - vk::ImageCopy { - src_subresource: vk::ImageSubresourceLayers { - aspect_mask: format_aspect_mask(src_info.fmt), - mip_level: 0, - base_array_layer: 0, - layer_count: src_info.array_layer_count, - }, - src_offset: vk::Offset3D { x: 0, y: 0, z: 0 }, - dst_subresource: vk::ImageSubresourceLayers { - aspect_mask: format_aspect_mask(dst_info.fmt), - mip_level: 0, - base_array_layer: 0, - layer_count: src_info.array_layer_count, - }, - dst_offset: vk::Offset3D { x: 0, y: 0, z: 0 }, - extent: vk::Extent3D { - depth: src_info.depth.clamp(1, dst_info.depth), - height: src_info.height.clamp(1, dst_info.height), - width: src_info.width.min(dst_info.width), - }, - }, - ) - } - - /// Copy data between images. - pub fn copy_image_region( - &mut self, - src_node: impl Into, - dst_node: impl Into, - region: vk::ImageCopy, - ) -> &mut Self { - self.copy_image_regions(src_node, dst_node, [region]) - } - - /// Copy data between images. - #[profiling::function] - pub fn copy_image_regions( - &mut self, - src_node: impl Into, - dst_node: impl Into, - regions: impl AsRef<[vk::ImageCopy]> + 'static + Send, - ) -> &mut Self { - let src_node = src_node.into(); - let dst_node = dst_node.into(); - - let mut pass = self.begin_pass("copy image"); - - for region in regions.as_ref() { - pass = pass - .access_node_subrange( - src_node, - AccessType::TransferRead, - image_subresource_range_from_layers(region.src_subresource), - ) - .access_node_subrange( - dst_node, - AccessType::TransferWrite, - image_subresource_range_from_layers(region.dst_subresource), - ); - } - - pass.record_cmd_buf(move |device, cmd_buf, bindings| { - let src_image = *bindings[src_node]; - let dst_image = *bindings[dst_node]; - - unsafe { - device.cmd_copy_image( - cmd_buf, - src_image, - vk::ImageLayout::TRANSFER_SRC_OPTIMAL, - dst_image, - vk::ImageLayout::TRANSFER_DST_OPTIMAL, - regions.as_ref(), - ); - } - }) - .submit_pass() - } - - /// Copy image data into a buffer. - pub fn copy_image_to_buffer( - &mut self, - src_node: impl Into, - dst_node: impl Into, - ) -> &mut Self { - let src_node = src_node.into(); - let dst_node = dst_node.into(); - - let src_info = self.node_info(src_node); - - self.copy_image_to_buffer_region( - src_node, - dst_node, - vk::BufferImageCopy { - buffer_offset: 0, - buffer_row_length: src_info.width, - buffer_image_height: src_info.height, - image_subresource: vk::ImageSubresourceLayers { - aspect_mask: format_aspect_mask(src_info.fmt), - mip_level: 0, - base_array_layer: 0, - layer_count: 1, - }, - image_offset: Default::default(), - image_extent: vk::Extent3D { - depth: src_info.depth, - height: src_info.height, - width: src_info.width, - }, - }, - ) - } - - /// Copy image data into a buffer. - pub fn copy_image_to_buffer_region( - &mut self, - src_node: impl Into, - dst_node: impl Into, - region: vk::BufferImageCopy, - ) -> &mut Self { - self.copy_image_to_buffer_regions(src_node, dst_node, [region]) - } - - /// Copy image data into a buffer. - #[profiling::function] - pub fn copy_image_to_buffer_regions( - &mut self, - src_node: impl Into, - dst_node: impl Into, - regions: impl AsRef<[vk::BufferImageCopy]> + 'static + Send, - ) -> &mut Self { - let src_node = src_node.into(); - let src_info = self.node_info(src_node); - let dst_node = dst_node.into(); - - let mut pass = self.begin_pass("copy image to buffer"); - - for region in regions.as_ref() { - let block_bytes_size = format_texel_block_size(src_info.fmt); - let (block_height, block_width) = format_texel_block_extent(src_info.fmt); - let data_size = block_bytes_size - * (region.buffer_row_length / block_width) - * (region.buffer_image_height / block_height); - - pass = pass - .access_node_subrange( - src_node, - AccessType::TransferRead, - image_subresource_range_from_layers(region.image_subresource), - ) - .access_node_subrange( - dst_node, - AccessType::TransferWrite, - region.buffer_offset..region.buffer_offset + data_size as vk::DeviceSize, - ); - } - - pass.record_cmd_buf(move |device, cmd_buf, bindings| { - let src_image = *bindings[src_node]; - let dst_buf = *bindings[dst_node]; - - unsafe { - device.cmd_copy_image_to_buffer( - cmd_buf, - src_image, - vk::ImageLayout::TRANSFER_SRC_OPTIMAL, - dst_buf, - regions.as_ref(), - ); - } - }) - .submit_pass() - } - - /// Fill a region of a buffer with a fixed value. - pub fn fill_buffer(&mut self, buffer_node: impl Into, data: u32) -> &mut Self { - let buffer_node = buffer_node.into(); - - let buffer_info = self.node_info(buffer_node); - - self.fill_buffer_region(buffer_node, data, 0..buffer_info.size) - } - - /// Fill a region of a buffer with a fixed value. - #[profiling::function] - pub fn fill_buffer_region( - &mut self, - buffer_node: impl Into, - data: u32, - region: Range, - ) -> &mut Self { - let buffer_node = buffer_node.into(); - - self.begin_pass("fill buffer") - .access_node_subrange(buffer_node, AccessType::TransferWrite, region.clone()) - .record_cmd_buf(move |device, cmd_buf, bindings| { - let buffer = *bindings[buffer_node]; - - unsafe { - device.cmd_fill_buffer( - cmd_buf, - buffer, - region.start, - region.end - region.start, - data, - ); - } - }) - .submit_pass() - } - - /// Returns the index of the first pass which accesses a given node - #[profiling::function] - fn first_node_access_pass_index(&self, node: impl Node) -> Option { - let node_idx = node.index(); - - for (pass_idx, pass) in self.passes.iter().enumerate() { - for exec in pass.execs.iter() { - if exec.accesses.contains_key(&node_idx) { - return Some(pass_idx); - } - } - } - - None - } - - /// Returns the device address of a buffer node. - /// - /// # Panics - /// - /// Panics if the buffer is not currently bound or was not created with the - /// `SHADER_DEVICE_ADDRESS` usage flag. - pub fn node_device_address(&self, node: impl Into) -> vk::DeviceAddress { - let node: AnyBufferNode = node.into(); - let buffer = self.bindings[node.index()].as_driver_buffer().unwrap(); - - Buffer::device_address(buffer) - } - - /// Returns information used to crate a node. - pub fn node_info(&self, node: N) -> ::Info - where - N: Information, - { - node.get(self) - } - - /// Finalizes the graph and provides an object with functions for submitting the resulting - /// commands. - #[profiling::function] - pub fn resolve(mut self) -> Resolver { - // The final execution of each pass has no function - for pass in &mut self.passes { - pass.execs.pop(); - } - - Resolver::new(self) - } - - /// Removes a node from this graph. - /// - /// Future access to `node` on this graph will return invalid results. - pub fn unbind_node(&mut self, node: N) -> >::Result - where - N: Edge, - N: Unbind>::Result>, - { - node.unbind(self) - } - - /// Note: `data` must not exceed 65536 bytes. - pub fn update_buffer( - &mut self, - buffer_node: impl Into, - data: impl AsRef<[u8]> + 'static + Send, - ) -> &mut Self { - self.update_buffer_offset(buffer_node, 0, data) - } - - /// Note: `data` must not exceed 65536 bytes. - #[profiling::function] - pub fn update_buffer_offset( - &mut self, - buffer_node: impl Into, - offset: vk::DeviceSize, - data: impl AsRef<[u8]> + 'static + Send, - ) -> &mut Self { - let buffer_node = buffer_node.into(); - let data_end = offset + data.as_ref().len() as vk::DeviceSize; - - #[cfg(debug_assertions)] - { - let buffer_info = self.node_info(buffer_node); - - assert!( - data_end <= buffer_info.size, - "data range end ({data_end}) exceeds buffer size ({})", - buffer_info.size - ); - } - - self.begin_pass("update buffer") - .access_node_subrange(buffer_node, AccessType::TransferWrite, offset..data_end) - .record_cmd_buf(move |device, cmd_buf, bindings| { - let buffer = *bindings[buffer_node]; - - unsafe { - device.cmd_update_buffer(cmd_buf, buffer, offset, data.as_ref()); - } - }) - .submit_pass() - } -} diff --git a/src/graph/node.rs b/src/graph/node.rs deleted file mode 100644 index fcf8b269..00000000 --- a/src/graph/node.rs +++ /dev/null @@ -1,274 +0,0 @@ -//! Bindings for Vulkan smart-pointer resources. - -use { - super::{Information, NodeIndex, RenderGraph, Unbind}, - crate::{ - driver::{ - accel_struct::{AccelerationStructure, AccelerationStructureInfo}, - buffer::{Buffer, BufferInfo}, - image::{Image, ImageInfo}, - }, - pool::Lease, - }, - std::sync::Arc, -}; - -/// Specifies either an owned acceleration structure or an acceleration structure leased from a -/// pool. -#[derive(Debug)] -pub enum AnyAccelerationStructureNode { - /// An owned acceleration structure. - AccelerationStructure(AccelerationStructureNode), - - /// An acceleration structure leased from a pool. - AccelerationStructureLease(AccelerationStructureLeaseNode), -} - -impl Clone for AnyAccelerationStructureNode { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for AnyAccelerationStructureNode {} - -impl Information for AnyAccelerationStructureNode { - type Info = AccelerationStructureInfo; - - fn get(self, graph: &RenderGraph) -> Self::Info { - match self { - Self::AccelerationStructure(node) => node.get(graph), - Self::AccelerationStructureLease(node) => node.get(graph), - } - } -} - -impl From for AnyAccelerationStructureNode { - fn from(node: AccelerationStructureNode) -> Self { - Self::AccelerationStructure(node) - } -} - -impl From for AnyAccelerationStructureNode { - fn from(node: AccelerationStructureLeaseNode) -> Self { - Self::AccelerationStructureLease(node) - } -} - -impl Node for AnyAccelerationStructureNode { - fn index(self) -> NodeIndex { - match self { - Self::AccelerationStructure(node) => node.index(), - Self::AccelerationStructureLease(node) => node.index(), - } - } -} - -/// Specifies either an owned buffer or a buffer leased from a pool. -#[derive(Debug)] -pub enum AnyBufferNode { - /// An owned buffer. - Buffer(BufferNode), - - /// A buffer leased from a pool. - BufferLease(BufferLeaseNode), -} - -impl Clone for AnyBufferNode { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for AnyBufferNode {} - -impl Information for AnyBufferNode { - type Info = BufferInfo; - - fn get(self, graph: &RenderGraph) -> Self::Info { - match self { - Self::Buffer(node) => node.get(graph), - Self::BufferLease(node) => node.get(graph), - } - } -} - -impl From for AnyBufferNode { - fn from(node: BufferNode) -> Self { - Self::Buffer(node) - } -} - -impl From for AnyBufferNode { - fn from(node: BufferLeaseNode) -> Self { - Self::BufferLease(node) - } -} - -impl Node for AnyBufferNode { - fn index(self) -> NodeIndex { - match self { - Self::Buffer(node) => node.index(), - Self::BufferLease(node) => node.index(), - } - } -} - -/// Specifies either an owned image or an image leased from a pool. -/// -/// The image may also be a special swapchain type of image. -#[derive(Debug)] -pub enum AnyImageNode { - /// An owned image. - Image(ImageNode), - - /// An image leased from a pool. - ImageLease(ImageLeaseNode), - - /// A special swapchain image. - SwapchainImage(SwapchainImageNode), -} - -impl Clone for AnyImageNode { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for AnyImageNode {} - -impl Information for AnyImageNode { - type Info = ImageInfo; - - fn get(self, graph: &RenderGraph) -> Self::Info { - match self { - Self::Image(node) => node.get(graph), - Self::ImageLease(node) => node.get(graph), - Self::SwapchainImage(node) => node.get(graph), - } - } -} - -impl From for AnyImageNode { - fn from(node: ImageNode) -> Self { - Self::Image(node) - } -} - -impl From for AnyImageNode { - fn from(node: ImageLeaseNode) -> Self { - Self::ImageLease(node) - } -} - -impl From for AnyImageNode { - fn from(node: SwapchainImageNode) -> Self { - Self::SwapchainImage(node) - } -} - -impl Node for AnyImageNode { - fn index(self) -> NodeIndex { - match self { - Self::Image(node) => node.index(), - Self::ImageLease(node) => node.index(), - Self::SwapchainImage(node) => node.index(), - } - } -} - -/// A Vulkan resource which has been bound to a [`RenderGraph`] using [`RenderGraph::bind_node`]. -pub trait Node: Copy { - /// The internal node index of this bound resource. - fn index(self) -> NodeIndex; -} - -macro_rules! node { - ($name:ident) => { - paste::paste! { - /// Resource node. - #[derive(Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] - pub struct [<$name Node>] { - pub(super) idx: NodeIndex, - } - - impl [<$name Node>] { - pub(super) fn new(idx: NodeIndex) -> Self { - Self { - idx, - } - } - } - - impl Clone for [<$name Node>] { - fn clone(&self) -> Self { - *self - } - } - - impl Copy for [<$name Node>] {} - - impl Node for [<$name Node>] { - fn index(self) -> NodeIndex { - self.idx - } - } - } - }; -} - -node!(AccelerationStructure); -node!(AccelerationStructureLease); -node!(Buffer); -node!(BufferLease); -node!(Image); -node!(ImageLease); -node!(SwapchainImage); - -macro_rules! node_unbind { - ($name:ident) => { - paste::paste! { - impl Unbind> for [<$name Node>] { - fn unbind(self, graph: &mut RenderGraph) -> Arc<$name> { - let binding = &mut graph.bindings[self.idx]; - let res = Arc::clone( - binding - .[]() - .unwrap() - ); - binding.unbind(); - - res - } - } - } - }; -} - -node_unbind!(AccelerationStructure); -node_unbind!(Buffer); -node_unbind!(Image); - -macro_rules! node_unbind_lease { - ($name:ident) => { - paste::paste! { - impl Unbind>> for [<$name LeaseNode>] { - fn unbind(self, graph: &mut RenderGraph) -> Arc> { - let binding = &mut graph.bindings[self.idx]; - let res = Arc::clone( - binding - .[]() - .unwrap() - ); - binding.unbind(); - - res - } - } - } - }; -} - -node_unbind_lease!(AccelerationStructure); -node_unbind_lease!(Buffer); -node_unbind_lease!(Image); diff --git a/src/graph/pass_ref.rs b/src/graph/pass_ref.rs deleted file mode 100644 index 08ea4bef..00000000 --- a/src/graph/pass_ref.rs +++ /dev/null @@ -1,5184 +0,0 @@ -//! Strongly-typed rendering commands. - -use { - super::{ - AccelerationStructureLeaseNode, AccelerationStructureNode, AnyAccelerationStructureNode, - AnyBufferNode, AnyImageNode, Area, Attachment, Bind, Binding, BufferLeaseNode, BufferNode, - ClearColorValue, Edge, Execution, ExecutionFunction, ExecutionPipeline, ImageLeaseNode, - ImageNode, Information, Node, NodeIndex, Pass, RenderGraph, SampleCount, - SwapchainImageNode, - }, - crate::driver::{ - accel_struct::{ - AccelerationStructure, AccelerationStructureGeometry, - AccelerationStructureGeometryInfo, DeviceOrHostAddress, - }, - buffer::{Buffer, BufferSubresourceRange}, - compute::ComputePipeline, - device::Device, - graphic::{DepthStencilMode, GraphicPipeline}, - image::{ - Image, ImageViewInfo, image_subresource_range_contains, - image_subresource_range_intersects, - }, - ray_trace::RayTracePipeline, - render_pass::ResolveMode, - }, - ash::vk, - log::trace, - std::{ - cell::RefCell, - marker::PhantomData, - ops::{Index, Range}, - sync::Arc, - }, - vk_sync::AccessType, -}; - -/// Alias for the index of a framebuffer attachment. -pub type AttachmentIndex = u32; - -/// Alias for the binding index of a shader descriptor. -pub type BindingIndex = u32; - -/// Alias for the binding offset of a shader descriptor array element. -pub type BindingOffset = u32; - -/// Alias for the descriptor set index of a shader descriptor. -pub type DescriptorSetIndex = u32; - -/// Recording interface for acceleration structure commands. -/// -/// This structure provides a strongly-typed set of methods which allow acceleration structures to -/// be built and updated. An instance of `Acceleration` is provided to the closure parameter of -/// [`PassRef::record_acceleration`]. -/// -/// # Examples -/// -/// Basic usage: -/// -/// ```no_run -/// # use std::sync::Arc; -/// # use ash::vk; -/// # use screen_13::driver::accel_struct::{AccelerationStructure, AccelerationStructureInfo}; -/// # use screen_13::driver::DriverError; -/// # use screen_13::driver::device::{Device, DeviceInfo}; -/// # use screen_13::graph::RenderGraph; -/// # use screen_13::driver::shader::Shader; -/// # fn main() -> Result<(), DriverError> { -/// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); -/// # let mut my_graph = RenderGraph::new(); -/// # let info = AccelerationStructureInfo::blas(1); -/// my_graph.begin_pass("my acceleration pass") -/// .record_acceleration(move |acceleration, bindings| { -/// // During this closure we have access to the acceleration methods! -/// }); -/// # Ok(()) } -/// ``` -pub struct Acceleration<'a> { - bindings: Bindings<'a>, - cmd_buf: vk::CommandBuffer, - device: &'a Device, -} - -impl Acceleration<'_> { - /// Build an acceleration structure. - /// - /// Requires a scratch buffer which was created with the following requirements: - /// - /// - Flags must include [`vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS`] - /// - Size must be equal to or greater than the `build_size` value returned by - /// [`AccelerationStructure::size_of`] aligned to `min_accel_struct_scratch_offset_alignment` - /// of - /// [`PhysicalDevice::accel_struct_properties`](crate::driver::physical_device::PhysicalDevice::accel_struct_properties). - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```no_run - /// # use std::sync::Arc; - /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::accel_struct::{AccelerationStructure, AccelerationStructureGeometry, AccelerationStructureGeometryData, AccelerationStructureGeometryInfo, AccelerationStructureInfo, DeviceOrHostAddress}; - /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; - /// # use screen_13::graph::RenderGraph; - /// # use screen_13::driver::shader::Shader; - /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); - /// # let mut my_graph = RenderGraph::new(); - /// # let info = AccelerationStructureInfo::blas(1); - /// # let blas_accel_struct = AccelerationStructure::create(&device, info)?; - /// # let blas_node = my_graph.bind_node(blas_accel_struct); - /// # let scratch_buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS); - /// # let scratch_buf = Buffer::create(&device, scratch_buf_info)?; - /// # let scratch_buf = my_graph.bind_node(scratch_buf); - /// # let buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::INDEX_BUFFER); - /// # let my_idx_buf = Buffer::create(&device, buf_info)?; - /// # let buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::VERTEX_BUFFER); - /// # let my_vtx_buf = Buffer::create(&device, buf_info)?; - /// # let index_node = my_graph.bind_node(my_idx_buf); - /// # let vertex_node = my_graph.bind_node(my_vtx_buf); - /// my_graph.begin_pass("my acceleration pass") - /// .read_node(index_node) - /// .read_node(vertex_node) - /// .write_node(blas_node) - /// .write_node(scratch_buf) - /// .record_acceleration(move |acceleration, bindings| { - /// let geom = AccelerationStructureGeometry { - /// max_primitive_count: 64, - /// flags: vk::GeometryFlagsKHR::OPAQUE, - /// geometry: AccelerationStructureGeometryData::Triangles { - /// index_addr: DeviceOrHostAddress::DeviceAddress( - /// Buffer::device_address(&bindings[index_node]) - /// ), - /// index_type: vk::IndexType::UINT32, - /// max_vertex: 42, - /// transform_addr: None, - /// vertex_addr: DeviceOrHostAddress::DeviceAddress(Buffer::device_address( - /// &bindings[vertex_node], - /// )), - /// vertex_format: vk::Format::R32G32B32_SFLOAT, - /// vertex_stride: 12, - /// }, - /// }; - /// let build_range = vk::AccelerationStructureBuildRangeInfoKHR { - /// first_vertex: 0, - /// primitive_count: 1, - /// primitive_offset: 0, - /// transform_offset: 0, - /// }; - /// let info = AccelerationStructureGeometryInfo::blas([(geom, build_range)]); - /// - /// acceleration.build_structure(&info, blas_node, Buffer::device_address(&bindings[scratch_buf])); - /// }); - /// # Ok(()) } - /// ``` - pub fn build_structure( - &self, - info: &AccelerationStructureGeometryInfo<( - AccelerationStructureGeometry, - vk::AccelerationStructureBuildRangeInfoKHR, - )>, - accel_struct: impl Into, - scratch_addr: impl Into, - ) -> &Self { - #[derive(Default)] - struct Tls { - geometries: Vec>, - ranges: Vec, - } - - thread_local! { - static TLS: RefCell = Default::default(); - } - - let accel_struct = accel_struct.into(); - let scratch_addr = scratch_addr.into().into(); - - TLS.with_borrow_mut(|tls| { - tls.geometries.clear(); - tls.ranges.clear(); - - for (geometry, range) in info.geometries.iter() { - tls.geometries.push(geometry.into()); - tls.ranges.push(*range); - } - - unsafe { - Device::expect_accel_struct_ext(self.device).cmd_build_acceleration_structures( - self.cmd_buf, - &[vk::AccelerationStructureBuildGeometryInfoKHR::default() - .ty(info.ty) - .flags(info.flags) - .mode(vk::BuildAccelerationStructureModeKHR::BUILD) - .dst_acceleration_structure(*self.bindings[accel_struct]) - .geometries(&tls.geometries) - .scratch_data(scratch_addr)], - &[&tls.ranges], - ); - } - }); - - self - } - - /// Build an acceleration structure with some parameters provided on the device. - /// - /// `range` is a buffer device address which points to `info.geometry.len()` - /// [vk::VkAccelerationStructureBuildRangeInfoKHR] structures defining dynamic offsets to the - /// addresses where geometry data is stored, as defined by `info`. - pub fn build_structure_indirect( - &self, - info: &AccelerationStructureGeometryInfo, - accel_struct: impl Into, - scratch_addr: impl Into, - range_base: vk::DeviceAddress, - range_stride: u32, - ) -> &Self { - #[derive(Default)] - struct Tls { - geometries: Vec>, - max_primitive_counts: Vec, - } - - thread_local! { - static TLS: RefCell = Default::default(); - } - - let accel_struct = accel_struct.into(); - let scratch_addr = scratch_addr.into().into(); - - TLS.with_borrow_mut(|tls| { - tls.geometries.clear(); - tls.max_primitive_counts.clear(); - - for geometry in info.geometries.iter() { - tls.geometries.push(geometry.into()); - tls.max_primitive_counts.push(geometry.max_primitive_count); - } - - unsafe { - Device::expect_accel_struct_ext(self.device) - .cmd_build_acceleration_structures_indirect( - self.cmd_buf, - &[vk::AccelerationStructureBuildGeometryInfoKHR::default() - .ty(info.ty) - .flags(info.flags) - .mode(vk::BuildAccelerationStructureModeKHR::BUILD) - .dst_acceleration_structure(*self.bindings[accel_struct]) - .geometries(&tls.geometries) - .scratch_data(scratch_addr)], - &[range_base], - &[range_stride], - &[&tls.max_primitive_counts], - ); - } - }); - - self - } - - /// Build acceleration structures. - /// - /// There is no ordering or synchronization implied between any of the individual acceleration - /// structure builds. - pub fn build_structures(&self, infos: &[AccelerationStructureBuildInfo]) -> &Self { - #[derive(Default)] - struct Tls { - geometries: Vec>, - ranges: Vec, - } - - thread_local! { - static TLS: RefCell = Default::default(); - } - - TLS.with_borrow_mut(|tls| { - tls.geometries.clear(); - tls.geometries.extend(infos.iter().flat_map(|info| { - info.build_data.geometries.iter().map(|(geometry, _)| { - <&AccelerationStructureGeometry as Into< - vk::AccelerationStructureGeometryKHR, - >>::into(geometry) - }) - })); - - tls.ranges.clear(); - tls.ranges.extend( - infos - .iter() - .flat_map(|info| info.build_data.geometries.iter().map(|(_, range)| *range)), - ); - - let vk_ranges = { - let mut start = 0; - let mut vk_ranges = Vec::with_capacity(infos.len()); - for info in infos { - let end = start + info.build_data.geometries.len(); - vk_ranges.push(&tls.ranges[start..end]); - start = end; - } - - vk_ranges - }; - - let vk_infos = { - let mut start = 0; - let mut vk_infos = Vec::with_capacity(infos.len()); - for info in infos { - let end = start + info.build_data.geometries.len(); - vk_infos.push( - vk::AccelerationStructureBuildGeometryInfoKHR::default() - .ty(info.build_data.ty) - .flags(info.build_data.flags) - .mode(vk::BuildAccelerationStructureModeKHR::BUILD) - .dst_acceleration_structure(*self.bindings[info.accel_struct]) - .geometries(&tls.geometries[start..end]) - .scratch_data(info.scratch_addr.into()), - ); - start = end; - } - - vk_infos - }; - - unsafe { - Device::expect_accel_struct_ext(self.device).cmd_build_acceleration_structures( - self.cmd_buf, - &vk_infos, - &vk_ranges, - ); - } - }); - - self - } - - /// Builds acceleration structures with some parameters provided on the device. - /// - /// There is no ordering or synchronization implied between any of the individual acceleration - /// structure builds. - /// - /// See [Self::build_structure_indirect] - pub fn build_structures_indirect( - &self, - infos: &[AccelerationStructureIndirectBuildInfo], - ) -> &Self { - #[derive(Default)] - struct Tls { - geometries: Vec>, - max_primitive_counts: Vec, - range_bases: Vec, - range_strides: Vec, - } - - thread_local! { - static TLS: RefCell = Default::default(); - } - - TLS.with_borrow_mut(|tls| { - tls.geometries.clear(); - tls.geometries.extend(infos.iter().flat_map(|info| { - info.build_data.geometries.iter().map( - <&AccelerationStructureGeometry as Into< - vk::AccelerationStructureGeometryKHR, - >>::into, - ) - })); - - tls.max_primitive_counts.clear(); - tls.max_primitive_counts - .extend(infos.iter().flat_map(|info| { - info.build_data - .geometries - .iter() - .map(|geometry| geometry.max_primitive_count) - })); - - tls.range_bases.clear(); - tls.range_strides.clear(); - let (vk_infos, vk_max_primitive_counts) = { - let mut start = 0; - let mut vk_infos = Vec::with_capacity(infos.len()); - let mut vk_max_primitive_counts = Vec::with_capacity(infos.len()); - for info in infos { - let end = start + info.build_data.geometries.len(); - vk_infos.push( - vk::AccelerationStructureBuildGeometryInfoKHR::default() - .ty(info.build_data.ty) - .flags(info.build_data.flags) - .mode(vk::BuildAccelerationStructureModeKHR::BUILD) - .dst_acceleration_structure(*self.bindings[info.accel_struct]) - .geometries(&tls.geometries[start..end]) - .scratch_data(info.scratch_data.into()), - ); - vk_max_primitive_counts.push(&tls.max_primitive_counts[start..end]); - start = end; - - tls.range_bases.push(info.range_base); - tls.range_strides.push(info.range_stride); - } - - (vk_infos, vk_max_primitive_counts) - }; - - unsafe { - Device::expect_accel_struct_ext(self.device) - .cmd_build_acceleration_structures_indirect( - self.cmd_buf, - &vk_infos, - &tls.range_bases, - &tls.range_strides, - &vk_max_primitive_counts, - ); - } - }); - - self - } - - /// Update an acceleration structure. - /// - /// Requires a scratch buffer which was created with the following requirements: - /// - /// - Flags must include [`vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS`] - /// - Size must be equal to or greater than the `update_size` value returned by - /// [`AccelerationStructure::size_of`] aligned to `min_accel_struct_scratch_offset_alignment` - /// of - /// [`PhysicalDevice::accel_struct_properties`](crate::driver::physical_device::PhysicalDevice::accel_struct_properties). - pub fn update_structure( - &self, - info: &AccelerationStructureGeometryInfo<( - AccelerationStructureGeometry, - vk::AccelerationStructureBuildRangeInfoKHR, - )>, - src_accel_struct: impl Into, - dst_accel_struct: impl Into, - scratch_addr: impl Into, - ) -> &Self { - #[derive(Default)] - struct Tls { - geometries: Vec>, - ranges: Vec, - } - - thread_local! { - static TLS: RefCell = Default::default(); - } - - let src_accel_struct = src_accel_struct.into(); - let dst_accel_struct = dst_accel_struct.into(); - let scratch_addr = scratch_addr.into().into(); - - TLS.with_borrow_mut(|tls| { - tls.geometries.clear(); - tls.ranges.clear(); - - for (geometry, range) in info.geometries.iter() { - tls.geometries.push(geometry.into()); - tls.ranges.push(*range); - } - - unsafe { - Device::expect_accel_struct_ext(self.device).cmd_build_acceleration_structures( - self.cmd_buf, - &[vk::AccelerationStructureBuildGeometryInfoKHR::default() - .ty(info.ty) - .flags(info.flags) - .mode(vk::BuildAccelerationStructureModeKHR::UPDATE) - .dst_acceleration_structure(*self.bindings[dst_accel_struct]) - .src_acceleration_structure(*self.bindings[src_accel_struct]) - .geometries(&tls.geometries) - .scratch_data(scratch_addr)], - &[&tls.ranges], - ); - } - }); - - self - } - - /// Update an acceleration structure with some parameters provided on the device. - /// - /// `range` is a buffer device address which points to `info.geometry.len()` - /// [vk::VkAccelerationStructureBuildRangeInfoKHR] structures defining dynamic offsets to the - /// addresses where geometry data is stored, as defined by `info`. - pub fn update_structure_indirect( - &self, - info: &AccelerationStructureGeometryInfo, - src_accel_struct: impl Into, - dst_accel_struct: impl Into, - scratch_addr: impl Into, - range_base: vk::DeviceAddress, - range_stride: u32, - ) -> &Self { - #[derive(Default)] - struct Tls { - geometries: Vec>, - max_primitive_counts: Vec, - } - - thread_local! { - static TLS: RefCell = Default::default(); - } - - let src_accel_struct = src_accel_struct.into(); - let dst_accel_struct = dst_accel_struct.into(); - let scratch_addr = scratch_addr.into().into(); - - TLS.with_borrow_mut(|tls| { - tls.geometries.clear(); - tls.max_primitive_counts.clear(); - - for geometry in info.geometries.iter() { - tls.geometries.push(geometry.into()); - tls.max_primitive_counts.push(geometry.max_primitive_count); - } - - unsafe { - Device::expect_accel_struct_ext(self.device) - .cmd_build_acceleration_structures_indirect( - self.cmd_buf, - &[vk::AccelerationStructureBuildGeometryInfoKHR::default() - .ty(info.ty) - .flags(info.flags) - .mode(vk::BuildAccelerationStructureModeKHR::UPDATE) - .src_acceleration_structure(*self.bindings[src_accel_struct]) - .dst_acceleration_structure(*self.bindings[dst_accel_struct]) - .geometries(&tls.geometries) - .scratch_data(scratch_addr)], - &[range_base], - &[range_stride], - &[&tls.max_primitive_counts], - ); - } - }); - - self - } - - /// Update acceleration structures. - /// - /// There is no ordering or synchronization implied between any of the individual acceleration - /// structure updates. - pub fn update_structures(&self, infos: &[AccelerationStructureUpdateInfo]) -> &Self { - #[derive(Default)] - struct Tls { - geometries: Vec>, - ranges: Vec, - } - - thread_local! { - static TLS: RefCell = Default::default(); - } - - TLS.with_borrow_mut(|tls| { - tls.geometries.clear(); - tls.geometries.extend(infos.iter().flat_map(|info| { - info.update_data.geometries.iter().map(|(geometry, _)| { - <&AccelerationStructureGeometry as Into< - vk::AccelerationStructureGeometryKHR, - >>::into(geometry) - }) - })); - - tls.ranges.clear(); - tls.ranges.extend( - infos - .iter() - .flat_map(|info| info.update_data.geometries.iter().map(|(_, range)| *range)), - ); - - let vk_ranges = { - let mut start = 0; - let mut vk_ranges = Vec::with_capacity(infos.len()); - for info in infos { - let end = start + info.update_data.geometries.len(); - vk_ranges.push(&tls.ranges[start..end]); - start = end; - } - - vk_ranges - }; - - let vk_infos = { - let mut start = 0; - let mut vk_infos = Vec::with_capacity(infos.len()); - for info in infos { - let end = start + info.update_data.geometries.len(); - vk_infos.push( - vk::AccelerationStructureBuildGeometryInfoKHR::default() - .ty(info.update_data.ty) - .flags(info.update_data.flags) - .mode(vk::BuildAccelerationStructureModeKHR::UPDATE) - .dst_acceleration_structure(*self.bindings[info.dst_accel_struct]) - .src_acceleration_structure(*self.bindings[info.src_accel_struct]) - .geometries(&tls.geometries[start..end]) - .scratch_data(info.scratch_addr.into()), - ); - start = end; - } - - vk_infos - }; - - unsafe { - Device::expect_accel_struct_ext(self.device).cmd_build_acceleration_structures( - self.cmd_buf, - &vk_infos, - &vk_ranges, - ); - } - }); - - self - } - - /// Updates acceleration structures with some parameters provided on the device. - /// - /// There is no ordering or synchronization implied between any of the individual acceleration - /// structure updates. - /// - /// See [Self::update_structure_indirect] - pub fn update_structures_indirect( - &self, - infos: &[AccelerationStructureIndirectUpdateInfo], - ) -> &Self { - #[derive(Default)] - struct Tls { - geometries: Vec>, - max_primitive_counts: Vec, - range_bases: Vec, - range_strides: Vec, - } - - thread_local! { - static TLS: RefCell = Default::default(); - } - - TLS.with_borrow_mut(|tls| { - tls.geometries.clear(); - tls.geometries.extend(infos.iter().flat_map(|info| { - info.update_data.geometries.iter().map( - <&AccelerationStructureGeometry as Into< - vk::AccelerationStructureGeometryKHR, - >>::into, - ) - })); - - tls.max_primitive_counts.clear(); - tls.max_primitive_counts - .extend(infos.iter().flat_map(|info| { - info.update_data - .geometries - .iter() - .map(|geometry| geometry.max_primitive_count) - })); - - tls.range_bases.clear(); - tls.range_strides.clear(); - let (vk_infos, vk_max_primitive_counts) = { - let mut start = 0; - let mut vk_infos = Vec::with_capacity(infos.len()); - let mut vk_max_primitive_counts = Vec::with_capacity(infos.len()); - for info in infos { - let end = start + info.update_data.geometries.len(); - vk_infos.push( - vk::AccelerationStructureBuildGeometryInfoKHR::default() - .ty(info.update_data.ty) - .flags(info.update_data.flags) - .mode(vk::BuildAccelerationStructureModeKHR::UPDATE) - .src_acceleration_structure(*self.bindings[info.src_accel_struct]) - .dst_acceleration_structure(*self.bindings[info.dst_accel_struct]) - .geometries(&tls.geometries[start..end]) - .scratch_data(info.scratch_addr.into()), - ); - vk_max_primitive_counts.push(&tls.max_primitive_counts[start..end]); - start = end; - - tls.range_bases.push(info.range_base); - tls.range_strides.push(info.range_stride); - } - - (vk_infos, vk_max_primitive_counts) - }; - - unsafe { - Device::expect_accel_struct_ext(self.device) - .cmd_build_acceleration_structures_indirect( - self.cmd_buf, - &vk_infos, - &tls.range_bases, - &tls.range_strides, - &vk_max_primitive_counts, - ); - } - }); - - self - } -} - -/// Specifies the information and data used to build an acceleration structure. -/// -/// See -/// [VkAccelerationStructureBuildGeometryInfoKHR](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkAccelerationStructureBuildGeometryInfoKHR.html) -/// for more information. -#[derive(Clone, Debug)] -pub struct AccelerationStructureBuildInfo { - /// The acceleration structure to be written. - pub accel_struct: AnyAccelerationStructureNode, - - /// Specifies the geometry data to use when building the acceleration structure. - pub build_data: AccelerationStructureGeometryInfo<( - AccelerationStructureGeometry, - vk::AccelerationStructureBuildRangeInfoKHR, - )>, - - /// The temporary buffer or host address (with enough capacity per - /// [AccelerationStructure::size_of]). - pub scratch_addr: DeviceOrHostAddress, -} - -impl AccelerationStructureBuildInfo { - /// Constructs new acceleration structure build information. - pub fn new( - accel_struct: impl Into, - build_data: AccelerationStructureGeometryInfo<( - AccelerationStructureGeometry, - vk::AccelerationStructureBuildRangeInfoKHR, - )>, - scratch_addr: impl Into, - ) -> Self { - let accel_struct = accel_struct.into(); - let scratch_addr = scratch_addr.into(); - - Self { - accel_struct, - build_data, - scratch_addr, - } - } -} - -/// Specifies the information and data used to build an acceleration structure with some parameters -/// sourced on the device. -/// -/// See -/// [VkAccelerationStructureBuildGeometryInfoKHR](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkAccelerationStructureBuildGeometryInfoKHR.html) -/// for more information. -#[derive(Clone, Debug)] -pub struct AccelerationStructureIndirectBuildInfo { - /// The acceleration structure to be written. - pub accel_struct: AnyAccelerationStructureNode, - - /// Specifies the geometry data to use when building the acceleration structure. - pub build_data: AccelerationStructureGeometryInfo, - - /// A buffer device addresses which points to `data.geometry.len()` - /// [vk::VkAccelerationStructureBuildRangeInfoKHR] structures defining dynamic offsets to the - /// addresses where geometry data is stored. - pub range_base: vk::DeviceAddress, - - /// Byte stride between elements of [range]. - pub range_stride: u32, - - /// The temporary buffer or host address (with enough capacity per - /// [AccelerationStructure::size_of]). - pub scratch_data: DeviceOrHostAddress, -} - -impl AccelerationStructureIndirectBuildInfo { - /// Constructs new acceleration structure indirect build information. - pub fn new( - accel_struct: impl Into, - build_data: AccelerationStructureGeometryInfo, - range_base: vk::DeviceAddress, - - range_stride: u32, - scratch_data: impl Into, - ) -> Self { - let accel_struct = accel_struct.into(); - let scratch_data = scratch_data.into(); - - Self { - accel_struct, - build_data, - range_base, - range_stride, - scratch_data, - } - } -} - -/// Specifies the information and data used to update an acceleration structure with some parameters -/// sourced on the device. -/// -/// See -/// [VkAccelerationStructureBuildGeometryInfoKHR](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkAccelerationStructureBuildGeometryInfoKHR.html) -/// for more information. -#[derive(Clone, Debug)] -pub struct AccelerationStructureIndirectUpdateInfo { - /// The acceleration structure to be written. - pub dst_accel_struct: AnyAccelerationStructureNode, - - /// A buffer device addresses which points to `data.geometry.len()` - /// [vk::VkAccelerationStructureBuildRangeInfoKHR] structures defining dynamic offsets to the - /// addresses where geometry data is stored. - pub range_base: vk::DeviceAddress, - - /// Byte stride between elements of [range]. - pub range_stride: u32, - - /// The temporary buffer or host address (with enough capacity per - /// [AccelerationStructure::size_of]). - pub scratch_addr: DeviceOrHostAddress, - - /// The source acceleration structure to be read. - pub src_accel_struct: AnyAccelerationStructureNode, - - /// Specifies the geometry data to use when building the acceleration structure. - pub update_data: AccelerationStructureGeometryInfo, -} - -impl AccelerationStructureIndirectUpdateInfo { - /// Constructs new acceleration structure indirect update information. - pub fn new( - src_accel_struct: impl Into, - dst_accel_struct: impl Into, - update_data: AccelerationStructureGeometryInfo, - range_base: vk::DeviceAddress, - - range_stride: u32, - scratch_addr: impl Into, - ) -> Self { - let src_accel_struct = src_accel_struct.into(); - let dst_accel_struct = dst_accel_struct.into(); - let scratch_addr = scratch_addr.into(); - - Self { - dst_accel_struct, - range_base, - range_stride, - scratch_addr, - src_accel_struct, - update_data, - } - } -} - -/// Specifies the information and data used to update an acceleration structure. -/// -/// See -/// [VkAccelerationStructureBuildGeometryInfoKHR](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkAccelerationStructureBuildGeometryInfoKHR.html) -/// for more information. -#[derive(Clone, Debug)] -pub struct AccelerationStructureUpdateInfo { - /// The acceleration structure to be written. - pub dst_accel_struct: AnyAccelerationStructureNode, - - /// The temporary buffer or host address (with enough capacity per - /// [AccelerationStructure::size_of]). - pub scratch_addr: DeviceOrHostAddress, - - /// The source acceleration structure to be read. - pub src_accel_struct: AnyAccelerationStructureNode, - - /// Specifies the geometry data to use when updating the acceleration structure. - pub update_data: AccelerationStructureGeometryInfo<( - AccelerationStructureGeometry, - vk::AccelerationStructureBuildRangeInfoKHR, - )>, -} - -impl AccelerationStructureUpdateInfo { - /// Constructs new acceleration structure update information. - pub fn new( - src_accel_struct: impl Into, - dst_accel_struct: impl Into, - update_data: AccelerationStructureGeometryInfo<( - AccelerationStructureGeometry, - vk::AccelerationStructureBuildRangeInfoKHR, - )>, - scratch_addr: impl Into, - ) -> Self { - let src_accel_struct = src_accel_struct.into(); - let dst_accel_struct = dst_accel_struct.into(); - let scratch_addr = scratch_addr.into(); - - Self { - dst_accel_struct, - scratch_addr, - src_accel_struct, - update_data, - } - } -} - -/// Associated type trait which enables default values for read and write methods. -pub trait Access { - /// The default `AccessType` for read operations, if not specified explicitly. - const DEFAULT_READ: AccessType; - - /// The default `AccessType` for write operations, if not specified explicitly. - const DEFAULT_WRITE: AccessType; -} - -impl Access for ComputePipeline { - const DEFAULT_READ: AccessType = AccessType::ComputeShaderReadOther; - const DEFAULT_WRITE: AccessType = AccessType::ComputeShaderWrite; -} - -impl Access for GraphicPipeline { - const DEFAULT_READ: AccessType = AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer; - const DEFAULT_WRITE: AccessType = AccessType::AnyShaderWrite; -} - -impl Access for RayTracePipeline { - const DEFAULT_READ: AccessType = - AccessType::RayTracingShaderReadSampledImageOrUniformTexelBuffer; - const DEFAULT_WRITE: AccessType = AccessType::AnyShaderWrite; -} - -macro_rules! bind { - ($name:ident) => { - paste::paste! { - impl<'a> Bind, PipelinePassRef<'a, [<$name Pipeline>]>> for &'a Arc<[<$name Pipeline>]> { - // TODO: Allow binding as explicit secondary command buffers? like with compute/raytrace stuff - fn bind(self, mut pass: PassRef<'a>) -> PipelinePassRef<'a, [<$name Pipeline>]> { - let pass_ref = pass.as_mut(); - if pass_ref.execs.last().unwrap().pipeline.is_some() { - // Binding from PipelinePass -> PipelinePass (changing shaders) - pass_ref.execs.push(Default::default()); - } - - pass_ref.execs.last_mut().unwrap().pipeline = Some(ExecutionPipeline::$name(Arc::clone(self))); - - PipelinePassRef { - __: PhantomData, - pass, - } - } - } - - impl<'a> Bind, PipelinePassRef<'a, [<$name Pipeline>]>> for Arc<[<$name Pipeline>]> { - // TODO: Allow binding as explicit secondary command buffers? like with compute/raytrace stuff - fn bind(self, mut pass: PassRef<'a>) -> PipelinePassRef<'a, [<$name Pipeline>]> { - let pass_ref = pass.as_mut(); - if pass_ref.execs.last().unwrap().pipeline.is_some() { - // Binding from PipelinePass -> PipelinePass (changing shaders) - pass_ref.execs.push(Default::default()); - } - - pass_ref.execs.last_mut().unwrap().pipeline = Some(ExecutionPipeline::$name(self)); - - PipelinePassRef { - __: PhantomData, - pass, - } - } - } - - impl<'a> Bind, PipelinePassRef<'a, [<$name Pipeline>]>> for [<$name Pipeline>] { - // TODO: Allow binding as explicit secondary command buffers? like with compute/raytrace stuff - fn bind(self, mut pass: PassRef<'a>) -> PipelinePassRef<'a, [<$name Pipeline>]> { - let pass_ref = pass.as_mut(); - if pass_ref.execs.last().unwrap().pipeline.is_some() { - // Binding from PipelinePass -> PipelinePass (changing shaders) - pass_ref.execs.push(Default::default()); - } - - pass_ref.execs.last_mut().unwrap().pipeline = Some(ExecutionPipeline::$name(Arc::new(self))); - - PipelinePassRef { - __: PhantomData, - pass, - } - } - } - - impl ExecutionPipeline { - #[allow(unused)] - pub(super) fn [](&self) -> bool { - matches!(self, Self::$name(_)) - } - - #[allow(unused)] - pub(super) fn [](&self) -> &Arc<[<$name Pipeline>]> { - if let Self::$name(binding) = self { - &binding - } else { - panic!(); - } - } - } - } - }; -} - -// Pipelines you can bind to a pass -bind!(Compute); -bind!(Graphic); -bind!(RayTrace); - -/// An indexable structure will provides access to Vulkan smart-pointer resources inside a record -/// closure. -/// -/// This type is available while recording commands in the following closures: -/// -/// - [`PassRef::record_acceleration`] for building and updating acceleration structures -/// - [`PassRef::record_cmd_buf`] for general command streams -/// - [`PipelinePassRef::record_compute`] for dispatched compute operations -/// - [`PipelinePassRef::record_subpass`] for raster drawing operations, such as triangles streams -/// - [`PipelinePassRef::record_ray_trace`] for ray-traced operations -/// -/// # Examples -/// -/// Basic usage: -/// -/// ```no_run -/// # use std::sync::Arc; -/// # use ash::vk; -/// # use screen_13::driver::DriverError; -/// # use screen_13::driver::device::{Device, DeviceInfo}; -/// # use screen_13::driver::image::{Image, ImageInfo}; -/// # use screen_13::graph::RenderGraph; -/// # use screen_13::graph::node::ImageNode; -/// # fn main() -> Result<(), DriverError> { -/// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); -/// # let info = ImageInfo::image_2d(32, 32, vk::Format::R8G8B8A8_UNORM, vk::ImageUsageFlags::SAMPLED); -/// # let image = Image::create(&device, info)?; -/// # let mut my_graph = RenderGraph::new(); -/// # let my_image_node = my_graph.bind_node(image); -/// my_graph.begin_pass("custom vulkan commands") -/// .record_cmd_buf(move |device, cmd_buf, bindings| { -/// let my_image = &bindings[my_image_node]; -/// -/// assert_ne!(**my_image, vk::Image::null()); -/// assert_eq!(my_image.info.width, 32); -/// }); -/// # Ok(()) } -/// ``` -#[derive(Clone, Copy, Debug)] -pub struct Bindings<'a> { - bindings: &'a [Binding], - exec: &'a Execution, -} - -impl<'a> Bindings<'a> { - pub(super) fn new(bindings: &'a [Binding], exec: &'a Execution) -> Self { - Self { bindings, exec } - } - - fn binding_ref(&self, node_idx: usize) -> &Binding { - // You must have called read or write for this node on this execution before indexing - // into the bindings data! - debug_assert!( - self.exec.accesses.contains_key(&node_idx), - "unexpected node access: call access, read, or write first" - ); - - &self.bindings[node_idx] - } -} - -macro_rules! index { - ($name:ident, $handle:ident) => { - paste::paste! { - impl<'a> Index<[<$name Node>]> for Bindings<'a> - { - type Output = $handle; - - fn index(&self, node: [<$name Node>]) -> &Self::Output { - &*self.binding_ref(node.idx).[]().unwrap() - } - } - } - }; -} - -// Allow indexing the Bindings data during command execution: -// (This gets you access to the driver images or other resources) -index!(AccelerationStructure, AccelerationStructure); -index!(AccelerationStructureLease, AccelerationStructure); -index!(Buffer, Buffer); -index!(BufferLease, Buffer); -index!(Image, Image); -index!(ImageLease, Image); -index!(SwapchainImage, Image); - -impl Index for Bindings<'_> { - type Output = AccelerationStructure; - - fn index(&self, node: AnyAccelerationStructureNode) -> &Self::Output { - let node_idx = match node { - AnyAccelerationStructureNode::AccelerationStructure(node) => node.idx, - AnyAccelerationStructureNode::AccelerationStructureLease(node) => node.idx, - }; - let binding = self.binding_ref(node_idx); - - match node { - AnyAccelerationStructureNode::AccelerationStructure(_) => { - binding.as_acceleration_structure().unwrap() - } - AnyAccelerationStructureNode::AccelerationStructureLease(_) => { - binding.as_acceleration_structure_lease().unwrap() - } - } - } -} - -impl Index for Bindings<'_> { - type Output = Buffer; - - fn index(&self, node: AnyBufferNode) -> &Self::Output { - let node_idx = match node { - AnyBufferNode::Buffer(node) => node.idx, - AnyBufferNode::BufferLease(node) => node.idx, - }; - let binding = self.binding_ref(node_idx); - - match node { - AnyBufferNode::Buffer(_) => binding.as_buffer().unwrap(), - AnyBufferNode::BufferLease(_) => binding.as_buffer_lease().unwrap(), - } - } -} - -impl Index for Bindings<'_> { - type Output = Image; - - fn index(&self, node: AnyImageNode) -> &Self::Output { - let node_idx = match node { - AnyImageNode::Image(node) => node.idx, - AnyImageNode::ImageLease(node) => node.idx, - AnyImageNode::SwapchainImage(node) => node.idx, - }; - let binding = self.binding_ref(node_idx); - - match node { - AnyImageNode::Image(_) => binding.as_image().unwrap(), - AnyImageNode::ImageLease(_) => binding.as_image_lease().unwrap(), - AnyImageNode::SwapchainImage(_) => binding.as_swapchain_image().unwrap(), - } - } -} - -/// Recording interface for computing commands. -/// -/// This structure provides a strongly-typed set of methods which allow compute shader code to be -/// executed. An instance of `Compute` is provided to the closure parameter of -/// [`PipelinePassRef::record_compute`] which may be accessed by binding a [`ComputePipeline`] to a -/// render pass. -/// -/// # Examples -/// -/// Basic usage: -/// -/// ```no_run -/// # use std::sync::Arc; -/// # use ash::vk; -/// # use screen_13::driver::DriverError; -/// # use screen_13::driver::device::{Device, DeviceInfo}; -/// # use screen_13::driver::compute::{ComputePipeline, ComputePipelineInfo}; -/// # use screen_13::driver::shader::{Shader}; -/// # use screen_13::graph::RenderGraph; -/// # fn main() -> Result<(), DriverError> { -/// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); -/// # let info = ComputePipelineInfo::default(); -/// # let shader = Shader::new_compute([0u8; 1].as_slice()); -/// # let my_compute_pipeline = Arc::new(ComputePipeline::create(&device, info, shader)?); -/// # let mut my_graph = RenderGraph::new(); -/// my_graph.begin_pass("my compute pass") -/// .bind_pipeline(&my_compute_pipeline) -/// .record_compute(move |compute, bindings| { -/// // During this closure we have access to the compute methods! -/// }); -/// # Ok(()) } -/// ``` -pub struct Compute<'a> { - bindings: Bindings<'a>, - cmd_buf: vk::CommandBuffer, - device: &'a Device, - pipeline: Arc, -} - -impl Compute<'_> { - /// [Dispatch] compute work items. - /// - /// When the command is executed, a global workgroup consisting of - /// `group_count_x × group_count_y × group_count_z` local workgroups is assembled. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ``` - /// # inline_spirv::inline_spirv!(r#" - /// #version 450 - /// - /// layout(set = 0, binding = 0, std430) restrict writeonly buffer MyBufer { - /// uint my_buf[]; - /// }; - /// - /// void main() - /// { - /// // TODO - /// } - /// # "#, comp); - /// ``` - /// - /// ```no_run - /// # use std::sync::Arc; - /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; - /// # use screen_13::driver::compute::{ComputePipeline, ComputePipelineInfo}; - /// # use screen_13::driver::shader::{Shader}; - /// # use screen_13::graph::RenderGraph; - /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); - /// # let buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::STORAGE_BUFFER); - /// # let my_buf = Buffer::create(&device, buf_info)?; - /// # let info = ComputePipelineInfo::default(); - /// # let shader = Shader::new_compute([0u8; 1].as_slice()); - /// # let my_compute_pipeline = Arc::new(ComputePipeline::create(&device, info, shader)?); - /// # let mut my_graph = RenderGraph::new(); - /// # let my_buf_node = my_graph.bind_node(my_buf); - /// my_graph.begin_pass("fill my_buf_node with data") - /// .bind_pipeline(&my_compute_pipeline) - /// .write_descriptor(0, my_buf_node) - /// .record_compute(move |compute, bindings| { - /// compute.dispatch(128, 64, 32); - /// }); - /// # Ok(()) } - /// ``` - /// - /// [Dispatch]: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/vkCmdDispatch.html - #[profiling::function] - pub fn dispatch(&self, group_count_x: u32, group_count_y: u32, group_count_z: u32) -> &Self { - unsafe { - self.device - .cmd_dispatch(self.cmd_buf, group_count_x, group_count_y, group_count_z); - } - - self - } - - /// [Dispatch] compute work items with non-zero base values for the workgroup IDs. - /// - /// When the command is executed, a global workgroup consisting of - /// `group_count_x × group_count_y × group_count_z` local workgroups is assembled, with - /// WorkgroupId values ranging from `[base_group*, base_group* + group_count*)` in each - /// component. - /// - /// [`Compute::dispatch`] is equivalent to - /// `dispatch_base(0, 0, 0, group_count_x, group_count_y, group_count_z)`. - /// - /// [Dispatch]: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/vkCmdDispatchBase.html - #[profiling::function] - pub fn dispatch_base( - &self, - base_group_x: u32, - base_group_y: u32, - base_group_z: u32, - group_count_x: u32, - group_count_y: u32, - group_count_z: u32, - ) -> &Self { - unsafe { - self.device.cmd_dispatch_base( - self.cmd_buf, - base_group_x, - base_group_y, - base_group_z, - group_count_x, - group_count_y, - group_count_z, - ); - } - - self - } - - /// Dispatch compute work items with indirect parameters. - /// - /// `dispatch_indirect` behaves similarly to [`Compute::dispatch`] except that the parameters - /// are read by the device from `args_buf` during execution. The parameters of the dispatch are - /// encoded in a [`vk::DispatchIndirectCommand`] structure taken from `args_buf` starting at - /// `args_offset`. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```no_run - /// # use std::sync::Arc; - /// # use std::mem::size_of; - /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; - /// # use screen_13::driver::compute::{ComputePipeline, ComputePipelineInfo}; - /// # use screen_13::driver::shader::{Shader}; - /// # use screen_13::graph::RenderGraph; - /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); - /// # let buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::STORAGE_BUFFER); - /// # let my_buf = Buffer::create(&device, buf_info)?; - /// # let info = ComputePipelineInfo::default(); - /// # let shader = Shader::new_compute([0u8; 1].as_slice()); - /// # let my_compute_pipeline = Arc::new(ComputePipeline::create(&device, info, shader)?); - /// # let mut my_graph = RenderGraph::new(); - /// # let my_buf_node = my_graph.bind_node(my_buf); - /// const CMD_SIZE: usize = size_of::(); - /// - /// let cmd = vk::DispatchIndirectCommand { - /// x: 1, - /// y: 2, - /// z: 3, - /// }; - /// let cmd_data = unsafe { - /// std::slice::from_raw_parts(&cmd as *const _ as *const _, CMD_SIZE) - /// }; - /// - /// let args_buf_flags = vk::BufferUsageFlags::STORAGE_BUFFER; - /// let args_buf = Buffer::create_from_slice(&device, args_buf_flags, cmd_data)?; - /// let args_buf_node = my_graph.bind_node(args_buf); - /// - /// my_graph.begin_pass("fill my_buf_node with data") - /// .bind_pipeline(&my_compute_pipeline) - /// .read_node(args_buf_node) - /// .write_descriptor(0, my_buf_node) - /// .record_compute(move |compute, bindings| { - /// compute.dispatch_indirect(args_buf_node, 0); - /// }); - /// # Ok(()) } - /// ``` - /// - /// [Dispatch]: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/vkCmdDispatchIndirect.html - /// [VkDispatchIndirectCommand]: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkDispatchIndirectCommand.html - #[profiling::function] - pub fn dispatch_indirect( - &self, - args_buf: impl Into, - args_offset: vk::DeviceSize, - ) -> &Self { - let args_buf = args_buf.into(); - - unsafe { - self.device - .cmd_dispatch_indirect(self.cmd_buf, *self.bindings[args_buf], args_offset); - } - - self - } - - /// Updates push constants. - /// - /// Push constants represent a high speed path to modify constant data in pipelines that is - /// expected to outperform memory-backed resource updates. - /// - /// Push constant values can be updated incrementally, causing shader stages to read the new - /// data for push constants modified by this command, while still reading the previous data for - /// push constants not modified by this command. - /// - /// # Device limitations - /// - /// See - /// [`device.physical_device.props.limits.max_push_constants_size`](vk::PhysicalDeviceLimits) - /// for the limits of the current device. You may also check [gpuinfo.org] for a listing of - /// reported limits on other devices. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ``` - /// # inline_spirv::inline_spirv!(r#" - /// #version 450 - /// - /// layout(push_constant) uniform PushConstants { - /// layout(offset = 0) uint the_answer; - /// } push_constants; - /// - /// void main() - /// { - /// // TODO: Add bindings to read/write things! - /// } - /// # "#, comp); - /// ``` - /// - /// ```no_run - /// # use std::sync::Arc; - /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; - /// # use screen_13::driver::compute::{ComputePipeline, ComputePipelineInfo}; - /// # use screen_13::driver::shader::{Shader}; - /// # use screen_13::graph::RenderGraph; - /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); - /// # let info = ComputePipelineInfo::default(); - /// # let shader = Shader::new_compute([0u8; 1].as_slice()); - /// # let my_compute_pipeline = Arc::new(ComputePipeline::create(&device, info, shader)?); - /// # let mut my_graph = RenderGraph::new(); - /// my_graph.begin_pass("compute the ultimate question") - /// .bind_pipeline(&my_compute_pipeline) - /// .record_compute(move |compute, bindings| { - /// compute.push_constants(&[42]) - /// .dispatch(1, 1, 1); - /// }); - /// # Ok(()) } - /// ``` - /// - /// [gpuinfo.org]: https://vulkan.gpuinfo.org/displaydevicelimit.php?name=maxPushConstantsSize&platform=all - pub fn push_constants(&self, data: &[u8]) -> &Self { - self.push_constants_offset(0, data) - } - - /// Updates push constants starting at the given `offset`. - /// - /// Behaves similary to [`Compute::push_constants`] except that `offset` describes the position - /// at which `data` updates the push constants of the currently bound pipeline. This may be used - /// to update a subset or single field of previously set push constant data. - /// - /// # Device limitations - /// - /// See - /// [`device.physical_device.props.limits.max_push_constants_size`](vk::PhysicalDeviceLimits) - /// for the limits of the current device. You may also check [gpuinfo.org] for a listing of - /// reported limits on other devices. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ``` - /// # inline_spirv::inline_spirv!(r#" - /// #version 450 - /// - /// layout(push_constant) uniform PushConstants { - /// layout(offset = 0) uint some_val1; - /// layout(offset = 4) uint some_val2; - /// } push_constants; - /// - /// void main() - /// { - /// // TODO: Add bindings to read/write things! - /// } - /// # "#, comp); - /// ``` - /// - /// ```no_run - /// # use std::sync::Arc; - /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; - /// # use screen_13::driver::compute::{ComputePipeline, ComputePipelineInfo}; - /// # use screen_13::driver::shader::{Shader}; - /// # use screen_13::graph::RenderGraph; - /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); - /// # let info = ComputePipelineInfo::default(); - /// # let shader = Shader::new_compute([0u8; 1].as_slice()); - /// # let my_compute_pipeline = Arc::new(ComputePipeline::create(&device, info, shader)?); - /// # let mut my_graph = RenderGraph::new(); - /// my_graph.begin_pass("calculate the wow factor") - /// .bind_pipeline(&my_compute_pipeline) - /// .record_compute(move |compute, bindings| { - /// compute.push_constants(&[0x00, 0x00]) - /// .dispatch(1, 1, 1) - /// .push_constants_offset(4, &[0xff]) - /// .dispatch(1, 1, 1); - /// }); - /// # Ok(()) } - /// ``` - /// - /// [gpuinfo.org]: https://vulkan.gpuinfo.org/displaydevicelimit.php?name=maxPushConstantsSize&platform=all - #[profiling::function] - pub fn push_constants_offset(&self, offset: u32, data: &[u8]) -> &Self { - if let Some(push_const) = self.pipeline.push_constants { - // Determine the range of the overall pipline push constants which overlap with `data` - let push_const_end = push_const.offset + push_const.size; - let data_end = offset + data.len() as u32; - let end = data_end.min(push_const_end); - let start = offset.max(push_const.offset); - - if end > start { - trace!( - " push constants {:?} {}..{}", - push_const.stage_flags, start, end - ); - - unsafe { - self.device.cmd_push_constants( - self.cmd_buf, - self.pipeline.layout, - vk::ShaderStageFlags::COMPUTE, - push_const.offset, - &data[(start - offset) as usize..(end - offset) as usize], - ); - } - } - } - - self - } -} - -/// Describes the SPIR-V binding index, and optionally a specific descriptor set -/// and array index. -/// -/// Generally you might pass a function a descriptor using a simple integer: -/// -/// ```rust -/// # fn my_func(_: usize, _: ()) {} -/// # let image = (); -/// let descriptor = 42; -/// my_func(descriptor, image); -/// ``` -/// -/// But also: -/// -/// - `(0, 42)` for descriptor set `0` and binding index `42` -/// - `(42, [8])` for the same binding, but the 8th element -/// - `(0, 42, [8])` same as the previous example -#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub enum Descriptor { - /// An array binding which includes an `offset` argument for the bound element. - ArrayBinding(DescriptorSetIndex, BindingIndex, BindingOffset), - - /// A single binding. - Binding(DescriptorSetIndex, BindingIndex), -} - -impl Descriptor { - pub(super) fn into_tuple(self) -> (DescriptorSetIndex, BindingIndex, BindingOffset) { - match self { - Self::ArrayBinding(descriptor_set_idx, binding_idx, binding_offset) => { - (descriptor_set_idx, binding_idx, binding_offset) - } - Self::Binding(descriptor_set_idx, binding_idx) => (descriptor_set_idx, binding_idx, 0), - } - } - - pub(super) fn set(self) -> DescriptorSetIndex { - let (res, _, _) = self.into_tuple(); - res - } -} - -impl From for Descriptor { - fn from(val: BindingIndex) -> Self { - Self::Binding(0, val) - } -} - -impl From<(DescriptorSetIndex, BindingIndex)> for Descriptor { - fn from(tuple: (DescriptorSetIndex, BindingIndex)) -> Self { - Self::Binding(tuple.0, tuple.1) - } -} - -impl From<(BindingIndex, [BindingOffset; 1])> for Descriptor { - fn from(tuple: (BindingIndex, [BindingOffset; 1])) -> Self { - Self::ArrayBinding(0, tuple.0, tuple.1[0]) - } -} - -impl From<(DescriptorSetIndex, BindingIndex, [BindingOffset; 1])> for Descriptor { - fn from(tuple: (DescriptorSetIndex, BindingIndex, [BindingOffset; 1])) -> Self { - Self::ArrayBinding(tuple.0, tuple.1, tuple.2[0]) - } -} - -/// Recording interface for drawing commands. -/// -/// This structure provides a strongly-typed set of methods which allow rasterization shader code to -/// be executed. An instance of `Draw` is provided to the closure parameter of -/// [`PipelinePassRef::record_subpass`] which may be accessed by binding a [`GraphicPipeline`] to a -/// render pass. -/// -/// # Examples -/// -/// Basic usage: -/// -/// ```no_run -/// # use std::sync::Arc; -/// # use ash::vk; -/// # use screen_13::driver::DriverError; -/// # use screen_13::driver::device::{Device, DeviceInfo}; -/// # use screen_13::driver::graphic::{GraphicPipeline, GraphicPipelineInfo}; -/// # use screen_13::driver::image::{Image, ImageInfo}; -/// # use screen_13::graph::RenderGraph; -/// # use screen_13::driver::shader::Shader; -/// # fn main() -> Result<(), DriverError> { -/// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); -/// # let my_frag_code = [0u8; 1]; -/// # let my_vert_code = [0u8; 1]; -/// # let vert = Shader::new_vertex(my_vert_code.as_slice()); -/// # let frag = Shader::new_fragment(my_frag_code.as_slice()); -/// # let info = GraphicPipelineInfo::default(); -/// # let my_graphic_pipeline = Arc::new(GraphicPipeline::create(&device, info, [vert, frag])?); -/// # let mut my_graph = RenderGraph::new(); -/// # let info = ImageInfo::image_2d(32, 32, vk::Format::R8G8B8A8_UNORM, vk::ImageUsageFlags::SAMPLED); -/// # let swapchain_image = my_graph.bind_node(Image::create(&device, info)?); -/// my_graph.begin_pass("my draw pass") -/// .bind_pipeline(&my_graphic_pipeline) -/// .store_color(0, swapchain_image) -/// .record_subpass(move |subpass, bindings| { -/// // During this closure we have access to the draw methods! -/// }); -/// # Ok(()) } -/// ``` -pub struct Draw<'a> { - bindings: Bindings<'a>, - cmd_buf: vk::CommandBuffer, - device: &'a Device, - pipeline: Arc, -} - -impl Draw<'_> { - /// Bind an index buffer to the current pass. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```no_run - /// # use std::sync::Arc; - /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; - /// # use screen_13::driver::graphic::{GraphicPipeline, GraphicPipelineInfo}; - /// # use screen_13::driver::image::{Image, ImageInfo}; - /// # use screen_13::driver::shader::Shader; - /// # use screen_13::graph::RenderGraph; - /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); - /// # let my_frag_code = [0u8; 1]; - /// # let my_vert_code = [0u8; 1]; - /// # let vert = Shader::new_vertex(my_vert_code.as_slice()); - /// # let frag = Shader::new_fragment(my_frag_code.as_slice()); - /// # let info = GraphicPipelineInfo::default(); - /// # let my_graphic_pipeline = Arc::new(GraphicPipeline::create(&device, info, [vert, frag])?); - /// # let mut my_graph = RenderGraph::new(); - /// # let info = ImageInfo::image_2d(32, 32, vk::Format::R8G8B8A8_UNORM, vk::ImageUsageFlags::SAMPLED); - /// # let swapchain_image = my_graph.bind_node(Image::create(&device, info)?); - /// # let buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::INDEX_BUFFER); - /// # let my_idx_buf = Buffer::create(&device, buf_info)?; - /// # let buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::VERTEX_BUFFER); - /// # let my_vtx_buf = Buffer::create(&device, buf_info)?; - /// # let my_idx_buf = my_graph.bind_node(my_idx_buf); - /// # let my_vtx_buf = my_graph.bind_node(my_vtx_buf); - /// my_graph.begin_pass("my indexed geometry draw pass") - /// .bind_pipeline(&my_graphic_pipeline) - /// .store_color(0, swapchain_image) - /// .read_node(my_idx_buf) - /// .read_node(my_vtx_buf) - /// .record_subpass(move |subpass, bindings| { - /// subpass.bind_index_buffer(my_idx_buf, vk::IndexType::UINT16) - /// .bind_vertex_buffer(my_vtx_buf) - /// .draw_indexed(42, 1, 0, 0, 0); - /// }); - /// # Ok(()) } - /// ``` - pub fn bind_index_buffer( - &self, - buffer: impl Into, - index_ty: vk::IndexType, - ) -> &Self { - self.bind_index_buffer_offset(buffer, index_ty, 0) - } - - /// Bind an index buffer to the current pass. - /// - /// Behaves similarly to `bind_index_buffer` except that `offset` is the starting offset in - /// bytes within `buffer` used in index buffer address calculations. - #[profiling::function] - pub fn bind_index_buffer_offset( - &self, - buffer: impl Into, - index_ty: vk::IndexType, - offset: vk::DeviceSize, - ) -> &Self { - let buffer = buffer.into(); - - unsafe { - self.device.cmd_bind_index_buffer( - self.cmd_buf, - *self.bindings[buffer], - offset, - index_ty, - ); - } - - self - } - - /// Bind a vertex buffer to the current pass. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```no_run - /// # use std::sync::Arc; - /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; - /// # use screen_13::driver::graphic::{GraphicPipeline, GraphicPipelineInfo}; - /// # use screen_13::driver::image::{Image, ImageInfo}; - /// # use screen_13::driver::shader::Shader; - /// # use screen_13::graph::RenderGraph; - /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); - /// # let buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::VERTEX_BUFFER); - /// # let my_vtx_buf = Buffer::create(&device, buf_info)?; - /// # let my_frag_code = [0u8; 1]; - /// # let my_vert_code = [0u8; 1]; - /// # let vert = Shader::new_vertex(my_vert_code.as_slice()); - /// # let frag = Shader::new_fragment(my_frag_code.as_slice()); - /// # let info = GraphicPipelineInfo::default(); - /// # let my_graphic_pipeline = Arc::new(GraphicPipeline::create(&device, info, [vert, frag])?); - /// # let mut my_graph = RenderGraph::new(); - /// # let info = ImageInfo::image_2d(32, 32, vk::Format::R8G8B8A8_UNORM, vk::ImageUsageFlags::SAMPLED); - /// # let swapchain_image = my_graph.bind_node(Image::create(&device, info)?); - /// # let my_vtx_buf = my_graph.bind_node(my_vtx_buf); - /// my_graph.begin_pass("my unindexed geometry draw pass") - /// .bind_pipeline(&my_graphic_pipeline) - /// .store_color(0, swapchain_image) - /// .read_node(my_vtx_buf) - /// .record_subpass(move |subpass, bindings| { - /// subpass.bind_vertex_buffer(my_vtx_buf) - /// .draw(42, 1, 0, 0); - /// }); - /// # Ok(()) } - /// ``` - pub fn bind_vertex_buffer(&self, buffer: impl Into) -> &Self { - self.bind_vertex_buffer_offset(buffer, 0) - } - - /// Bind a vertex buffer to the current pass. - /// - /// Behaves similarly to `bind_vertex_buffer` except the vertex input binding is updated to - /// start at `offset` from the start of `buffer`. - #[profiling::function] - pub fn bind_vertex_buffer_offset( - &self, - buffer: impl Into, - offset: vk::DeviceSize, - ) -> &Self { - use std::slice::from_ref; - - let buffer = buffer.into(); - - unsafe { - self.device.cmd_bind_vertex_buffers( - self.cmd_buf, - 0, - from_ref(&self.bindings[buffer]), - from_ref(&offset), - ); - } - - self - } - - /// Binds multiple vertex buffers to the current pass, starting at the given `first_binding`. - /// - /// Each vertex input binding in `buffers` specifies an offset from the start of the - /// corresponding buffer. - /// - /// The vertex input attributes that use each of these bindings will use these updated addresses - /// in their address calculations for subsequent drawing commands. - #[profiling::function] - pub fn bind_vertex_buffers( - &self, - first_binding: u32, - buffer_offsets: impl IntoIterator, - ) -> &Self - where - B: Into, - { - thread_local! { - static BUFFERS_OFFSETS: RefCell<(Vec, Vec)> = Default::default(); - } - - BUFFERS_OFFSETS.with_borrow_mut(|(buffers, offsets)| { - buffers.clear(); - offsets.clear(); - - for (buffer, offset) in buffer_offsets { - let buffer = buffer.into(); - - buffers.push(*self.bindings[buffer]); - offsets.push(offset); - } - - unsafe { - self.device.cmd_bind_vertex_buffers( - self.cmd_buf, - first_binding, - buffers.as_slice(), - offsets.as_slice(), - ); - } - }); - - self - } - - /// Draw unindexed primitives. - /// - /// When the command is executed, primitives are assembled using the current primitive topology - /// and `vertex_count` consecutive vertex indices with the first `vertex_index` value equal to - /// `first_vertex`. The primitives are drawn `instance_count` times with `instance_index` - /// starting with `first_instance` and increasing sequentially for each instance. - #[profiling::function] - pub fn draw( - &self, - vertex_count: u32, - instance_count: u32, - first_vertex: u32, - first_instance: u32, - ) -> &Self { - unsafe { - self.device.cmd_draw( - self.cmd_buf, - vertex_count, - instance_count, - first_vertex, - first_instance, - ); - } - - self - } - - /// Draw indexed primitives. - /// - /// When the command is executed, primitives are assembled using the current primitive topology - /// and `index_count` vertices whose indices are retrieved from the index buffer. The index - /// buffer is treated as an array of tightly packed unsigned integers of size defined by the - /// `index_ty` parameter with which the buffer was bound. - #[profiling::function] - pub fn draw_indexed( - &self, - index_count: u32, - instance_count: u32, - first_index: u32, - vertex_offset: i32, - first_instance: u32, - ) -> &Self { - unsafe { - self.device.cmd_draw_indexed( - self.cmd_buf, - index_count, - instance_count, - first_index, - vertex_offset, - first_instance, - ); - } - - self - } - - /// Draw primitives with indirect parameters and indexed vertices. - /// - /// `draw_indexed_indirect` behaves similarly to `draw_indexed` except that the parameters are - /// read by the device from `buffer` during execution. `draw_count` draws are executed by the - /// command, with parameters taken from `buffer` starting at `offset` and increasing by `stride` - /// bytes for each successive draw. The parameters of each draw are encoded in an array of - /// [`vk::DrawIndexedIndirectCommand`] structures. - /// - /// If `draw_count` is less than or equal to one, `stride` is ignored. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```no_run - /// # use std::sync::Arc; - /// # use std::mem::size_of; - /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; - /// # use screen_13::driver::graphic::{GraphicPipeline, GraphicPipelineInfo}; - /// # use screen_13::driver::image::{Image, ImageInfo}; - /// # use screen_13::driver::shader::Shader; - /// # use screen_13::graph::RenderGraph; - /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); - /// # let my_frag_code = [0u8; 1]; - /// # let my_vert_code = [0u8; 1]; - /// # let vert = Shader::new_vertex(my_vert_code.as_slice()); - /// # let frag = Shader::new_fragment(my_frag_code.as_slice()); - /// # let info = GraphicPipelineInfo::default(); - /// # let my_graphic_pipeline = Arc::new(GraphicPipeline::create(&device, info, [vert, frag])?); - /// # let mut my_graph = RenderGraph::new(); - /// # let buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::INDEX_BUFFER); - /// # let my_idx_buf = Buffer::create(&device, buf_info)?; - /// # let buf_info = BufferInfo::device_mem(8, vk::BufferUsageFlags::VERTEX_BUFFER); - /// # let my_vtx_buf = Buffer::create(&device, buf_info)?; - /// # let my_idx_buf = my_graph.bind_node(my_idx_buf); - /// # let my_vtx_buf = my_graph.bind_node(my_vtx_buf); - /// # let info = ImageInfo::image_2d(32, 32, vk::Format::R8G8B8A8_UNORM, vk::ImageUsageFlags::SAMPLED); - /// # let swapchain_image = my_graph.bind_node(Image::create(&device, info)?); - /// const CMD_SIZE: usize = size_of::(); - /// - /// let cmd = vk::DrawIndexedIndirectCommand { - /// index_count: 3, - /// instance_count: 1, - /// first_index: 0, - /// vertex_offset: 0, - /// first_instance: 0, - /// }; - /// let cmd_data = unsafe { - /// std::slice::from_raw_parts(&cmd as *const _ as *const _, CMD_SIZE) - /// }; - /// - /// let buf_flags = vk::BufferUsageFlags::STORAGE_BUFFER; - /// let buf = Buffer::create_from_slice(&device, buf_flags, cmd_data)?; - /// let buf_node = my_graph.bind_node(buf); - /// - /// my_graph.begin_pass("draw a single triangle") - /// .bind_pipeline(&my_graphic_pipeline) - /// .store_color(0, swapchain_image) - /// .read_node(my_idx_buf) - /// .read_node(my_vtx_buf) - /// .read_node(buf_node) - /// .record_subpass(move |subpass, bindings| { - /// subpass.bind_index_buffer(my_idx_buf, vk::IndexType::UINT16) - /// .bind_vertex_buffer(my_vtx_buf) - /// .draw_indexed_indirect(buf_node, 0, 1, 0); - /// }); - /// # Ok(()) } - /// ``` - #[profiling::function] - pub fn draw_indexed_indirect( - &self, - buffer: impl Into, - offset: vk::DeviceSize, - draw_count: u32, - stride: u32, - ) -> &Self { - let buffer = buffer.into(); - - unsafe { - self.device.cmd_draw_indexed_indirect( - self.cmd_buf, - *self.bindings[buffer], - offset, - draw_count, - stride, - ); - } - - self - } - - /// Draw primitives with indirect parameters, indexed vertices, and draw count. - /// - /// `draw_indexed_indirect_count` behaves similarly to `draw_indexed_indirect` except that the - /// draw count is read by the device from `buffer` during execution. The command will read an - /// unsigned 32-bit integer from `count_buf` located at `count_buf_offset` and use this as the - /// draw count. - /// - /// `max_draw_count` specifies the maximum number of draws that will be executed. The actual - /// number of executed draw calls is the minimum of the count specified in `count_buf` and - /// `max_draw_count`. - /// - /// `stride` is the byte stride between successive sets of draw parameters. - #[profiling::function] - pub fn draw_indexed_indirect_count( - &self, - buffer: impl Into, - offset: vk::DeviceSize, - count_buf: impl Into, - count_buf_offset: vk::DeviceSize, - max_draw_count: u32, - stride: u32, - ) -> &Self { - let buffer = buffer.into(); - let count_buf = count_buf.into(); - - unsafe { - self.device.cmd_draw_indexed_indirect_count( - self.cmd_buf, - *self.bindings[buffer], - offset, - *self.bindings[count_buf], - count_buf_offset, - max_draw_count, - stride, - ); - } - - self - } - - /// Draw primitives with indirect parameters and unindexed vertices. - /// - /// Behaves otherwise similar to [`Draw::draw_indexed_indirect`]. - #[profiling::function] - pub fn draw_indirect( - &self, - buffer: impl Into, - offset: vk::DeviceSize, - draw_count: u32, - stride: u32, - ) -> &Self { - let buffer = buffer.into(); - - unsafe { - self.device.cmd_draw_indirect( - self.cmd_buf, - *self.bindings[buffer], - offset, - draw_count, - stride, - ); - } - - self - } - - /// Draw primitives with indirect parameters, unindexed vertices, and draw count. - /// - /// Behaves otherwise similar to [`Draw::draw_indexed_indirect_count`]. - #[profiling::function] - pub fn draw_indirect_count( - &self, - buffer: impl Into, - offset: vk::DeviceSize, - count_buf: impl Into, - count_buf_offset: vk::DeviceSize, - max_draw_count: u32, - stride: u32, - ) -> &Self { - let buffer = buffer.into(); - let count_buf = count_buf.into(); - - unsafe { - self.device.cmd_draw_indirect_count( - self.cmd_buf, - *self.bindings[buffer], - offset, - *self.bindings[count_buf], - count_buf_offset, - max_draw_count, - stride, - ); - } - - self - } - - /// Updates push constants. - /// - /// Push constants represent a high speed path to modify constant data in pipelines that is - /// expected to outperform memory-backed resource updates. - /// - /// Push constant values can be updated incrementally, causing shader stages to read the new - /// data for push constants modified by this command, while still reading the previous data for - /// push constants not modified by this command. - /// - /// # Device limitations - /// - /// See - /// [`device.physical_device.props.limits.max_push_constants_size`](vk::PhysicalDeviceLimits) - /// for the limits of the current device. You may also check [gpuinfo.org] for a listing of - /// reported limits on other devices. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ``` - /// # inline_spirv::inline_spirv!(r#" - /// #version 450 - /// - /// layout(push_constant) uniform PushConstants { - /// layout(offset = 0) uint the_answer; - /// } push_constants; - /// - /// void main() - /// { - /// // TODO: Add code! - /// } - /// # "#, vert); - /// ``` - /// - /// ```no_run - /// # use std::sync::Arc; - /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::graphic::{GraphicPipeline, GraphicPipelineInfo}; - /// # use screen_13::driver::image::{Image, ImageInfo}; - /// # use screen_13::graph::RenderGraph; - /// # use screen_13::driver::shader::Shader; - /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); - /// # let my_frag_code = [0u8; 1]; - /// # let my_vert_code = [0u8; 1]; - /// # let vert = Shader::new_vertex(my_vert_code.as_slice()); - /// # let frag = Shader::new_fragment(my_frag_code.as_slice()); - /// # let info = GraphicPipelineInfo::default(); - /// # let my_graphic_pipeline = Arc::new(GraphicPipeline::create(&device, info, [vert, frag])?); - /// # let info = ImageInfo::image_2d(32, 32, vk::Format::R8G8B8A8_UNORM, vk::ImageUsageFlags::SAMPLED); - /// # let swapchain_image = Image::create(&device, info)?; - /// # let mut my_graph = RenderGraph::new(); - /// # let swapchain_image = my_graph.bind_node(swapchain_image); - /// my_graph.begin_pass("draw a quad") - /// .bind_pipeline(&my_graphic_pipeline) - /// .store_color(0, swapchain_image) - /// .record_subpass(move |subpass, bindings| { - /// subpass.push_constants(&[42]) - /// .draw(6, 1, 0, 0); - /// }); - /// # Ok(()) } - /// ``` - /// - /// [gpuinfo.org]: https://vulkan.gpuinfo.org/displaydevicelimit.php?name=maxPushConstantsSize&platform=all - pub fn push_constants(&self, data: &[u8]) -> &Self { - self.push_constants_offset(0, data) - } - - /// Updates push constants starting at the given `offset`. - /// - /// Behaves similary to [`Draw::push_constants`] except that `offset` describes the position at - /// which `data` updates the push constants of the currently bound pipeline. This may be used to - /// update a subset or single field of previously set push constant data. - /// - /// # Device limitations - /// - /// See - /// [`device.physical_device.props.limits.max_push_constants_size`](vk::PhysicalDeviceLimits) - /// for the limits of the current device. You may also check [gpuinfo.org] for a listing of - /// reported limits on other devices. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ``` - /// # inline_spirv::inline_spirv!(r#" - /// #version 450 - /// - /// layout(push_constant) uniform PushConstants { - /// layout(offset = 0) uint some_val1; - /// layout(offset = 4) uint some_val2; - /// } push_constants; - /// - /// void main() - /// { - /// // TODO: Add code! - /// } - /// # "#, vert); - /// ``` - /// - /// ```no_run - /// # use std::sync::Arc; - /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::graphic::{GraphicPipeline, GraphicPipelineInfo}; - /// # use screen_13::driver::image::{Image, ImageInfo}; - /// # use screen_13::graph::RenderGraph; - /// # use screen_13::driver::shader::Shader; - /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); - /// # let my_frag_code = [0u8; 1]; - /// # let my_vert_code = [0u8; 1]; - /// # let vert = Shader::new_vertex(my_vert_code.as_slice()); - /// # let frag = Shader::new_fragment(my_frag_code.as_slice()); - /// # let info = GraphicPipelineInfo::default(); - /// # let my_graphic_pipeline = Arc::new(GraphicPipeline::create(&device, info, [vert, frag])?); - /// # let info = ImageInfo::image_2d(32, 32, vk::Format::R8G8B8A8_UNORM, vk::ImageUsageFlags::SAMPLED); - /// # let swapchain_image = Image::create(&device, info)?; - /// # let mut my_graph = RenderGraph::new(); - /// # let swapchain_image = my_graph.bind_node(swapchain_image); - /// my_graph.begin_pass("draw a quad") - /// .bind_pipeline(&my_graphic_pipeline) - /// .store_color(0, swapchain_image) - /// .record_subpass(move |subpass, bindings| { - /// subpass.push_constants(&[0x00, 0x00]) - /// .draw(6, 1, 0, 0) - /// .push_constants_offset(4, &[0xff]) - /// .draw(6, 1, 0, 0); - /// }); - /// # Ok(()) } - /// ``` - /// - /// [gpuinfo.org]: https://vulkan.gpuinfo.org/displaydevicelimit.php?name=maxPushConstantsSize&platform=all - #[profiling::function] - pub fn push_constants_offset(&self, offset: u32, data: &[u8]) -> &Self { - for push_const in self.pipeline.push_constants.iter() { - // Determine the range of the overall pipline push constants which overlap with `data` - let push_const_end = push_const.offset + push_const.size; - let data_end = offset + data.len() as u32; - let end = data_end.min(push_const_end); - let start = offset.max(push_const.offset); - - if end > start { - trace!( - " push constants {:?} {}..{}", - push_const.stage_flags, start, end - ); - - unsafe { - self.device.cmd_push_constants( - self.cmd_buf, - self.pipeline.layout, - push_const.stage_flags, - start, - &data[(start - offset) as usize..(end - offset) as usize], - ); - } - } - } - - self - } - - /// Set scissor rectangle dynamically for a pass. - #[profiling::function] - pub fn set_scissor(&self, x: i32, y: i32, width: u32, height: u32) -> &Self { - unsafe { - self.device.cmd_set_scissor( - self.cmd_buf, - 0, - &[vk::Rect2D { - extent: vk::Extent2D { width, height }, - offset: vk::Offset2D { x, y }, - }], - ); - } - - self - } - - /// Set scissor rectangles dynamically for a pass. - #[profiling::function] - pub fn set_scissors( - &self, - first_scissor: u32, - scissors: impl IntoIterator, - ) -> &Self - where - S: Into, - { - thread_local! { - static SCISSORS: RefCell> = Default::default(); - } - - SCISSORS.with_borrow_mut(|scissors_vec| { - scissors_vec.clear(); - - for scissor in scissors { - scissors_vec.push(scissor.into()); - } - - unsafe { - self.device - .cmd_set_scissor(self.cmd_buf, first_scissor, scissors_vec.as_slice()); - } - }); - - self - } - - /// Set the viewport dynamically for a pass. - #[profiling::function] - pub fn set_viewport( - &self, - x: f32, - y: f32, - width: f32, - height: f32, - depth: Range, - ) -> &Self { - unsafe { - self.device.cmd_set_viewport( - self.cmd_buf, - 0, - &[vk::Viewport { - x, - y, - width, - height, - min_depth: depth.start, - max_depth: depth.end, - }], - ); - } - - self - } - - /// Set the viewports dynamically for a pass. - #[profiling::function] - pub fn set_viewports( - &self, - first_viewport: u32, - viewports: impl IntoIterator, - ) -> &Self - where - V: Into, - { - thread_local! { - static VIEWPORTS: RefCell> = Default::default(); - } - - VIEWPORTS.with_borrow_mut(|viewports_vec| { - viewports_vec.clear(); - - for viewport in viewports { - viewports_vec.push(viewport.into()); - } - - unsafe { - self.device.cmd_set_viewport( - self.cmd_buf, - first_viewport, - viewports_vec.as_slice(), - ); - } - }); - - self - } -} - -/// A general render pass which may contain acceleration structure commands, general commands, or -/// have pipeline bound to then record commands specific to those pipeline types. -pub struct PassRef<'a> { - pub(super) exec_idx: usize, - pub(super) graph: &'a mut RenderGraph, - pub(super) pass_idx: usize, -} - -impl<'a> PassRef<'a> { - pub(super) fn new(graph: &'a mut RenderGraph, name: String) -> PassRef<'a> { - let pass_idx = graph.passes.len(); - graph.passes.push(Pass { - execs: vec![Default::default()], // We start off with a default execution! - name, - }); - - Self { - exec_idx: 0, - graph, - pass_idx, - } - } - - /// Informs the pass that the next recorded command buffer will read or write the given `node` - /// using `access`. - /// - /// This function must be called for `node` before it is read or written within a `record` - /// function. For general purpose access, see [`PassRef::read_node`] or [`PassRef::write_node`]. - pub fn access_node(mut self, node: impl Node + Information, access: AccessType) -> Self { - self.access_node_mut(node, access); - - self - } - - /// Informs the pass that the next recorded command buffer will read or write the given `node` - /// using `access`. - /// - /// This function must be called for `node` before it is read or written within a `record` - /// function. For general purpose access, see [`PassRef::read_node_mut`] or - /// [`PassRef::write_node_mut`]. - pub fn access_node_mut(&mut self, node: impl Node + Information, access: AccessType) { - self.assert_bound_graph_node(node); - - let idx = node.index(); - let binding = &self.graph.bindings[idx]; - - let node_access_range = if let Some(buf) = binding.as_driver_buffer() { - Subresource::Buffer((0..buf.info.size).into()) - } else if let Some(image) = binding.as_driver_image() { - Subresource::Image(image.info.default_view_info().into()) - } else { - Subresource::AccelerationStructure - }; - - self.push_node_access(node, access, node_access_range); - } - - /// Informs the pass that the next recorded command buffer will read or write the `subresource` - /// of `node` using `access`. - /// - /// This function must be called for `node` before it is read or written within a `record` - /// function. For general purpose access, see [`PassRef::read_node`] or [`PassRef::write_node`]. - pub fn access_node_subrange( - mut self, - node: N, - access: AccessType, - subresource: impl Into, - ) -> Self - where - N: View, - { - self.access_node_subrange_mut(node, access, subresource); - - self - } - - /// Informs the pass that the next recorded command buffer will read or write the `subresource` - /// of `node` using `access`. - /// - /// This function must be called for `node` before it is read or written within a `record` - /// function. For general purpose access, see [`PassRef::read_node`] or [`PassRef::write_node`]. - pub fn access_node_subrange_mut( - &mut self, - node: N, - access: AccessType, - subresource: impl Into, - ) where - N: View, - { - self.push_node_access(node, access, subresource.into().into()); - } - - fn as_mut(&mut self) -> &mut Pass { - &mut self.graph.passes[self.pass_idx] - } - - fn as_ref(&self) -> &Pass { - &self.graph.passes[self.pass_idx] - } - - fn assert_bound_graph_node(&self, node: impl Node) { - let idx = node.index(); - - assert!(self.graph.bindings[idx].is_bound()); - } - - /// Binds a Vulkan acceleration structure, buffer, or image to the graph associated with this - /// pass. - /// - /// Bound nodes may be used in passes for pipeline and shader operations. - pub fn bind_node<'b, B>(&'b mut self, binding: B) -> >::Result - where - B: Edge, - B: Bind<&'b mut RenderGraph, >::Result>, - { - self.graph.bind_node(binding) - } - - /// Binds a [`ComputePipeline`], [`GraphicPipeline`], or [`RayTracePipeline`] to the current - /// pass, allowing for strongly typed access to the related functions. - pub fn bind_pipeline(self, binding: B) -> >::Result - where - B: Edge, - B: Bind>::Result>, - { - binding.bind(self) - } - - /// Returns information used to crate a node. - pub fn node_info(&self, node: N) -> ::Info - where - N: Information, - { - node.get(self.graph) - } - - fn push_execute( - &mut self, - func: impl FnOnce(&Device, vk::CommandBuffer, Bindings<'_>) + Send + 'static, - ) { - let pass = self.as_mut(); - let exec = { - let last_exec = pass.execs.last_mut().unwrap(); - last_exec.func = Some(ExecutionFunction(Box::new(func))); - - Execution { - pipeline: last_exec.pipeline.clone(), - ..Default::default() - } - }; - - pass.execs.push(exec); - self.exec_idx += 1; - } - - fn push_node_access(&mut self, node: impl Node, access: AccessType, subresource: Subresource) { - let node_idx = node.index(); - self.assert_bound_graph_node(node); - - let access = SubresourceAccess { - access, - subresource, - }; - self.as_mut() - .execs - .last_mut() - .unwrap() - .accesses - .entry(node_idx) - .and_modify(|accesses| accesses.push(access)) - .or_insert(vec![access]); - } - - /// Informs the pass that the next recorded command buffer will read the given `node` using - /// [`AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer`]. - /// - /// This function must be called for `node` before it is read within a `record` function. For - /// more specific access, see [`PassRef::access_node`]. - pub fn read_node(mut self, node: impl Node + Information) -> Self { - self.read_node_mut(node); - - self - } - - /// Informs the pass that the next recorded command buffer will read the given `node` using - /// [`AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer`]. - /// - /// This function must be called for `node` before it is read within a `record` function. For - /// more specific access, see [`PassRef::access_node`]. - pub fn read_node_mut(&mut self, node: impl Node + Information) { - self.access_node_mut( - node, - AccessType::AnyShaderReadSampledImageOrUniformTexelBuffer, - ); - } - - /// Begin recording an acceleration structure command buffer. - /// - /// This is the entry point for building and updating an [`AccelerationStructure`] instance. - pub fn record_acceleration( - mut self, - func: impl FnOnce(Acceleration<'_>, Bindings<'_>) + Send + 'static, - ) -> Self { - self.push_execute(move |device, cmd_buf, bindings| { - func( - Acceleration { - bindings, - cmd_buf, - device, - }, - bindings, - ); - }); - - self - } - - /// Begin recording a general command buffer. - /// - /// The provided closure allows you to run any Vulkan code, or interoperate with other Vulkan - /// code and interfaces. - pub fn record_cmd_buf( - mut self, - func: impl FnOnce(&Device, vk::CommandBuffer, Bindings<'_>) + Send + 'static, - ) -> Self { - self.push_execute(func); - - self - } - - /// Finalize the recording of this pass and return to the `RenderGraph` where you may record - /// additional passes. - pub fn submit_pass(self) -> &'a mut RenderGraph { - // If nothing was done in this pass we can just ignore it - if self.exec_idx == 0 { - self.graph.passes.pop(); - } - - self.graph - } - - /// Informs the pass that the next recorded command buffer will write the given `node` using - /// [`AccessType::AnyShaderWrite`]. - /// - /// This function must be called for `node` before it is written within a `record` function. For - /// more specific access, see [`PassRef::access_node`]. - pub fn write_node(mut self, node: impl Node + Information) -> Self { - self.write_node_mut(node); - - self - } - - /// Informs the pass that the next recorded command buffer will write the given `node` using - /// [`AccessType::AnyShaderWrite`]. - /// - /// This function must be called for `node` before it is written within a `record` function. For - /// more specific access, see [`PassRef::access_node`]. - pub fn write_node_mut(&mut self, node: impl Node + Information) { - self.access_node_mut(node, AccessType::AnyShaderWrite); - } -} - -/// A render pass which has been bound to a particular compute, graphic, or ray-trace pipeline. -pub struct PipelinePassRef<'a, T> -where - T: Access, -{ - __: PhantomData, - pass: PassRef<'a>, -} - -impl<'a, T> PipelinePassRef<'a, T> -where - T: Access, -{ - /// Informs the pass that the next recorded command buffer will read or write the given `node` - /// at the specified shader descriptor using `access`. - /// - /// This function must be called for `node` before it is read or written within a `record` - /// function. For general purpose access, see [`PipelinePassRef::read_descriptor`] or - /// [`PipelinePassRef::write_descriptor`]. - pub fn access_descriptor( - self, - descriptor: impl Into, - node: N, - access: AccessType, - ) -> Self - where - N: Information, - N: View, - ViewType: From<::Information>, - ::Information: From<::Info>, - ::Subresource: From<::Information>, - { - let view_info = node.get(self.pass.graph); - self.access_descriptor_as(descriptor, node, access, view_info) - } - - /// Informs the pass that the next recorded command buffer will read or write the given `node` - /// at the specified shader descriptor using `access`. The node will be interpreted using - /// `view_info`. - /// - /// This function must be called for `node` before it is read or written within a `record` - /// function. For general purpose access, see [`PipelinePassRef::read_descriptor_as`] or - /// [`PipelinePassRef::write_descriptor_as`]. - pub fn access_descriptor_as( - self, - descriptor: impl Into, - node: N, - access: AccessType, - view_info: impl Into, - ) -> Self - where - N: View, - ::Information: Into, - ::Subresource: From<::Information>, - { - let view_info = view_info.into(); - let subresource = ::Subresource::from(view_info); - - self.access_descriptor_subrange(descriptor, node, access, view_info, subresource) - } - - /// Informs the pass that the next recorded command buffer will read or write the `subresource` - /// of `node` at the specified shader descriptor using `access`. The node will be interpreted - /// using `view_info`. - /// - /// This function must be called for `node` before it is read or written within a `record` - /// function. For general purpose access, see [`PipelinePassRef::read_descriptor_subrange`] or - /// [`PipelinePassRef::write_descriptor_subrange`]. - pub fn access_descriptor_subrange( - mut self, - descriptor: impl Into, - node: N, - access: AccessType, - view_info: impl Into, - subresource: impl Into, - ) -> Self - where - N: View, - ::Information: Into, - { - self.pass - .push_node_access(node, access, subresource.into().into()); - self.push_node_view_bind(node, view_info.into(), descriptor.into()); - - self - } - - /// Informs the pass that the next recorded command buffer will read or write the given `node` - /// using `access`. - /// - /// This function must be called for `node` before it is read or written within a `record` - /// function. For general purpose access, see [`PipelinePassRef::read_node`] or - /// [`PipelinePassRef::write_node`]. - pub fn access_node(mut self, node: impl Node + Information, access: AccessType) -> Self { - self.access_node_mut(node, access); - - self - } - - /// Informs the pass that the next recorded command buffer will read or write the given `node` - /// using `access`. - /// - /// This function must be called for `node` before it is read or written within a `record` - /// function. For general purpose access, see [`PipelinePassRef::read_node_mut`] or - /// [`PipelinePassRef::write_node_mut`]. - pub fn access_node_mut(&mut self, node: impl Node + Information, access: AccessType) { - self.pass.assert_bound_graph_node(node); - - let idx = node.index(); - let binding = &self.pass.graph.bindings[idx]; - - let node_access_range = if let Some(buf) = binding.as_driver_buffer() { - Subresource::Buffer((0..buf.info.size).into()) - } else if let Some(image) = binding.as_driver_image() { - Subresource::Image(image.info.default_view_info().into()) - } else { - Subresource::AccelerationStructure - }; - - self.pass.push_node_access(node, access, node_access_range); - } - - /// Informs the pass that the next recorded command buffer will read or write the `subresource` - /// of `node` using `access`. - /// - /// This function must be called for `node` before it is read or written within a `record` - /// function. For general purpose access, see [`PipelinePassRef::read_node_subrange`] or - /// [`PipelinePassRef::write_node_subrange`]. - pub fn access_node_subrange( - mut self, - node: N, - access: AccessType, - subresource: impl Into, - ) -> Self - where - N: View, - { - self.access_node_subrange_mut(node, access, subresource); - - self - } - - /// Informs the pass that the next recorded command buffer will read or write the `subresource` - /// of `node` using `access`. - /// - /// This function must be called for `node` before it is read or written within a `record` - /// function. For general purpose access, see [`PipelinePassRef::read_node_subrange_mut`] or - /// [`PipelinePassRef::write_node_subrange_mut`]. - pub fn access_node_subrange_mut( - &mut self, - node: N, - access: AccessType, - subresource: impl Into, - ) where - N: View, - { - self.pass - .push_node_access(node, access, subresource.into().into()); - } - - /// Binds a Vulkan acceleration structure, buffer, or image to the graph associated with this - /// pass. - /// - /// Bound nodes may be used in passes for pipeline and shader operations. - pub fn bind_node<'b, B>(&'b mut self, binding: B) -> >::Result - where - B: Edge, - B: Bind<&'b mut RenderGraph, >::Result>, - { - self.pass.graph.bind_node(binding) - } - - /// Returns information used to crate a node. - pub fn node_info(&self, node: N) -> ::Info - where - N: Information, - { - node.get(self.pass.graph) - } - - fn push_node_view_bind( - &mut self, - node: impl Node, - view_info: impl Into, - binding: Descriptor, - ) { - let node_idx = node.index(); - self.pass.assert_bound_graph_node(node); - - assert!( - self.pass - .as_mut() - .execs - .last_mut() - .unwrap() - .bindings - .insert(binding, (node_idx, Some(view_info.into()))) - .is_none(), - "descriptor {binding:?} has already been bound" - ); - } - - /// Informs the pass that the next recorded command buffer will read the given `node` at the - /// specified shader descriptor. - /// - /// The [`AccessType`] is inferred by the currently bound pipeline. See [`Access`] for details. - /// - /// This function must be called for `node` before it is read within a `record` function. For - /// more specific access, see [`PipelinePassRef::access_descriptor`]. - pub fn read_descriptor(self, descriptor: impl Into, node: N) -> Self - where - N: Information, - N: View, - ViewType: From<::Information>, - ::Information: From<::Info>, - ::Subresource: From<::Information>, - { - let view_info = node.get(self.pass.graph); - self.read_descriptor_as(descriptor, node, view_info) - } - - /// Informs the pass that the next recorded command buffer will read the given `node` at the - /// specified shader descriptor. The node will be interpreted using `view_info`. - /// - /// The [`AccessType`] is inferred by the currently bound pipeline. See [`Access`] for details. - /// - /// This function must be called for `node` before it is read within a `record` function. For - /// more specific access, see [`PipelinePassRef::access_descriptor_as`]. - pub fn read_descriptor_as( - self, - descriptor: impl Into, - node: N, - view_info: impl Into, - ) -> Self - where - N: View, - ::Information: Into, - ::Subresource: From<::Information>, - { - let view_info = view_info.into(); - let subresource = ::Subresource::from(view_info); - - self.read_descriptor_subrange(descriptor, node, view_info, subresource) - } - - /// Informs the pass that the next recorded command buffer will read the `subresource` of `node` - /// at the specified shader descriptor. The node will be interpreted using `view_info`. - /// - /// The [`AccessType`] is inferred by the currently bound pipeline. See [`Access`] for details. - /// - /// This function must be called for `node` before it is read within a `record` function. For - /// more specific access, see [`PipelinePassRef::access_descriptor_subrange`]. - pub fn read_descriptor_subrange( - self, - descriptor: impl Into, - node: N, - view_info: impl Into, - subresource: impl Into, - ) -> Self - where - N: View, - ::Information: Into, - { - let access = ::DEFAULT_READ; - self.access_descriptor_subrange(descriptor, node, access, view_info, subresource) - } - - /// Informs the pass that the next recorded command buffer will read the given `node`. - /// - /// The [`AccessType`] is inferred by the currently bound pipeline. See [`Access`] for details. - /// - /// This function must be called for `node` before it is read within a `record` function. For - /// more specific access, see [`PipelinePassRef::access_node`]. - pub fn read_node(mut self, node: impl Node + Information) -> Self { - self.read_node_mut(node); - - self - } - - /// Informs the pass that the next recorded command buffer will read the given `node`. - /// - /// The [`AccessType`] is inferred by the currently bound pipeline. See [`Access`] for details. - /// - /// This function must be called for `node` before it is read within a `record` function. For - /// more specific access, see [`PipelinePassRef::access_node_mut`]. - pub fn read_node_mut(&mut self, node: impl Node + Information) { - let access = ::DEFAULT_READ; - self.access_node_mut(node, access); - } - - /// Informs the pass that the next recorded command buffer will read the `subresource` of - /// `node`. - /// - /// The [`AccessType`] is inferred by the currently bound pipeline. See [`Access`] for details. - /// - /// This function must be called for `node` before it is read within a `record` function. For - /// more specific access, see [`PipelinePassRef::access_node_subrange`]. - pub fn read_node_subrange(mut self, node: N, subresource: impl Into) -> Self - where - N: View, - { - self.read_node_subrange_mut(node, subresource); - - self - } - - /// Informs the pass that the next recorded command buffer will read the `subresource` of - /// `node`. - /// - /// The [`AccessType`] is inferred by the currently bound pipeline. See [`Access`] for details. - /// - /// This function must be called for `node` before it is read within a `record` function. For - /// more specific access, see [`PipelinePassRef::access_node_subrange_mut`]. - pub fn read_node_subrange_mut(&mut self, node: N, subresource: impl Into) - where - N: View, - { - let access = ::DEFAULT_READ; - self.access_node_subrange_mut(node, access, subresource); - } - - /// Finalizes a pass and returns the render graph so that additional passes may be added. - pub fn submit_pass(self) -> &'a mut RenderGraph { - self.pass.submit_pass() - } - - /// Informs the pass that the next recorded command buffer will write the given `node` at the - /// specified shader descriptor. - /// - /// The [`AccessType`] is inferred by the currently bound pipeline. See [`Access`] for details. - /// - /// This function must be called for `node` before it is written within a `record` function. For - /// more specific access, see [`PipelinePassRef::access_descriptor`]. - pub fn write_descriptor(self, descriptor: impl Into, node: N) -> Self - where - N: Information, - N: View, - ::Information: Into, - ::Information: From<::Info>, - ::Subresource: From<::Information>, - { - let view_info = node.get(self.pass.graph); - self.write_descriptor_as(descriptor, node, view_info) - } - - /// Informs the pass that the next recorded command buffer will write the given `node` at the - /// specified shader descriptor. The node will be interpreted using `view_info`. - /// - /// The [`AccessType`] is inferred by the currently bound pipeline. See [`Access`] for details. - /// - /// This function must be called for `node` before it is written within a `record` function. For - /// more specific access, see [`PipelinePassRef::access_descriptor_as`]. - pub fn write_descriptor_as( - self, - descriptor: impl Into, - node: N, - view_info: impl Into, - ) -> Self - where - N: View, - ::Information: Into, - ::Subresource: From<::Information>, - { - let view_info = view_info.into(); - let subresource = ::Subresource::from(view_info); - - self.write_descriptor_subrange(descriptor, node, view_info, subresource) - } - - /// Informs the pass that the next recorded command buffer will write the `subresource` of - /// `node` at the specified shader descriptor. The node will be interpreted using `view_info`. - /// - /// The [`AccessType`] is inferred by the currently bound pipeline. See [`Access`] for details. - /// - /// This function must be called for `node` before it is written within a `record` function. For - /// more specific access, see [`PipelinePassRef::access_descriptor_subrange`]. - pub fn write_descriptor_subrange( - self, - descriptor: impl Into, - node: N, - view_info: impl Into, - subresource: impl Into, - ) -> Self - where - N: View, - ::Information: Into, - { - let access = ::DEFAULT_WRITE; - self.access_descriptor_subrange(descriptor, node, access, view_info, subresource) - } - - /// Informs the pass that the next recorded command buffer will write the given `node`. - /// - /// The [`AccessType`] is inferred by the currently bound pipeline. See [`Access`] for details. - /// - /// This function must be called for `node` before it is written within a `record` function. For - /// more specific access, see [`PipelinePassRef::access_node`]. - pub fn write_node(mut self, node: impl Node + Information) -> Self { - self.write_node_mut(node); - - self - } - - /// Informs the pass that the next recorded command buffer will write the given `node`. - /// - /// The [`AccessType`] is inferred by the currently bound pipeline. See [`Access`] for details. - /// - /// This function must be called for `node` before it is written within a `record` function. For - /// more specific access, see [`PipelinePassRef::access_node_mut`]. - pub fn write_node_mut(&mut self, node: impl Node + Information) { - let access = ::DEFAULT_WRITE; - self.access_node_mut(node, access); - } - - /// Informs the pass that the next recorded command buffer will write the `subresource` of - /// `node`. - /// - /// The [`AccessType`] is inferred by the currently bound pipeline. See [`Access`] for details. - /// - /// This function must be called for `node` before it is written within a `record` function. For - /// more specific access, see [`PipelinePassRef::access_node_subrange`]. - pub fn write_node_subrange(mut self, node: N, subresource: impl Into) -> Self - where - N: View, - { - self.write_node_subrange_mut(node, subresource); - - self - } - - /// Informs the pass that the next recorded command buffer will write the `subresource` of - /// `node`. - /// - /// The [`AccessType`] is inferred by the currently bound pipeline. See [`Access`] for details. - /// - /// This function must be called for `node` before it is written within a `record` function. For - /// more specific access, see [`PipelinePassRef::access_node_subrange_mut`]. - pub fn write_node_subrange_mut(&mut self, node: N, subresource: impl Into) - where - N: View, - { - let access = ::DEFAULT_WRITE; - self.access_node_subrange_mut(node, access, subresource); - } -} - -impl PipelinePassRef<'_, ComputePipeline> { - /// Begin recording a computing command buffer. - pub fn record_compute( - mut self, - func: impl FnOnce(Compute<'_>, Bindings<'_>) + Send + 'static, - ) -> Self { - let pipeline = Arc::clone( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .pipeline - .as_ref() - .unwrap() - .unwrap_compute(), - ); - - self.pass.push_execute(move |device, cmd_buf, bindings| { - func( - Compute { - bindings, - cmd_buf, - device, - pipeline, - }, - bindings, - ); - }); - - self - } -} - -impl PipelinePassRef<'_, GraphicPipeline> { - /// Specifies `VK_ATTACHMENT_LOAD_OP_DONT_CARE` for the render pass attachment, and loads an - /// image into the framebuffer. - pub fn attach_color( - self, - attachment_idx: AttachmentIndex, - image: impl Into, - ) -> Self { - let image: AnyImageNode = image.into(); - let image_info = image.get(self.pass.graph); - let image_view_info: ImageViewInfo = image_info.into(); - - self.attach_color_as(attachment_idx, image, image_view_info) - } - - /// Specifies `VK_ATTACHMENT_LOAD_OP_DONT_CARE` for the render pass attachment, and loads an - /// image into the framebuffer. - pub fn attach_color_as( - mut self, - attachment_idx: AttachmentIndex, - image: impl Into, - image_view_info: impl Into, - ) -> Self { - let image = image.into(); - let image_view_info = image_view_info.into(); - let node_idx = image.index(); - let (_, sample_count) = self.image_info(node_idx); - - debug_assert!( - !self - .pass - .as_ref() - .execs - .last() - .unwrap() - .color_clears - .contains_key(&attachment_idx), - "color attachment {attachment_idx} already attached via clear" - ); - debug_assert!( - !self - .pass - .as_ref() - .execs - .last() - .unwrap() - .color_loads - .contains_key(&attachment_idx), - "color attachment {attachment_idx} already attached via load" - ); - - self.pass - .as_mut() - .execs - .last_mut() - .unwrap() - .color_attachments - .insert( - attachment_idx, - Attachment::new(image_view_info, sample_count, node_idx), - ); - - debug_assert!( - Attachment::are_compatible( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_resolves - .get(&attachment_idx) - .map(|(attachment, _)| *attachment), - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_attachments - .get(&attachment_idx) - .copied() - ), - "color attachment {attachment_idx} incompatible with existing resolve" - ); - debug_assert!( - Attachment::are_compatible( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_stores - .get(&attachment_idx) - .copied(), - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_attachments - .get(&attachment_idx) - .copied() - ), - "color attachment {attachment_idx} incompatible with existing store" - ); - - self.pass.push_node_access( - image, - AccessType::ColorAttachmentWrite, - Subresource::Image(image_view_info.into()), - ); - - self - } - - /// Specifies `VK_ATTACHMENT_LOAD_OP_DONT_CARE` for the render pass attachment, and loads an - /// image into the framebuffer. - pub fn attach_depth_stencil(self, image: impl Into) -> Self { - let image: AnyImageNode = image.into(); - let image_info = image.get(self.pass.graph); - let image_view_info: ImageViewInfo = image_info.into(); - - self.attach_depth_stencil_as(image, image_view_info) - } - - /// Specifies `VK_ATTACHMENT_LOAD_OP_DONT_CARE` for the render pass attachment, and loads an - /// image into the framebuffer. - pub fn attach_depth_stencil_as( - mut self, - image: impl Into, - image_view_info: impl Into, - ) -> Self { - let image = image.into(); - let image_view_info = image_view_info.into(); - let node_idx = image.index(); - let (_, sample_count) = self.image_info(node_idx); - - debug_assert!( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .depth_stencil_clear - .is_none(), - "depth/stencil attachment already attached via clear" - ); - debug_assert!( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .depth_stencil_load - .is_none(), - "depth/stencil attachment already attached via load" - ); - - self.pass - .as_mut() - .execs - .last_mut() - .unwrap() - .depth_stencil_attachment = - Some(Attachment::new(image_view_info, sample_count, node_idx)); - - debug_assert!( - Attachment::are_compatible( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .depth_stencil_resolve - .map(|(attachment, ..)| attachment), - self.pass - .as_ref() - .execs - .last() - .unwrap() - .depth_stencil_attachment - ), - "depth/stencil attachment incompatible with existing resolve" - ); - debug_assert!( - Attachment::are_compatible( - self.pass.as_ref().execs.last().unwrap().depth_stencil_store, - self.pass - .as_ref() - .execs - .last() - .unwrap() - .depth_stencil_attachment - ), - "depth/stencil attachment incompatible with existing store" - ); - - self.pass.push_node_access( - image, - if image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::DEPTH | vk::ImageAspectFlags::STENCIL) - { - AccessType::DepthStencilAttachmentWrite - } else if image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::DEPTH) - { - AccessType::DepthAttachmentWriteStencilReadOnly - } else { - AccessType::StencilAttachmentWriteDepthReadOnly - }, - Subresource::Image(image_view_info.into()), - ); - - self - } - - /// Clears the render pass attachment of any existing data. - pub fn clear_color( - self, - attachment_idx: AttachmentIndex, - image: impl Into, - ) -> Self { - self.clear_color_value(attachment_idx, image, [0.0, 0.0, 0.0, 0.0]) - } - - /// Clears the render pass attachment of any existing data. - pub fn clear_color_value( - self, - attachment_idx: AttachmentIndex, - image: impl Into, - color: impl Into, - ) -> Self { - let image: AnyImageNode = image.into(); - let image_info = image.get(self.pass.graph); - let image_view_info: ImageViewInfo = image_info.into(); - - self.clear_color_value_as(attachment_idx, image, color, image_view_info) - } - - /// Clears the render pass attachment of any existing data. - pub fn clear_color_value_as( - mut self, - attachment_idx: AttachmentIndex, - image: impl Into, - color: impl Into, - image_view_info: impl Into, - ) -> Self { - let image = image.into(); - let image_view_info = image_view_info.into(); - let node_idx = image.index(); - let (_, sample_count) = self.image_info(node_idx); - - let color = color.into(); - - debug_assert!( - !self - .pass - .as_ref() - .execs - .last() - .unwrap() - .color_attachments - .contains_key(&attachment_idx), - "color attachment {attachment_idx} already attached" - ); - debug_assert!( - !self - .pass - .as_ref() - .execs - .last() - .unwrap() - .color_loads - .contains_key(&attachment_idx), - "color attachment {attachment_idx} already attached via load" - ); - - self.pass - .as_mut() - .execs - .last_mut() - .unwrap() - .color_clears - .insert( - attachment_idx, - ( - Attachment::new(image_view_info, sample_count, node_idx), - color, - ), - ); - - debug_assert!( - Attachment::are_compatible( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_resolves - .get(&attachment_idx) - .map(|(attachment, _)| *attachment), - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_clears - .get(&attachment_idx) - .map(|(attachment, _)| *attachment) - ), - "color attachment {attachment_idx} clear incompatible with existing resolve" - ); - debug_assert!( - Attachment::are_compatible( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_stores - .get(&attachment_idx) - .copied(), - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_clears - .get(&attachment_idx) - .map(|(attachment, _)| *attachment) - ), - "color attachment {attachment_idx} clear incompatible with existing store" - ); - - let mut image_access = AccessType::ColorAttachmentWrite; - let image_range = image_view_info.into(); - - // Upgrade existing read access to read-write - if let Some(accesses) = self - .pass - .as_mut() - .execs - .last_mut() - .unwrap() - .accesses - .get_mut(&node_idx) - { - for SubresourceAccess { - access, - subresource, - } in accesses - { - let access_image_range = *subresource.as_image().unwrap(); - if !image_subresource_range_intersects(access_image_range, image_range) { - continue; - } - - image_access = match *access { - AccessType::ColorAttachmentRead | AccessType::ColorAttachmentReadWrite => { - AccessType::ColorAttachmentReadWrite - } - AccessType::ColorAttachmentWrite => AccessType::ColorAttachmentWrite, - _ => continue, - }; - - *access = image_access; - - // If the clear access is a subset of the existing access range there is no need - // to push a new access - if image_subresource_range_contains(access_image_range, image_range) { - return self; - } - } - } - - self.pass - .push_node_access(image, image_access, Subresource::Image(image_range)); - - self - } - - /// Clears the render pass attachment of any existing data. - pub fn clear_depth_stencil(self, image: impl Into) -> Self { - self.clear_depth_stencil_value(image, 1.0, 0) - } - - /// Clears the render pass attachment of any existing data. - pub fn clear_depth_stencil_value( - self, - image: impl Into, - depth: f32, - stencil: u32, - ) -> Self { - let image: AnyImageNode = image.into(); - let image_info = image.get(self.pass.graph); - let image_view_info: ImageViewInfo = image_info.into(); - - self.clear_depth_stencil_value_as(image, depth, stencil, image_view_info) - } - - /// Clears the render pass attachment of any existing data. - pub fn clear_depth_stencil_value_as( - mut self, - image: impl Into, - depth: f32, - stencil: u32, - image_view_info: impl Into, - ) -> Self { - let image = image.into(); - let image_view_info = image_view_info.into(); - let node_idx = image.index(); - let (_, sample_count) = self.image_info(node_idx); - - debug_assert!( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .depth_stencil_attachment - .is_none(), - "depth/stencil attachment already attached" - ); - debug_assert!( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .depth_stencil_load - .is_none(), - "depth/stencil attachment already attached via load" - ); - - self.pass - .as_mut() - .execs - .last_mut() - .unwrap() - .depth_stencil_clear = Some(( - Attachment::new(image_view_info, sample_count, node_idx), - vk::ClearDepthStencilValue { depth, stencil }, - )); - - debug_assert!( - Attachment::are_compatible( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .depth_stencil_resolve - .map(|(attachment, ..)| attachment), - self.pass - .as_ref() - .execs - .last() - .unwrap() - .depth_stencil_clear - .map(|(attachment, _)| attachment) - ), - "depth/stencil attachment clear incompatible with existing resolve" - ); - debug_assert!( - Attachment::are_compatible( - self.pass.as_ref().execs.last().unwrap().depth_stencil_store, - self.pass - .as_ref() - .execs - .last() - .unwrap() - .depth_stencil_clear - .map(|(attachment, _)| attachment) - ), - "depth/stencil attachment clear incompatible with existing store" - ); - - let mut image_access = if image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::DEPTH | vk::ImageAspectFlags::STENCIL) - { - AccessType::DepthStencilAttachmentWrite - } else if image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::DEPTH) - { - AccessType::DepthAttachmentWriteStencilReadOnly - } else { - debug_assert!( - image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::STENCIL) - ); - - AccessType::StencilAttachmentWriteDepthReadOnly - }; - let image_range = image_view_info.into(); - - // Upgrade existing read access to read-write - if let Some(accesses) = self - .pass - .as_mut() - .execs - .last_mut() - .unwrap() - .accesses - .get_mut(&node_idx) - { - for SubresourceAccess { - access, - subresource, - } in accesses - { - let access_image_range = *subresource.as_image().unwrap(); - if !image_subresource_range_intersects(access_image_range, image_range) { - continue; - } - - image_access = match *access { - AccessType::DepthAttachmentWriteStencilReadOnly => { - if image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::STENCIL) - { - AccessType::DepthStencilAttachmentReadWrite - } else { - AccessType::DepthAttachmentWriteStencilReadOnly - } - } - AccessType::DepthStencilAttachmentRead => { - if !image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::DEPTH) - { - AccessType::StencilAttachmentWriteDepthReadOnly - } else { - AccessType::DepthAttachmentWriteStencilReadOnly - } - } - AccessType::DepthStencilAttachmentWrite => { - AccessType::DepthStencilAttachmentWrite - } - AccessType::StencilAttachmentWriteDepthReadOnly => { - if image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::DEPTH) - { - AccessType::DepthStencilAttachmentReadWrite - } else { - AccessType::StencilAttachmentWriteDepthReadOnly - } - } - _ => continue, - }; - - *access = image_access; - - // If the clear access is a subset of the existing access range there is no need - // to push a new access - if image_subresource_range_contains(access_image_range, image_range) { - return self; - } - } - } - - self.pass - .push_node_access(image, image_access, Subresource::Image(image_range)); - - self - } - - fn image_info(&self, node_idx: NodeIndex) -> (vk::Format, SampleCount) { - let image_info = self.pass.graph.bindings[node_idx] - .as_driver_image() - .unwrap() - .info; - - (image_info.fmt, image_info.sample_count) - } - - /// Specifies `VK_ATTACHMENT_LOAD_OP_LOAD` for the render pass attachment, and loads an image - /// into the framebuffer. - pub fn load_color( - self, - attachment_idx: AttachmentIndex, - image: impl Into, - ) -> Self { - let image: AnyImageNode = image.into(); - let image_info = image.get(self.pass.graph); - let image_view_info: ImageViewInfo = image_info.into(); - - self.load_color_as(attachment_idx, image, image_view_info) - } - - /// Specifies `VK_ATTACHMENT_LOAD_OP_LOAD` for the render pass attachment, and loads an image - /// into the framebuffer. - pub fn load_color_as( - mut self, - attachment_idx: AttachmentIndex, - image: impl Into, - image_view_info: impl Into, - ) -> Self { - let image = image.into(); - let image_view_info = image_view_info.into(); - let node_idx = image.index(); - let (_, sample_count) = self.image_info(node_idx); - - debug_assert!( - !self - .pass - .as_ref() - .execs - .last() - .unwrap() - .color_attachments - .contains_key(&attachment_idx), - "color attachment {attachment_idx} already attached" - ); - debug_assert!( - !self - .pass - .as_ref() - .execs - .last() - .unwrap() - .color_clears - .contains_key(&attachment_idx), - "color attachment {attachment_idx} already attached via clear" - ); - - self.pass - .as_mut() - .execs - .last_mut() - .unwrap() - .color_loads - .insert( - attachment_idx, - Attachment::new(image_view_info, sample_count, node_idx), - ); - - debug_assert!( - Attachment::are_compatible( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_resolves - .get(&attachment_idx) - .map(|(attachment, _)| *attachment), - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_loads - .get(&attachment_idx) - .copied() - ), - "color attachment {attachment_idx} load incompatible with existing resolve" - ); - debug_assert!( - Attachment::are_compatible( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_stores - .get(&attachment_idx) - .copied(), - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_loads - .get(&attachment_idx) - .copied() - ), - "color attachment {attachment_idx} load incompatible with existing store" - ); - - let mut image_access = AccessType::ColorAttachmentRead; - let image_range = image_view_info.into(); - - // Upgrade existing write access to read-write - if let Some(accesses) = self - .pass - .as_mut() - .execs - .last_mut() - .unwrap() - .accesses - .get_mut(&node_idx) - { - for SubresourceAccess { - access, - subresource, - } in accesses - { - let access_image_range = *subresource.as_image().unwrap(); - if !image_subresource_range_intersects(access_image_range, image_range) { - continue; - } - - image_access = match *access { - AccessType::ColorAttachmentRead => AccessType::ColorAttachmentRead, - AccessType::ColorAttachmentReadWrite | AccessType::ColorAttachmentWrite => { - AccessType::ColorAttachmentReadWrite - } - _ => continue, - }; - - *access = image_access; - - // If the load access is a subset of the existing access range there is no need - // to push a new access - if image_subresource_range_contains(access_image_range, image_range) { - return self; - } - } - } - - self.pass - .push_node_access(image, image_access, Subresource::Image(image_range)); - - self - } - - /// Specifies `VK_ATTACHMENT_LOAD_OP_LOAD` for the render pass attachment, and loads an image - /// into the framebuffer. - pub fn load_depth_stencil(self, image: impl Into) -> Self { - let image: AnyImageNode = image.into(); - let image_info = image.get(self.pass.graph); - let image_view_info: ImageViewInfo = image_info.into(); - - self.load_depth_stencil_as(image, image_view_info) - } - - /// Specifies `VK_ATTACHMENT_LOAD_OP_LOAD` for the render pass attachment, and loads an image - /// into the framebuffer. - pub fn load_depth_stencil_as( - mut self, - image: impl Into, - image_view_info: impl Into, - ) -> Self { - let image = image.into(); - let image_view_info = image_view_info.into(); - let node_idx = image.index(); - let (_, sample_count) = self.image_info(node_idx); - - debug_assert!( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .depth_stencil_attachment - .is_none(), - "depth/stencil attachment already attached" - ); - debug_assert!( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .depth_stencil_clear - .is_none(), - "depth/stencil attachment already attached via clear" - ); - - self.pass - .as_mut() - .execs - .last_mut() - .unwrap() - .depth_stencil_load = Some(Attachment::new(image_view_info, sample_count, node_idx)); - - debug_assert!( - Attachment::are_compatible( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .depth_stencil_resolve - .map(|(attachment, ..)| attachment), - self.pass.as_ref().execs.last().unwrap().depth_stencil_load - ), - "depth/stencil attachment load incompatible with existing resolve" - ); - debug_assert!( - Attachment::are_compatible( - self.pass.as_ref().execs.last().unwrap().depth_stencil_store, - self.pass.as_ref().execs.last().unwrap().depth_stencil_load - ), - "depth/stencil attachment load incompatible with existing store" - ); - - let mut image_access = AccessType::DepthStencilAttachmentRead; - let image_range = image_view_info.into(); - - // Upgrade existing write access to read-write - if let Some(accesses) = self - .pass - .as_mut() - .execs - .last_mut() - .unwrap() - .accesses - .get_mut(&node_idx) - { - for SubresourceAccess { - access, - subresource, - } in accesses - { - let access_image_range = *subresource.as_image().unwrap(); - if !image_subresource_range_intersects(access_image_range, image_range) { - continue; - } - - image_access = match *access { - AccessType::DepthAttachmentWriteStencilReadOnly => { - AccessType::DepthAttachmentWriteStencilReadOnly - } - AccessType::DepthStencilAttachmentRead => { - AccessType::DepthStencilAttachmentRead - } - AccessType::DepthStencilAttachmentWrite => { - AccessType::DepthStencilAttachmentReadWrite - } - AccessType::StencilAttachmentWriteDepthReadOnly => { - AccessType::StencilAttachmentWriteDepthReadOnly - } - _ => continue, - }; - - *access = image_access; - - // If the load access is a subset of the existing access range there is no need - // to push a new access - if image_subresource_range_contains(access_image_range, image_range) { - return self; - } - } - } - - self.pass - .push_node_access(image, image_access, Subresource::Image(image_range)); - - self - } - - /// Begin recording a graphics command buffer. - pub fn record_subpass( - mut self, - func: impl FnOnce(Draw<'_>, Bindings<'_>) + Send + 'static, - ) -> Self { - let pipeline = Arc::clone( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .pipeline - .as_ref() - .unwrap() - .unwrap_graphic(), - ); - - self.pass.push_execute(move |device, cmd_buf, bindings| { - func( - Draw { - bindings, - cmd_buf, - device, - pipeline, - }, - bindings, - ); - }); - - self - } - - /// Resolves a multisample framebuffer to a non-multisample image for the render pass - /// attachment. - pub fn resolve_color( - self, - src_attachment_idx: AttachmentIndex, - dst_attachment_idx: AttachmentIndex, - image: impl Into, - ) -> Self { - let image: AnyImageNode = image.into(); - let image_info = image.get(self.pass.graph); - let image_view_info: ImageViewInfo = image_info.into(); - - self.resolve_color_as( - src_attachment_idx, - dst_attachment_idx, - image, - image_view_info, - ) - } - - /// Resolves a multisample framebuffer to a non-multisample image for the render pass - /// attachment. - pub fn resolve_color_as( - mut self, - src_attachment_idx: AttachmentIndex, - dst_attachment_idx: AttachmentIndex, - image: impl Into, - image_view_info: impl Into, - ) -> Self { - let image = image.into(); - let image_view_info = image_view_info.into(); - let node_idx = image.index(); - let (_, sample_count) = self.image_info(node_idx); - - self.pass - .as_mut() - .execs - .last_mut() - .unwrap() - .color_resolves - .insert( - dst_attachment_idx, - ( - Attachment::new(image_view_info, sample_count, node_idx), - src_attachment_idx, - ), - ); - - debug_assert!( - Attachment::are_compatible( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_attachments - .get(&dst_attachment_idx) - .copied(), - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_resolves - .get(&dst_attachment_idx) - .map(|(attachment, _)| *attachment) - ), - "color attachment {dst_attachment_idx} resolve incompatible with existing attachment" - ); - debug_assert!( - Attachment::are_compatible( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_clears - .get(&dst_attachment_idx) - .map(|(attachment, _)| *attachment), - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_resolves - .get(&dst_attachment_idx) - .map(|(attachment, _)| *attachment) - ), - "color attachment {dst_attachment_idx} resolve incompatible with existing clear" - ); - debug_assert!( - Attachment::are_compatible( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_loads - .get(&dst_attachment_idx) - .copied(), - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_resolves - .get(&dst_attachment_idx) - .map(|(attachment, _)| *attachment) - ), - "color attachment {dst_attachment_idx} resolve incompatible with existing load" - ); - - let mut image_access = AccessType::ColorAttachmentWrite; - let image_range = image_view_info.into(); - - // Upgrade existing read access to read-write - if let Some(accesses) = self - .pass - .as_mut() - .execs - .last_mut() - .unwrap() - .accesses - .get_mut(&node_idx) - { - for SubresourceAccess { - access, - subresource, - } in accesses - { - let access_image_range = *subresource.as_image().unwrap(); - if !image_subresource_range_intersects(access_image_range, image_range) { - continue; - } - - image_access = match *access { - AccessType::ColorAttachmentRead | AccessType::ColorAttachmentReadWrite => { - AccessType::ColorAttachmentReadWrite - } - AccessType::ColorAttachmentWrite => AccessType::ColorAttachmentWrite, - _ => continue, - }; - - *access = image_access; - - // If the resolve access is a subset of the existing access range there is no need - // to push a new access - if image_subresource_range_contains(access_image_range, image_range) { - return self; - } - } - } - - self.pass - .push_node_access(image, image_access, Subresource::Image(image_range)); - - self - } - - /// Resolves a multisample framebuffer to a non-multisample image for the render pass - /// attachment. - pub fn resolve_depth_stencil( - self, - dst_attachment_idx: AttachmentIndex, - image: impl Into, - depth_mode: Option, - stencil_mode: Option, - ) -> Self { - let image: AnyImageNode = image.into(); - let image_info = image.get(self.pass.graph); - let image_view_info: ImageViewInfo = image_info.into(); - - self.resolve_depth_stencil_as( - dst_attachment_idx, - image, - image_view_info, - depth_mode, - stencil_mode, - ) - } - - /// Resolves a multisample framebuffer to a non-multisample image for the render pass - /// attachment. - pub fn resolve_depth_stencil_as( - mut self, - dst_attachment_idx: AttachmentIndex, - image: impl Into, - image_view_info: impl Into, - depth_mode: Option, - stencil_mode: Option, - ) -> Self { - let image = image.into(); - let image_view_info = image_view_info.into(); - let node_idx = image.index(); - let (_, sample_count) = self.image_info(node_idx); - - self.pass - .as_mut() - .execs - .last_mut() - .unwrap() - .depth_stencil_resolve = Some(( - Attachment::new(image_view_info, sample_count, node_idx), - dst_attachment_idx, - depth_mode, - stencil_mode, - )); - - let mut image_access = if image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::DEPTH | vk::ImageAspectFlags::STENCIL) - { - AccessType::DepthStencilAttachmentWrite - } else if image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::DEPTH) - { - AccessType::DepthAttachmentWriteStencilReadOnly - } else { - debug_assert!( - image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::STENCIL) - ); - - AccessType::StencilAttachmentWriteDepthReadOnly - }; - let image_range = image_view_info.into(); - - // Upgrade existing read access to read-write - if let Some(accesses) = self - .pass - .as_mut() - .execs - .last_mut() - .unwrap() - .accesses - .get_mut(&node_idx) - { - for SubresourceAccess { - access, - subresource, - } in accesses - { - let access_image_range = *subresource.as_image().unwrap(); - if !image_subresource_range_intersects(access_image_range, image_range) { - continue; - } - - image_access = match *access { - AccessType::DepthAttachmentWriteStencilReadOnly => { - if image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::STENCIL) - { - AccessType::DepthStencilAttachmentReadWrite - } else { - AccessType::DepthAttachmentWriteStencilReadOnly - } - } - AccessType::DepthStencilAttachmentRead => { - if !image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::DEPTH) - { - AccessType::StencilAttachmentWriteDepthReadOnly - } else { - AccessType::DepthStencilAttachmentReadWrite - } - } - AccessType::DepthStencilAttachmentWrite => { - AccessType::DepthStencilAttachmentWrite - } - AccessType::StencilAttachmentWriteDepthReadOnly => { - if image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::DEPTH) - { - AccessType::DepthStencilAttachmentReadWrite - } else { - AccessType::StencilAttachmentWriteDepthReadOnly - } - } - _ => continue, - }; - - *access = image_access; - - // If the resolve access is a subset of the existing access range there is no need - // to push a new access - if image_subresource_range_contains(access_image_range, image_range) { - return self; - } - } - } - - self.pass - .push_node_access(image, image_access, Subresource::Image(image_range)); - - self - } - - /// Sets a particular depth/stencil mode. - pub fn set_depth_stencil(mut self, depth_stencil: DepthStencilMode) -> Self { - let pass = self.pass.as_mut(); - let exec = pass.execs.last_mut().unwrap(); - - assert!(exec.depth_stencil.is_none()); - - exec.depth_stencil = Some(depth_stencil); - - self - } - - /// Sets multiview view and correlation masks. - /// - /// See [`VkRenderPassMultiviewCreateInfo`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkRenderPassMultiviewCreateInfo.html#_description). - pub fn set_multiview(mut self, view_mask: u32, correlated_view_mask: u32) -> Self { - let pass = self.pass.as_mut(); - let exec = pass.execs.last_mut().unwrap(); - - exec.correlated_view_mask = correlated_view_mask; - exec.view_mask = view_mask; - - self - } - - /// Sets the [`renderArea`](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkRenderPassBeginInfo.html#_c_specification) - /// field when beginning a render pass. - /// - /// NOTE: Setting this value will cause the viewport and scissor to be unset, which is not the default - /// behavior. When this value is set you should call `set_viewport` and `set_scissor` on the subpass. - /// - /// If not set, this value defaults to the first loaded, resolved, or stored attachment dimensions and - /// sets the viewport and scissor to the same values, with a `0..1` depth if not specified by - /// `set_depth_stencil`. - pub fn set_render_area(mut self, x: i32, y: i32, width: u32, height: u32) -> Self { - self.pass.as_mut().execs.last_mut().unwrap().render_area = Some(Area { - height, - width, - x, - y, - }); - - self - } - - /// Specifies `VK_ATTACHMENT_STORE_OP_STORE` for the render pass attachment, and stores the - /// rendered pixels into an image. - pub fn store_color( - self, - attachment_idx: AttachmentIndex, - image: impl Into, - ) -> Self { - let image: AnyImageNode = image.into(); - let image_info = image.get(self.pass.graph); - let image_view_info: ImageViewInfo = image_info.into(); - - self.store_color_as(attachment_idx, image, image_view_info) - } - - /// Specifies `VK_ATTACHMENT_STORE_OP_STORE` for the render pass attachment, and stores the - /// rendered pixels into an image. - pub fn store_color_as( - mut self, - attachment_idx: AttachmentIndex, - image: impl Into, - image_view_info: impl Into, - ) -> Self { - let image = image.into(); - let image_view_info = image_view_info.into(); - let node_idx = image.index(); - let (_, sample_count) = self.image_info(node_idx); - - self.pass - .as_mut() - .execs - .last_mut() - .unwrap() - .color_stores - .insert( - attachment_idx, - Attachment::new(image_view_info, sample_count, node_idx), - ); - - debug_assert!( - Attachment::are_compatible( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_attachments - .get(&attachment_idx) - .copied(), - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_stores - .get(&attachment_idx) - .copied() - ), - "color attachment {attachment_idx} store incompatible with existing attachment" - ); - debug_assert!( - Attachment::are_compatible( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_clears - .get(&attachment_idx) - .map(|(attachment, _)| *attachment), - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_stores - .get(&attachment_idx) - .copied() - ), - "color attachment {attachment_idx} store incompatible with existing clear" - ); - debug_assert!( - Attachment::are_compatible( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_loads - .get(&attachment_idx) - .copied(), - self.pass - .as_ref() - .execs - .last() - .unwrap() - .color_stores - .get(&attachment_idx) - .copied() - ), - "color attachment {attachment_idx} store incompatible with existing load" - ); - - let mut image_access = AccessType::ColorAttachmentWrite; - let image_range = image_view_info.into(); - - // Upgrade existing read access to read-write - if let Some(accesses) = self - .pass - .as_mut() - .execs - .last_mut() - .unwrap() - .accesses - .get_mut(&node_idx) - { - for SubresourceAccess { - access, - subresource, - } in accesses - { - let access_image_range = *subresource.as_image().unwrap(); - if !image_subresource_range_intersects(access_image_range, image_range) { - continue; - } - - image_access = match *access { - AccessType::ColorAttachmentRead | AccessType::ColorAttachmentReadWrite => { - AccessType::ColorAttachmentReadWrite - } - AccessType::ColorAttachmentWrite => AccessType::ColorAttachmentWrite, - _ => continue, - }; - - *access = image_access; - - // If the store access is a subset of the existing access range there is no need - // to push a new access - if image_subresource_range_contains(access_image_range, image_range) { - return self; - } - } - } - - self.pass - .push_node_access(image, image_access, Subresource::Image(image_range)); - - self - } - - /// Specifies `VK_ATTACHMENT_STORE_OP_STORE` for the render pass attachment, and stores the - /// rendered pixels into an image. - pub fn store_depth_stencil(self, image: impl Into) -> Self { - let image: AnyImageNode = image.into(); - let image_info = image.get(self.pass.graph); - let image_view_info: ImageViewInfo = image_info.into(); - - self.store_depth_stencil_as(image, image_view_info) - } - - /// Specifies `VK_ATTACHMENT_STORE_OP_STORE` for the render pass attachment, and stores the - /// rendered pixels into an image. - /// - /// _NOTE:_ Order matters, call store after clear or load. - pub fn store_depth_stencil_as( - mut self, - image: impl Into, - image_view_info: impl Into, - ) -> Self { - let image = image.into(); - let image_view_info = image_view_info.into(); - let node_idx = image.index(); - let (_, sample_count) = self.image_info(node_idx); - - self.pass - .as_mut() - .execs - .last_mut() - .unwrap() - .depth_stencil_store = Some(Attachment::new(image_view_info, sample_count, node_idx)); - - debug_assert!( - Attachment::are_compatible( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .depth_stencil_attachment, - self.pass.as_ref().execs.last().unwrap().depth_stencil_store - ), - "depth/stencil attachment store incompatible with existing attachment" - ); - debug_assert!( - Attachment::are_compatible( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .depth_stencil_clear - .map(|(attachment, _)| attachment), - self.pass.as_ref().execs.last().unwrap().depth_stencil_store - ), - "depth/stencil attachment store incompatible with existing clear" - ); - debug_assert!( - Attachment::are_compatible( - self.pass.as_ref().execs.last().unwrap().depth_stencil_load, - self.pass.as_ref().execs.last().unwrap().depth_stencil_store - ), - "depth/stencil attachment store incompatible with existing load" - ); - - let mut image_access = if image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::DEPTH | vk::ImageAspectFlags::STENCIL) - { - AccessType::DepthStencilAttachmentWrite - } else if image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::DEPTH) - { - AccessType::DepthAttachmentWriteStencilReadOnly - } else { - debug_assert!( - image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::STENCIL) - ); - - AccessType::StencilAttachmentWriteDepthReadOnly - }; - let image_range = image_view_info.into(); - - // Upgrade existing read access to read-write - if let Some(accesses) = self - .pass - .as_mut() - .execs - .last_mut() - .unwrap() - .accesses - .get_mut(&node_idx) - { - for SubresourceAccess { - access, - subresource, - } in accesses - { - let access_image_range = *subresource.as_image().unwrap(); - if !image_subresource_range_intersects(access_image_range, image_range) { - continue; - } - - image_access = match *access { - AccessType::DepthAttachmentWriteStencilReadOnly => { - if image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::STENCIL) - { - AccessType::DepthStencilAttachmentReadWrite - } else { - AccessType::DepthAttachmentWriteStencilReadOnly - } - } - AccessType::DepthStencilAttachmentRead => { - if !image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::DEPTH) - { - AccessType::StencilAttachmentWriteDepthReadOnly - } else { - AccessType::DepthStencilAttachmentReadWrite - } - } - AccessType::DepthStencilAttachmentWrite => { - AccessType::DepthStencilAttachmentWrite - } - AccessType::StencilAttachmentWriteDepthReadOnly => { - if image_view_info - .aspect_mask - .contains(vk::ImageAspectFlags::DEPTH) - { - AccessType::DepthStencilAttachmentReadWrite - } else { - AccessType::StencilAttachmentWriteDepthReadOnly - } - } - _ => continue, - }; - - *access = image_access; - - // If the store access is a subset of the existing access range there is no need - // to push a new access - if image_subresource_range_contains(access_image_range, image_range) { - return self; - } - } - } - - self.pass - .push_node_access(image, image_access, Subresource::Image(image_range)); - - self - } -} - -impl PipelinePassRef<'_, RayTracePipeline> { - /// Begin recording a ray tracing command buffer. - pub fn record_ray_trace( - mut self, - func: impl FnOnce(RayTrace<'_>, Bindings<'_>) + Send + 'static, - ) -> Self { - let pipeline = Arc::clone( - self.pass - .as_ref() - .execs - .last() - .unwrap() - .pipeline - .as_ref() - .unwrap() - .unwrap_ray_trace(), - ); - - #[cfg(debug_assertions)] - let dynamic_stack_size = pipeline.info.dynamic_stack_size; - - self.pass.push_execute(move |device, cmd_buf, bindings| { - func( - RayTrace { - cmd_buf, - device, - - #[cfg(debug_assertions)] - dynamic_stack_size, - - pipeline, - }, - bindings, - ); - }); - - self - } -} - -/// Recording interface for ray tracing commands. -/// -/// This structure provides a strongly-typed set of methods which allow ray trace shader code to be -/// executed. An instance of `RayTrace` is provided to the closure parameter of -/// [`PipelinePassRef::record_ray_trace`] which may be accessed by binding a [`RayTracePipeline`] to -/// a render pass. -/// -/// # Examples -/// -/// Basic usage: -/// -/// ```no_run -/// # use std::sync::Arc; -/// # use ash::vk; -/// # use screen_13::driver::DriverError; -/// # use screen_13::driver::device::{Device, DeviceInfo}; -/// # use screen_13::driver::ray_trace::{RayTracePipeline, RayTracePipelineInfo, RayTraceShaderGroup}; -/// # use screen_13::driver::shader::Shader; -/// # use screen_13::graph::RenderGraph; -/// # fn main() -> Result<(), DriverError> { -/// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); -/// # let info = RayTracePipelineInfo::default(); -/// # let my_miss_code = [0u8; 1]; -/// # let my_ray_trace_pipeline = Arc::new(RayTracePipeline::create(&device, info, -/// [Shader::new_miss(my_miss_code.as_slice())], -/// [RayTraceShaderGroup::new_general(0)], -/// )?); -/// # let mut my_graph = RenderGraph::new(); -/// my_graph.begin_pass("my ray trace pass") -/// .bind_pipeline(&my_ray_trace_pipeline) -/// .record_ray_trace(move |ray_trace, bindings| { -/// // During this closure we have access to the ray trace methods! -/// }); -/// # Ok(()) } -/// ``` -pub struct RayTrace<'a> { - cmd_buf: vk::CommandBuffer, - device: &'a Device, - - #[cfg(debug_assertions)] - dynamic_stack_size: bool, - - pipeline: Arc, -} - -impl RayTrace<'_> { - /// Updates push constants. - /// - /// Push constants represent a high speed path to modify constant data in pipelines that is - /// expected to outperform memory-backed resource updates. - /// - /// Push constant values can be updated incrementally, causing shader stages to read the new - /// data for push constants modified by this command, while still reading the previous data for - /// push constants not modified by this command. - /// - /// # Device limitations - /// - /// See - /// [`device.physical_device.props.limits.max_push_constants_size`](vk::PhysicalDeviceLimits) - /// for the limits of the current device. You may also check [gpuinfo.org] for a listing of - /// reported limits on other devices. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ``` - /// # inline_spirv::inline_spirv!(r#" - /// #version 460 - /// - /// layout(push_constant) uniform PushConstants { - /// layout(offset = 0) uint some_val; - /// } push_constants; - /// - /// void main() - /// { - /// // TODO: Add bindings to write things! - /// } - /// # "#, rchit, vulkan1_2); - /// ``` - /// - /// ```no_run - /// # use std::sync::Arc; - /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; - /// # use screen_13::driver::ray_trace::{RayTracePipeline, RayTracePipelineInfo, RayTraceShaderGroup}; - /// # use screen_13::driver::shader::Shader; - /// # use screen_13::graph::RenderGraph; - /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); - /// # let shader = [0u8; 1]; - /// # let info = RayTracePipelineInfo::default(); - /// # let my_miss_code = [0u8; 1]; - /// # let my_ray_trace_pipeline = Arc::new(RayTracePipeline::create(&device, info, - /// # [Shader::new_miss(my_miss_code.as_slice())], - /// # [RayTraceShaderGroup::new_general(0)], - /// # )?); - /// # let rgen_sbt = vk::StridedDeviceAddressRegionKHR { device_address: 0, stride: 0, size: 0 }; - /// # let hit_sbt = vk::StridedDeviceAddressRegionKHR { device_address: 0, stride: 0, size: 0 }; - /// # let miss_sbt = vk::StridedDeviceAddressRegionKHR { device_address: 0, stride: 0, size: 0 }; - /// # let call_sbt = vk::StridedDeviceAddressRegionKHR { device_address: 0, stride: 0, size: 0 }; - /// # let mut my_graph = RenderGraph::new(); - /// my_graph.begin_pass("draw a cornell box") - /// .bind_pipeline(&my_ray_trace_pipeline) - /// .record_ray_trace(move |ray_trace, bindings| { - /// ray_trace.push_constants(&[0xcb]) - /// .trace_rays(&rgen_sbt, &hit_sbt, &miss_sbt, &call_sbt, 320, 200, 1); - /// }); - /// # Ok(()) } - /// ``` - /// - /// [gpuinfo.org]: https://vulkan.gpuinfo.org/displaydevicelimit.php?name=maxPushConstantsSize&platform=all - pub fn push_constants(&self, data: &[u8]) -> &Self { - self.push_constants_offset(0, data) - } - - /// Updates push constants starting at the given `offset`. - /// - /// Behaves similary to [`RayTrace::push_constants`] except that `offset` describes the position - /// at which `data` updates the push constants of the currently bound pipeline. This may be used - /// to update a subset or single field of previously set push constant data. - /// - /// # Device limitations - /// - /// See - /// [`device.physical_device.props.limits.max_push_constants_size`](vk::PhysicalDeviceLimits) - /// for the limits of the current device. You may also check [gpuinfo.org] for a listing of - /// reported limits on other devices. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ``` - /// # inline_spirv::inline_spirv!(r#" - /// #version 460 - /// - /// layout(push_constant) uniform PushConstants { - /// layout(offset = 0) uint some_val1; - /// layout(offset = 4) uint some_val2; - /// } push_constants; - /// - /// void main() - /// { - /// // TODO: Add bindings to write things! - /// } - /// # "#, rchit, vulkan1_2); - /// ``` - /// - /// ```no_run - /// # use std::sync::Arc; - /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; - /// # use screen_13::driver::ray_trace::{RayTracePipeline, RayTracePipelineInfo, RayTraceShaderGroup}; - /// # use screen_13::driver::shader::Shader; - /// # use screen_13::graph::RenderGraph; - /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); - /// # let shader = [0u8; 1]; - /// # let info = RayTracePipelineInfo::default(); - /// # let my_miss_code = [0u8; 1]; - /// # let my_ray_trace_pipeline = Arc::new(RayTracePipeline::create(&device, info, - /// # [Shader::new_miss(my_miss_code.as_slice())], - /// # [RayTraceShaderGroup::new_general(0)], - /// # )?); - /// # let rgen_sbt = vk::StridedDeviceAddressRegionKHR { device_address: 0, stride: 0, size: 0 }; - /// # let hit_sbt = vk::StridedDeviceAddressRegionKHR { device_address: 0, stride: 0, size: 0 }; - /// # let miss_sbt = vk::StridedDeviceAddressRegionKHR { device_address: 0, stride: 0, size: 0 }; - /// # let call_sbt = vk::StridedDeviceAddressRegionKHR { device_address: 0, stride: 0, size: 0 }; - /// # let mut my_graph = RenderGraph::new(); - /// my_graph.begin_pass("draw a cornell box") - /// .bind_pipeline(&my_ray_trace_pipeline) - /// .record_ray_trace(move |ray_trace, bindings| { - /// ray_trace.push_constants(&[0xcb, 0xff]) - /// .trace_rays(&rgen_sbt, &hit_sbt, &miss_sbt, &call_sbt, 320, 200, 1) - /// .push_constants_offset(4, &[0xae]) - /// .trace_rays(&rgen_sbt, &hit_sbt, &miss_sbt, &call_sbt, 320, 200, 1); - /// }); - /// # Ok(()) } - /// ``` - /// - /// [gpuinfo.org]: https://vulkan.gpuinfo.org/displaydevicelimit.php?name=maxPushConstantsSize&platform=all - #[profiling::function] - pub fn push_constants_offset(&self, offset: u32, data: &[u8]) -> &Self { - for push_const in self.pipeline.push_constants.iter() { - let push_const_end = push_const.offset + push_const.size; - let data_end = offset + data.len() as u32; - let end = data_end.min(push_const_end); - let start = offset.max(push_const.offset); - - if end > start { - trace!( - " push constants {:?} {}..{}", - push_const.stage_flags, start, end - ); - - unsafe { - self.device.cmd_push_constants( - self.cmd_buf, - self.pipeline.layout, - push_const.stage_flags, - start, - &data[(start - offset) as usize..(end - offset) as usize], - ); - } - } - } - self - } - - /// Set the stack size dynamically for a ray trace pipeline. - /// - /// See - /// [`RayTracePipelineInfo::dynamic_stack_size`](crate::driver::ray_trace::RayTracePipelineInfo::dynamic_stack_size) - /// and - /// [`vkCmdSetRayTracingPipelineStackSizeKHR`](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/vkCmdSetRayTracingPipelineStackSizeKHR.html). - #[profiling::function] - pub fn set_stack_size(&self, pipeline_stack_size: u32) -> &Self { - #[cfg(debug_assertions)] - assert!(self.dynamic_stack_size); - - unsafe { - // Safely use unchecked because ray_trace_ext is checked during pipeline creation - self.device - .ray_trace_ext - .as_ref() - .unwrap_unchecked() - .cmd_set_ray_tracing_pipeline_stack_size(self.cmd_buf, pipeline_stack_size); - } - - self - } - - // TODO: If the rayTraversalPrimitiveCulling or rayQuery features are enabled, the SkipTrianglesKHR and SkipAABBsKHR ray flags can be specified when tracing a ray. SkipTrianglesKHR and SkipAABBsKHR are mutually exclusive. - - /// Ray traces using the currently-bound [`RayTracePipeline`] and the given shader binding - /// tables. - /// - /// Shader binding tables must be constructed according to this [example]. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```no_run - /// # use std::sync::Arc; - /// # use ash::vk; - /// # use screen_13::driver::DriverError; - /// # use screen_13::driver::device::{Device, DeviceInfo}; - /// # use screen_13::driver::buffer::{Buffer, BufferInfo}; - /// # use screen_13::driver::ray_trace::{RayTracePipeline, RayTracePipelineInfo, RayTraceShaderGroup}; - /// # use screen_13::driver::shader::Shader; - /// # use screen_13::graph::RenderGraph; - /// # fn main() -> Result<(), DriverError> { - /// # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); - /// # let shader = [0u8; 1]; - /// # let info = RayTracePipelineInfo::default(); - /// # let my_miss_code = [0u8; 1]; - /// # let my_ray_trace_pipeline = Arc::new(RayTracePipeline::create(&device, info, - /// # [Shader::new_miss(my_miss_code.as_slice())], - /// # [RayTraceShaderGroup::new_general(0)], - /// # )?); - /// # let rgen_sbt = vk::StridedDeviceAddressRegionKHR { device_address: 0, stride: 0, size: 0 }; - /// # let hit_sbt = vk::StridedDeviceAddressRegionKHR { device_address: 0, stride: 0, size: 0 }; - /// # let miss_sbt = vk::StridedDeviceAddressRegionKHR { device_address: 0, stride: 0, size: 0 }; - /// # let call_sbt = vk::StridedDeviceAddressRegionKHR { device_address: 0, stride: 0, size: 0 }; - /// # let mut my_graph = RenderGraph::new(); - /// my_graph.begin_pass("draw a cornell box") - /// .bind_pipeline(&my_ray_trace_pipeline) - /// .record_ray_trace(move |ray_trace, bindings| { - /// ray_trace.trace_rays(&rgen_sbt, &hit_sbt, &miss_sbt, &call_sbt, 320, 200, 1); - /// }); - /// # Ok(()) } - /// ``` - /// - /// [example]: https://github.com/attackgoat/screen-13/blob/master/examples/ray_trace.rs - #[allow(clippy::too_many_arguments)] - #[profiling::function] - pub fn trace_rays( - &self, - raygen_shader_binding_table: &vk::StridedDeviceAddressRegionKHR, - miss_shader_binding_table: &vk::StridedDeviceAddressRegionKHR, - hit_shader_binding_table: &vk::StridedDeviceAddressRegionKHR, - callable_shader_binding_table: &vk::StridedDeviceAddressRegionKHR, - width: u32, - height: u32, - depth: u32, - ) -> &Self { - unsafe { - // Safely use unchecked because ray_trace_ext is checked during pipeline creation - self.device - .ray_trace_ext - .as_ref() - .unwrap_unchecked() - .cmd_trace_rays( - self.cmd_buf, - raygen_shader_binding_table, - miss_shader_binding_table, - hit_shader_binding_table, - callable_shader_binding_table, - width, - height, - depth, - ); - } - - self - } - - /// Ray traces using the currently-bound [`RayTracePipeline`] and the given shader binding - /// tables. - /// - /// `indirect_device_address` is a [buffer device address] which is a pointer to a - /// [`vk::TraceRaysIndirectCommandKHR`] structure containing the trace ray parameters. - /// - /// See [`vkCmdTraceRaysIndirectKHR`](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/vkCmdTraceRaysIndirectKHR.html). - /// - /// [buffer device address]: Buffer::device_address - #[profiling::function] - pub fn trace_rays_indirect( - &self, - raygen_shader_binding_table: &vk::StridedDeviceAddressRegionKHR, - miss_shader_binding_table: &vk::StridedDeviceAddressRegionKHR, - hit_shader_binding_table: &vk::StridedDeviceAddressRegionKHR, - callable_shader_binding_table: &vk::StridedDeviceAddressRegionKHR, - indirect_device_address: vk::DeviceAddress, - ) -> &Self { - unsafe { - // Safely use unchecked because ray_trace_ext is checked during pipeline creation - self.device - .ray_trace_ext - .as_ref() - .unwrap_unchecked() - .cmd_trace_rays_indirect( - self.cmd_buf, - raygen_shader_binding_table, - miss_shader_binding_table, - hit_shader_binding_table, - callable_shader_binding_table, - indirect_device_address, - ) - } - - self - } -} - -/// Describes a portion of a resource which is bound. -#[derive(Clone, Copy, Debug)] -pub enum Subresource { - /// Acceleration structures are bound whole. - AccelerationStructure, - - /// Images may be partially bound. - Image(vk::ImageSubresourceRange), - - /// Buffers may be partially bound. - Buffer(BufferSubresourceRange), -} - -impl Subresource { - pub(super) fn as_image(&self) -> Option<&vk::ImageSubresourceRange> { - if let Self::Image(subresource) = self { - Some(subresource) - } else { - None - } - } -} - -impl From<()> for Subresource { - fn from(_: ()) -> Self { - Self::AccelerationStructure - } -} - -impl From for Subresource { - fn from(subresource: vk::ImageSubresourceRange) -> Self { - Self::Image(subresource) - } -} - -impl From for Subresource { - fn from(subresource: BufferSubresourceRange) -> Self { - Self::Buffer(subresource) - } -} - -#[derive(Clone, Copy, Debug)] -pub(super) struct SubresourceAccess { - pub access: AccessType, - pub subresource: Subresource, -} - -/// Allows for a resource to be reinterpreted as differently formatted data. -pub trait View: Node -where - Self::Information: Copy, - Self::Subresource: Into, -{ - /// The information about the resource interpretation. - type Information; - - /// The portion of the resource which is bound. - type Subresource; -} - -impl View for AccelerationStructureNode { - type Information = (); - type Subresource = (); -} - -impl View for AccelerationStructureLeaseNode { - type Information = (); - type Subresource = (); -} - -impl View for AnyAccelerationStructureNode { - type Information = (); - type Subresource = (); -} - -impl View for AnyBufferNode { - type Information = BufferSubresourceRange; - type Subresource = BufferSubresourceRange; -} - -impl View for AnyImageNode { - type Information = ImageViewInfo; - type Subresource = vk::ImageSubresourceRange; -} - -impl View for BufferLeaseNode { - type Information = BufferSubresourceRange; - type Subresource = BufferSubresourceRange; -} - -impl View for BufferNode { - type Information = BufferSubresourceRange; - type Subresource = BufferSubresourceRange; -} - -impl View for ImageLeaseNode { - type Information = ImageViewInfo; - type Subresource = vk::ImageSubresourceRange; -} - -impl View for ImageNode { - type Information = ImageViewInfo; - type Subresource = vk::ImageSubresourceRange; -} - -impl View for SwapchainImageNode { - type Information = ImageViewInfo; - type Subresource = vk::ImageSubresourceRange; -} - -/// Describes the interpretation of a resource. -#[derive(Debug)] -pub enum ViewType { - /// Acceleration structures are not reinterpreted. - AccelerationStructure, - - /// Images may be interpreted as differently formatted images. - Image(ImageViewInfo), - - /// Buffers may be interpreted as subregions of the same buffer. - Buffer(Range), -} - -impl ViewType { - pub(super) fn as_buffer(&self) -> Option<&Range> { - match self { - Self::Buffer(view_info) => Some(view_info), - _ => None, - } - } - - pub(super) fn as_image(&self) -> Option<&ImageViewInfo> { - match self { - Self::Image(view_info) => Some(view_info), - _ => None, - } - } -} - -impl From<()> for ViewType { - fn from(_: ()) -> Self { - Self::AccelerationStructure - } -} - -impl From for ViewType { - fn from(subresource: BufferSubresourceRange) -> Self { - Self::Buffer(subresource.start..subresource.end) - } -} - -impl From for ViewType { - fn from(info: ImageViewInfo) -> Self { - Self::Image(info) - } -} - -impl From> for ViewType { - fn from(range: Range) -> Self { - Self::Buffer(range) - } -} diff --git a/src/graph/swapchain.rs b/src/graph/swapchain.rs deleted file mode 100644 index fa8a9e25..00000000 --- a/src/graph/swapchain.rs +++ /dev/null @@ -1,33 +0,0 @@ -use { - super::{Bind, Binding, RenderGraph, SwapchainImageNode}, - crate::driver::swapchain::SwapchainImage, -}; - -impl Bind<&mut RenderGraph, SwapchainImageNode> for SwapchainImage { - fn bind(self, graph: &mut RenderGraph) -> SwapchainImageNode { - // We will return a new node - let res = SwapchainImageNode::new(graph.bindings.len()); - - //trace!("Node {}: {:?}", res.idx, &self); - - let binding = Binding::SwapchainImage(Box::new(self), true); - graph.bindings.push(binding); - - res - } -} - -impl Binding { - pub(super) fn as_swapchain_image(&self) -> Option<&SwapchainImage> { - if let Self::SwapchainImage(binding, true) = self { - Some(binding) - } else if let Self::SwapchainImage(_, false) = self { - // User code might try this - but it is a programmer error - // to access a binding after it has been unbound so dont - None - } else { - // The private code in this module should prevent this branch - unreachable!(); - } - } -} diff --git a/src/lib.rs b/src/lib.rs index 48531c5a..04d6fac0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,392 +1,1655 @@ /*! -Provides a high performance [Vulkan 1.2](https://www.vulkan.org/) driver using smart pointers. -Supports windowed and headless rendering with a fully-featured render graph and resource pooling -types. +This crate provides a high-performance [Vulkan](https://www.vulkan.org/) driver featuring automated +resource management and execution. -This crate allows graphics programmers to focus on acceleration structure, buffer, and image -resources and the shader pipelines they are accessed from. There are no restrictions or opinions -placed on the types of graphics algorithms you might create. Some implementations of common graphics -patterns are provided in the `contrib` directory. +For an overview, including installation and typical usage, see the +[Guide Book](https://attackgoat.github.io/vk-graph). -# Getting Sarted -Typical usage involves creating an operating system window event loop, and rendering frames -using the provided [`FrameContext`] closure. The [`EventLoop`] builder handles creating an instance -of the [`Device`] driver, however you may construct one manually for headless rendering. -```no_run -use screen_13_window::{Window, WindowError}; -fn main() -> Result<(), WindowError> { - let window = Window::new()?; +*/ + +#![deny(missing_docs)] +#![deny(rustdoc::broken_intra_doc_links)] + +pub mod cmd; +pub mod driver; +pub mod node; +pub mod pool; + +mod submission; + +use std::sync::Arc; + +use crate::{ + cmd::{ClearColorValue, CommandRef}, + driver::{ + accel_struct::AccelerationStructure, buffer::Buffer, image::Image, + swapchain::SwapchainImage, + }, + pool::Lease, +}; + +pub use self::submission::Submission; - // Use the device to create resources and pipelines before running - let device = &window.device; +#[allow(deprecated)] +pub use self::deprecated::{Display, DisplayInfo, DisplayInfoBuilder}; - window.run(|frame| { - // You may also create resources and pipelines while running - let device = &frame.device; - }) +use { + self::{ + cmd::{AttachmentIndex, Binding, Command, SubresourceAccess, ViewInfo}, + node::{ + AccelerationStructureLeaseNode, AccelerationStructureNode, + AnyAccelerationStructureNode, AnyBufferNode, AnyImageNode, BufferLeaseNode, BufferNode, + ImageLeaseNode, ImageNode, SwapchainImageNode, + }, + }, + crate::driver::{ + DescriptorBindingMap, + compute::ComputePipeline, + format_aspect_mask, format_texel_block_extent, format_texel_block_size, + graphic::{DepthStencilInfo, GraphicPipeline}, + image::{ImageInfo, ImageViewInfo, SampleCount}, + image_subresource_range_from_layers, + ray_trace::RayTracePipeline, + render_pass::ResolveMode, + shader::PipelineDescriptorInfo, + }, + ash::vk, + std::{ + cmp::Ord, + collections::{BTreeMap, HashMap}, + fmt::{Debug, Formatter}, + ops::Range, + }, + vk_sync::AccessType, +}; + +type ExecFn = Box; +type NodeIndex = usize; + +#[derive(Debug)] +#[doc(hidden)] +pub enum AnyResource { + AccelerationStructure(Arc), + AccelerationStructureLease(Arc>), + Buffer(Arc), + BufferLease(Arc>), + Image(Arc), + ImageLease(Arc>), + SwapchainImage(Box), } -``` - -# Resources and Pipelines - -All resources and pipelines, as well as the driver itself, use shared reference tracking to keep -pointers alive. _Screen 13_ uses `std::sync::Arc` to track references. - -## Information - -All [`driver`] types have associated information structures which describe their properties. -Each object provides a `create` function which uses the information to return an instance. - -| Resource | Create Using | -|-------------------------------|-----------------------------------------------------| -| [`AccelerationStructureInfo`] | [`AccelerationStructure::create`] | -| [`BufferInfo`] | [`Buffer::create`] or [`Buffer::create_from_slice`] | -| [`ImageInfo`] | [`Image::create`] | - -For example, a typical host-mappable buffer: - -```no_run -# use std::sync::Arc; -# use ash::vk; -# use screen_13::driver::DriverError; -# use screen_13::driver::device::{Device, DeviceInfo}; -# use screen_13::driver::buffer::{Buffer, BufferInfo}; -# fn main() -> Result<(), DriverError> { -# let device = Arc::new(Device::create_headless(DeviceInfo::default())?); -let info = BufferInfo::host_mem(1024, vk::BufferUsageFlags::STORAGE_BUFFER); -let my_buf = Buffer::create(&device, info)?; -# Ok(()) } -``` - -| Pipeline | Create Using | -|-------------------------------|-----------------------------------------------------| -| [`ComputePipelineInfo`] | [`ComputePipeline::create`] | -| [`GraphicPipelineInfo`] | [`GraphicPipeline::create`] | -| [`RayTracePipelineInfo`] | [`RayTracePipeline::create`] | - -For example, a graphics pipeline: - -```no_run -# use std::sync::Arc; -# use ash::vk; -# use screen_13::driver::DriverError; -# use screen_13::driver::device::{Device, DeviceInfo}; -# use screen_13::driver::graphic::{GraphicPipeline, GraphicPipelineInfo}; -# use screen_13::driver::shader::Shader; -# fn main() -> Result<(), DriverError> { -# let device = Arc::new(Device::create_headless(DeviceInfo::default())?); -# let my_frag_code = [0u8; 1]; -# let my_vert_code = [0u8; 1]; -// shader code is raw SPIR-V code as bytes -let vert = Shader::new_vertex(my_vert_code.as_slice()); -let frag = Shader::new_fragment(my_frag_code.as_slice()); -let info = GraphicPipelineInfo::default(); -let my_pipeline = GraphicPipeline::create(&device, info, [vert, frag])?; -# Ok(()) } -``` - -## Pooling - -Multiple [`pool`] types are available to reduce the impact of frequently creating and dropping -resources. Leased resources behave identically to owned resources and can be used in a render graph. - -Resource aliasing is also availble as an optional way to reduce the number of concurrent resources -that may be required. - -For example, leasing an image: - -```no_run -# use std::sync::Arc; -# use ash::vk; -# use screen_13::driver::DriverError; -# use screen_13::driver::device::{Device, DeviceInfo}; -# use screen_13::driver::image::{ImageInfo}; -# use screen_13::pool::{Pool}; -# use screen_13::pool::lazy::{LazyPool}; -# fn main() -> Result<(), DriverError> { -# let device = Arc::new(Device::create_headless(DeviceInfo::default())?); -let mut pool = LazyPool::new(&device); - -let info = ImageInfo::image_2d(8, 8, vk::Format::R8G8B8A8_UNORM, vk::ImageUsageFlags::STORAGE); -let my_image = pool.lease(info)?; -# Ok(()) } -``` - -# Render Graph Operations - -All rendering in _Screen 13_ is performed using a [`RenderGraph`] composed of user-specified passes, -which may include pipelines and read/write access to resources. Recorded passes are automatically -optimized before submission to the graphics hardware. - -Some notes about the awesome render pass optimization which was _totally stolen_ from [Granite]: - -- Scheduling: passes are submitted to the Vulkan API using batches designed for low-latency -- Re-ordering: passes are shuffled using a heuristic which gives the GPU more time to complete work -- Merging: compatible passes are merged into dynamic subpasses when it is more efficient (_on-tile - rendering_) -- Aliasing: resources and pipelines are optimized to emit minimal barriers per unit of work (_max - one, typically zero_) - -## Nodes - -Resources may be directly bound to a render graph. During the time a resource is bound we refer to -it as a node. Bound nodes may only be used with the graphs they were bound to. Nodes implement -`Copy` to make using them easier. - -```no_run -# use std::sync::Arc; -# use ash::vk; -# use screen_13::driver::DriverError; -# use screen_13::driver::device::{Device, DeviceInfo}; -# use screen_13::driver::buffer::{Buffer, BufferInfo}; -# use screen_13::driver::image::{Image, ImageInfo}; -# use screen_13::graph::RenderGraph; -# use screen_13::pool::{Pool}; -# use screen_13::pool::lazy::{LazyPool}; -# fn main() -> Result<(), DriverError> { -# let device = Arc::new(Device::create_headless(DeviceInfo::default())?); -# let info = BufferInfo::host_mem(1024, vk::BufferUsageFlags::STORAGE_BUFFER); -# let buffer = Buffer::create(&device, info)?; -# let info = ImageInfo::image_2d(8, 8, vk::Format::R8G8B8A8_UNORM, vk::ImageUsageFlags::STORAGE); -# let image = Image::create(&device, info)?; -# let mut graph = RenderGraph::new(); -println!("{:?}", buffer); // Buffer -println!("{:?}", image); // Image - -// Bind our resources into opaque "usize" nodes -let buffer = graph.bind_node(buffer); -let image = graph.bind_node(image); - -// The results have unique types! -println!("{:?}", buffer); // BufferNode -println!("{:?}", image); // ImageNode - -// Unbind nodes back into resources (Optional!) -let buffer = graph.unbind_node(buffer); -let image = graph.unbind_node(image); - -// Magically, they return to the correct types! (the graph wrapped them in Arc for us) -println!("{:?}", buffer); // Arc -println!("{:?}", image); // Arc -# Ok(()) } -``` - -_Note:_ See [this code](https://github.com/attackgoat/screen-13/blob/master/src/graph/edge.rs#L34) -for all the things that can be bound or unbound from a graph. - -_Note:_ Once unbound, the node is invalid and should be dropped. - -## Access and synchronization - -Render graphs and their passes contain a set of functions used to handle Vulkan synchronization with -prefixes of `access`, `read`, or `write`. For each resource used in a computing, graphics subpass, -ray tracing, or general command buffer you must call an access function. Generally choose a `read` -or `write` function unless you want to be most efficient. - -Example: - -```no_run -# use std::sync::Arc; -# use ash::vk; -# use screen_13::driver::DriverError; -# use screen_13::driver::device::{Device, DeviceInfo}; -# use screen_13::driver::buffer::{Buffer, BufferInfo}; -# use screen_13::driver::image::{Image, ImageInfo}; -# use screen_13::graph::RenderGraph; -# use screen_13::pool::{Pool}; -# use screen_13::pool::lazy::{LazyPool}; -# fn main() -> Result<(), DriverError> { -# let device = Arc::new(Device::create_headless(DeviceInfo::default())?); -# let info = BufferInfo::host_mem(1024, vk::BufferUsageFlags::STORAGE_BUFFER); -# let buffer = Buffer::create(&device, info)?; -# let info = ImageInfo::image_2d(8, 8, vk::Format::R8G8B8A8_UNORM, vk::ImageUsageFlags::STORAGE); -# let image = Image::create(&device, info)?; -let mut graph = RenderGraph::new(); -let buffer_node = graph.bind_node(buffer); -let image_node = graph.bind_node(image); -graph - .begin_pass("Do some raw Vulkan or interop with another Vulkan library") - .record_cmd_buf(move |device, cmd_buf, bindings| unsafe { - // I always run first! - }) - .read_node(buffer_node) // <-- These two functions, read_node/write_node, completely - .write_node(image_node) // handle vulkan synchronization. - .record_cmd_buf(move |device, cmd_buf, bindings| unsafe { - // device is &ash::Device - // cmd_buf is vk::CommandBuffer - // bindings is a magical object you can retrieve the Vulkan resource from - let vk_buffer: vk::Buffer = *bindings[buffer_node]; - let vk_image: vk::Image = *bindings[image_node]; - - // You are free to READ vk_buffer and WRITE vk_image! - }); -# Ok(()) } -``` - -## Shader pipelines - -Pipeline instances may be bound to a [`PassRef`] in order to execute the associated shader code: - -```no_run -# use std::sync::Arc; -# use ash::vk; -# use screen_13::driver::DriverError; -# use screen_13::driver::device::{Device, DeviceInfo}; -# use screen_13::driver::compute::{ComputePipeline, ComputePipelineInfo}; -# use screen_13::driver::shader::{Shader}; -# use screen_13::graph::RenderGraph; -# fn main() -> Result<(), DriverError> { -# let device = Arc::new(Device::create_headless(DeviceInfo::default())?); -# let my_shader_code = [0u8; 1]; -# let info = ComputePipelineInfo::default(); -# let shader = Shader::new_compute(my_shader_code.as_slice()); -# let my_compute_pipeline = Arc::new(ComputePipeline::create(&device, info, shader)?); -# let mut graph = RenderGraph::new(); -graph - .begin_pass("My compute pass") - .bind_pipeline(&my_compute_pipeline) - .record_compute(|compute, _| { - compute.push_constants(&42u32.to_ne_bytes()) - .dispatch(128, 1, 1); - }); -# Ok(()) } -``` - -## Image samplers - -By default, _Screen 13_ will use "linear repeat-mode" samplers unless a special suffix appears as -part of the name within GLSL or HLSL shader code. The `_sampler_123` suffix should be used where -`1`, `2`, and `3` are replaced with: - -1. `l` for `LINEAR` texel filtering (default) or `n` for `NEAREST` -1. `l` (default) or `n`, as above, but for mipmap filtering -1. Addressing mode where: - - `b` is `CLAMP_TO_BORDER` - - `e` is `CLAMP_TO_EDGE` - - `m` is `MIRRORED_REPEAT` - - `r` is `REPEAT` - -For example, the following sampler named `pages_sampler_nnr` specifies nearest texel/mipmap modes and repeat addressing: - -```glsl -layout(set = 0, binding = 0) uniform sampler2D pages_sampler_nnr[NUM_PAGES]; -``` - -For more complex image sampling, use [`ShaderBuilder::image_sampler`] to specify the exact image -sampling mode. - -## Vertex input - -Optional name suffixes are used in the same way with vertex input as with image samplers. The -additional attribution of your shader code is optional but may help in a few scenarios: - -- Per-instance vertex rate data -- Multiple vertex buffer binding indexes - -The data for vertex input is assumed to be per-vertex and bound to vertex buffer binding index zero. -Add `_ibindX` for per-instance data, or the matching `_vbindX` for per-vertex data where `X` is -replaced with the vertex buffer binding index in each case. - -For more complex vertex layouts, use the [`ShaderBuilder::vertex_input`] to specify the exact -layout. - -[`AccelerationStructureInfo`]: driver::accel_struct::AccelerationStructureInfo -[`AccelerationStructure::create`]: driver::accel_struct::AccelerationStructure::create -[`Buffer::create`]: driver::buffer::Buffer::create -[`Buffer::create_from_slice`]: driver::buffer::Buffer::create_from_slice -[`BufferInfo`]: driver::buffer::BufferInfo -[`ComputePipeline::create`]: driver::compute::ComputePipeline::create -[`ComputePipelineInfo`]: driver::compute::ComputePipelineInfo -[`Device`]: driver::device::Device -[`EventLoop`]: EventLoop -[`FrameContext`]: FrameContext -[Granite]: https://github.com/Themaister/Granite -[`GraphicPipeline::create`]: driver::graphic::GraphicPipeline::create -[`GraphicPipelineInfo`]: driver::graphic::GraphicPipelineInfo -[`Image::create`]: driver::image::Image::create -[`ImageInfo`]: driver::image::ImageInfo -[`PassRef`]: graph::pass_ref::PassRef -[`RayTracePipeline::create`]: driver::ray_trace::RayTracePipeline::create -[`RayTracePipelineInfo`]: driver::ray_trace::RayTracePipelineInfo -[`RenderGraph`]: graph::RenderGraph -[`ShaderBuilder::image_sampler`]: driver::shader::ShaderBuilder::image_sampler -[`ShaderBuilder::vertex_input`]: driver::shader::ShaderBuilder::vertex_input -*/ +impl AnyResource { + fn as_accel_struct(&self) -> Option<&AccelerationStructure> { + Some(match self { + Self::AccelerationStructure(resource) => resource, + Self::AccelerationStructureLease(resource) => resource, + _ => return None, + }) + } -#![warn(missing_docs)] + fn as_buffer(&self) -> Option<&Buffer> { + Some(match self { + Self::Buffer(resource) => resource, + Self::BufferLease(resource) => resource, + _ => return None, + }) + } -pub mod driver; -pub mod graph; -pub mod pool; + fn as_image(&self) -> Option<&Image> { + Some(match self { + Self::Image(resource) => resource, + Self::ImageLease(resource) => resource, + Self::SwapchainImage(resource) => resource, + _ => return None, + }) + } -mod display; - -/// Things which are used in almost every single _Screen 13_ program. -pub mod prelude { - pub use super::{ - display::{Display, DisplayError, DisplayInfo, DisplayInfoBuilder, ResolverPool}, - driver::{ - AccessType, CommandBuffer, DriverError, Instance, - accel_struct::{ - AccelerationStructure, AccelerationStructureGeometry, - AccelerationStructureGeometryData, AccelerationStructureGeometryInfo, - AccelerationStructureInfo, AccelerationStructureInfoBuilder, - AccelerationStructureSize, DeviceOrHostAddress, - }, - ash::vk, - buffer::{Buffer, BufferInfo, BufferInfoBuilder, BufferSubresourceRange}, - compute::{ComputePipeline, ComputePipelineInfo, ComputePipelineInfoBuilder}, - device::{Device, DeviceInfo, DeviceInfoBuilder}, - graphic::{ - BlendMode, BlendModeBuilder, DepthStencilMode, DepthStencilModeBuilder, - GraphicPipeline, GraphicPipelineInfo, GraphicPipelineInfoBuilder, StencilMode, - }, - image::{ - Image, ImageInfo, ImageInfoBuilder, ImageViewInfo, ImageViewInfoBuilder, - SampleCount, - }, - physical_device::{ - AccelerationStructureProperties, PhysicalDevice, RayQueryFeatures, - RayTraceFeatures, RayTraceProperties, Vulkan10Features, Vulkan10Limits, - Vulkan10Properties, Vulkan11Features, Vulkan11Properties, Vulkan12Features, - Vulkan12Properties, - }, - ray_trace::{ - RayTracePipeline, RayTracePipelineInfo, RayTracePipelineInfoBuilder, - RayTraceShaderGroup, RayTraceShaderGroupType, - }, - render_pass::ResolveMode, - shader::{ - SamplerInfo, SamplerInfoBuilder, Shader, ShaderBuilder, ShaderCode, - SpecializationInfo, - }, - surface::Surface, - swapchain::{ - Swapchain, SwapchainError, SwapchainImage, SwapchainInfo, SwapchainInfoBuilder, + fn as_swapchain_image(&self) -> Option<&SwapchainImage> { + Some(match self { + Self::SwapchainImage(resource) => resource, + _ => return None, + }) + } + + fn expect_accel_struct(&self) -> &AccelerationStructure { + self.as_accel_struct() + .expect("missing acceleration structure resource") + } + + fn expect_buffer(&self) -> &Buffer { + self.as_buffer().expect("missing buffer resource") + } + + fn expect_image(&self) -> &Image { + self.as_image().expect("missing image resource") + } +} + +#[derive(Clone, Copy, Debug)] +struct Attachment { + array_layer_count: u32, + aspect_mask: vk::ImageAspectFlags, + base_array_layer: u32, + base_mip_level: u32, + format: vk::Format, + mip_level_count: u32, + sample_count: SampleCount, + target: NodeIndex, +} + +impl Attachment { + fn new(image_view_info: ImageViewInfo, sample_count: SampleCount, target: NodeIndex) -> Self { + Self { + array_layer_count: image_view_info.array_layer_count, + aspect_mask: image_view_info.aspect_mask, + base_array_layer: image_view_info.base_array_layer, + base_mip_level: image_view_info.base_mip_level, + format: image_view_info.fmt, + mip_level_count: image_view_info.mip_level_count, + sample_count, + target, + } + } + + fn are_compatible(lhs: Option, rhs: Option) -> bool { + // Two attachment references are compatible if they have matching format and sample + // count, or are both VK_ATTACHMENT_UNUSED or the pointer that would contain the + // reference is NULL. + let (Some(lhs), Some(rhs)) = (lhs, rhs) else { + return true; + }; + + Self::are_identical(lhs, rhs) + } + + fn are_identical(lhs: Self, rhs: Self) -> bool { + lhs.array_layer_count == rhs.array_layer_count + && lhs.base_array_layer == rhs.base_array_layer + && lhs.base_mip_level == rhs.base_mip_level + && lhs.format == rhs.format + && lhs.mip_level_count == rhs.mip_level_count + && lhs.sample_count == rhs.sample_count + && lhs.target == rhs.target + } + + fn image_view_info(self, image_info: ImageInfo) -> ImageViewInfo { + image_info + .into_builder() + .array_layer_count(self.array_layer_count) + .mip_level_count(self.mip_level_count) + .fmt(self.format) + .into_image_view() + .aspect_mask(self.aspect_mask) + .base_array_layer(self.base_array_layer) + .base_mip_level(self.base_mip_level) + .build() + } +} + +#[derive(Default)] +struct Execution { + accesses: HashMap>, + bindings: BTreeMap, + + correlated_view_mask: u32, + depth_stencil: Option, + render_area: Option, + view_mask: u32, + + color_attachments: HashMap, + color_clears: HashMap, + color_loads: HashMap, + color_resolves: HashMap, + color_stores: HashMap, + depth_stencil_attachment: Option, + depth_stencil_clear: Option<(Attachment, vk::ClearDepthStencilValue)>, + depth_stencil_load: Option, + depth_stencil_resolve: Option<( + Attachment, + AttachmentIndex, + Option, + Option, + )>, + depth_stencil_store: Option, + + func: Option, + pipeline: Option, +} + +impl Debug for Execution { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + // The only field missing is func which cannot easily be implemented because it is a + // FnOnce. + f.debug_struct("Execution") + .field("accesses", &self.accesses) + .field("bindings", &self.bindings) + .field("depth_stencil", &self.depth_stencil) + .field("color_attachments", &self.color_attachments) + .field("color_clears", &self.color_clears) + .field("color_loads", &self.color_loads) + .field("color_resolves", &self.color_resolves) + .field("color_stores", &self.color_stores) + .field("depth_stencil_attachment", &self.depth_stencil_attachment) + .field("depth_stencil_clear", &self.depth_stencil_clear) + .field("depth_stencil_load", &self.depth_stencil_load) + .field("depth_stencil_resolve", &self.depth_stencil_resolve) + .field("depth_stencil_store", &self.depth_stencil_store) + .field("pipeline", &self.pipeline) + .finish() + } +} + +struct ExecutionFunction(ExecFn); + +#[derive(Clone, Debug)] +enum ExecutionPipeline { + Compute(ComputePipeline), + Graphic(GraphicPipeline), + RayTrace(RayTracePipeline), +} + +impl ExecutionPipeline { + fn as_graphic(&self) -> Option<&GraphicPipeline> { + if let Self::Graphic(pipeline) = self { + Some(pipeline) + } else { + None + } + } + + fn bind_point(&self) -> vk::PipelineBindPoint { + match self { + ExecutionPipeline::Compute(_) => vk::PipelineBindPoint::COMPUTE, + ExecutionPipeline::Graphic(_) => vk::PipelineBindPoint::GRAPHICS, + ExecutionPipeline::RayTrace(_) => vk::PipelineBindPoint::RAY_TRACING_KHR, + } + } + + fn descriptor_bindings(&self) -> &DescriptorBindingMap { + match self { + ExecutionPipeline::Compute(pipeline) => &pipeline.inner.descriptor_bindings, + ExecutionPipeline::Graphic(pipeline) => &pipeline.inner.descriptor_bindings, + ExecutionPipeline::RayTrace(pipeline) => &pipeline.inner.descriptor_bindings, + } + } + + fn descriptor_info(&self) -> &PipelineDescriptorInfo { + match self { + ExecutionPipeline::Compute(pipeline) => &pipeline.inner.descriptor_info, + ExecutionPipeline::Graphic(pipeline) => &pipeline.inner.descriptor_info, + ExecutionPipeline::RayTrace(pipeline) => &pipeline.inner.descriptor_info, + } + } + + fn expect_compute(&self) -> &ComputePipeline { + if let Self::Compute(pipeline) = self { + pipeline + } else { + panic!("missing compute pipeline") + } + } + + fn expect_graphic(&self) -> &GraphicPipeline { + self.as_graphic().expect("missing graphic pipeline") + } + + fn expect_ray_trace(&self) -> &RayTracePipeline { + if let Self::RayTrace(pipeline) = self { + pipeline + } else { + panic!("missing ray trace pipeline") + } + } + + fn layout(&self) -> vk::PipelineLayout { + match self { + ExecutionPipeline::Compute(pipeline) => pipeline.inner.layout, + ExecutionPipeline::Graphic(pipeline) => pipeline.inner.layout, + ExecutionPipeline::RayTrace(pipeline) => pipeline.inner.layout, + } + } + + fn stage(&self) -> vk::PipelineStageFlags { + match self { + ExecutionPipeline::Compute(_) => vk::PipelineStageFlags::COMPUTE_SHADER, + ExecutionPipeline::Graphic(_) => vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, + ExecutionPipeline::RayTrace(_) => vk::PipelineStageFlags::RAY_TRACING_SHADER_KHR, + } + } +} + +#[derive(Debug)] +struct CommandData { + execs: Vec, + name: Option, +} + +impl CommandData { + fn descriptor_pools_sizes( + &self, + ) -> impl Iterator> { + self.execs + .iter() + .flat_map(|exec| &exec.pipeline) + .map(|pipeline| { + pipeline + .descriptor_info() + .pool_sizes + .values() + .flat_map(HashMap::iter) + }) + } + + fn expect_first_exec(&self) -> &Execution { + self.execs.first().expect("missing command execution") + } + + /// # Panics + /// + /// Panics if the execution list is empty (a command always has at least one execution). + fn expect_last_exec(&self) -> &Execution { + self.execs.last().expect("missing command execution") + } + + /// # Panics + /// + /// Panics if the execution list is empty (a command always has at least one execution). + fn expect_last_exec_mut(&mut self) -> &mut Execution { + self.execs.last_mut().expect("missing command execution") + } + + fn expect_last_pipeline(&self) -> &ExecutionPipeline { + self.expect_last_exec() + .pipeline + .as_ref() + .expect("missing command pipeline") + } + + fn name(&self) -> &str { + self.name.as_deref().unwrap_or("command") + } +} + +/// A composable graph of Vulkan command buffer operations. +/// +/// `Graph` instances are are intended for one-time use. +/// +/// The design of this code originated with a combination of +/// [`PassBuilder`](https://github.com/EmbarkStudios/kajiya/blob/main/crates/lib/kajiya-rg/src/pass_builder.rs) +/// and +/// [`graph.cpp`](https://github.com/Themaister/Granite/blob/master/renderer/graph.cpp). +#[derive(Debug, Default)] +pub struct Graph { + cmds: Vec, + resources: Vec, +} + +impl Graph { + /// Constructs a default `Graph`. + pub fn new() -> Self { + Self::default() + } + + /// Allocates and begins writing a new command. + pub fn begin_cmd(&mut self) -> Command<'_> { + Command::new(self) + } + + /// Binds a Vulkan buffer, image, or acceleration structure resource to this graph. + /// + /// Bound resource nodes may be used in commands for shader pipeline operations and other + /// general functions. + pub fn bind_resource(&mut self, resource: R) -> R::Node + where + R: Resource, + { + resource.bind_graph(self) + } + + /// Copy an image, potentially performing format conversion. + pub fn blit_image( + &mut self, + src: impl Into, + dst: impl Into, + filter: vk::Filter, + ) -> &mut Self { + let src = src.into(); + let src_info = self.resource(src).info; + + let dst = dst.into(); + let dst_info = self.resource(dst).info; + + self.blit_image_region( + src, + dst, + filter, + [vk::ImageBlit { + src_subresource: vk::ImageSubresourceLayers { + aspect_mask: format_aspect_mask(src_info.fmt), + mip_level: 0, + base_array_layer: 0, + layer_count: 1, + }, + src_offsets: [ + vk::Offset3D { x: 0, y: 0, z: 0 }, + vk::Offset3D { + x: src_info.width as _, + y: src_info.height as _, + z: src_info.depth as _, + }, + ], + dst_subresource: vk::ImageSubresourceLayers { + aspect_mask: format_aspect_mask(dst_info.fmt), + mip_level: 0, + base_array_layer: 0, + layer_count: 1, + }, + dst_offsets: [ + vk::Offset3D { x: 0, y: 0, z: 0 }, + vk::Offset3D { + x: dst_info.width as _, + y: dst_info.height as _, + z: dst_info.depth as _, + }, + ], + }], + ) + } + + /// Copy regions of an image, potentially performing format conversion. + #[profiling::function] + pub fn blit_image_region( + &mut self, + src: impl Into, + dst: impl Into, + filter: vk::Filter, + regions: impl AsRef<[vk::ImageBlit]> + 'static + Send, + ) -> &mut Self { + let src = src.into(); + let dst = dst.into(); + + let mut cmd = self.begin_cmd().debug_name("blit image"); + + for region in regions.as_ref() { + let src_region = image_subresource_range_from_layers(region.src_subresource); + cmd.set_subresource_access(src, src_region, AccessType::TransferRead); + + let dst_region = image_subresource_range_from_layers(region.dst_subresource); + cmd.set_subresource_access(dst, dst_region, AccessType::TransferWrite); + } + + cmd.record_cmd(move |cmd| { + let src_image = cmd.resource(src).handle; + let dst_image = cmd.resource(dst).handle; + + unsafe { + cmd.device.cmd_blit_image( + cmd.handle, + src_image, + vk::ImageLayout::TRANSFER_SRC_OPTIMAL, + dst_image, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + regions.as_ref(), + filter, + ); + } + }) + .end_cmd() + } + + /// Clear a color image. + #[profiling::function] + pub fn clear_color_image( + &mut self, + image: impl Into, + color: impl Into, + ) -> &mut Self { + let color = color.into().into(); + let image = image.into(); + let image_view = self.resource(image).info.into(); + + self.begin_cmd() + .debug_name("clear color") + .subresource_access(image, image_view, AccessType::TransferWrite) + .record_cmd(move |cmd| { + let image = cmd.resource(image); + + unsafe { + cmd.device.cmd_clear_color_image( + cmd.handle, + image.handle, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + &color, + &[image_view], + ); + } + }) + .end_cmd() + } + + /// Clears a depth/stencil image. + #[profiling::function] + pub fn clear_depth_stencil_image( + &mut self, + image: impl Into, + depth: f32, + stencil: u32, + ) -> &mut Self { + let image = image.into(); + let image_view = self.resource(image).info.into(); + + self.begin_cmd() + .debug_name("clear depth/stencil") + .subresource_access(image, image_view, AccessType::TransferWrite) + .record_cmd(move |cmd| { + let image = cmd.resource(image); + + unsafe { + cmd.device.cmd_clear_depth_stencil_image( + cmd.handle, + image.handle, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + &vk::ClearDepthStencilValue { depth, stencil }, + &[image_view], + ); + } + }) + .end_cmd() + } + + /// Copy data between buffers + pub fn copy_buffer( + &mut self, + src: impl Into, + dst: impl Into, + ) -> &mut Self { + let src = src.into(); + let dst = dst.into(); + let src_info = self.resource(src).info; + let dst_info = self.resource(dst).info; + + self.copy_buffer_region( + src, + dst, + [vk::BufferCopy { + src_offset: 0, + dst_offset: 0, + size: src_info.size.min(dst_info.size), + }], + ) + } + + /// Copy data between buffer regions. + #[profiling::function] + pub fn copy_buffer_region( + &mut self, + src: impl Into, + dst: impl Into, + regions: impl AsRef<[vk::BufferCopy]> + 'static + Send, + ) -> &mut Self { + let src = src.into(); + let dst = dst.into(); + + #[cfg(debug_assertions)] + let src_size = self.resource(src).info.size; + + #[cfg(debug_assertions)] + let dst_size = self.resource(dst).info.size; + + let mut cmd = self.begin_cmd().debug_name("copy buffer"); + + for region in regions.as_ref() { + #[cfg(debug_assertions)] + { + assert!( + region.src_offset + region.size <= src_size, + "source range end ({}) exceeds source size ({src_size})", + region.src_offset + region.size + ); + assert!( + region.dst_offset + region.size <= dst_size, + "destination range end ({}) exceeds destination size ({dst_size})", + region.dst_offset + region.size + ); + }; + + cmd.set_subresource_access( + src, + region.src_offset..region.src_offset + region.size, + AccessType::TransferRead, + ); + cmd.set_subresource_access( + dst, + region.dst_offset..region.dst_offset + region.size, + AccessType::TransferWrite, + ); + } + + cmd.record_cmd(move |cmd| { + let src = cmd.resource(src); + let dst = cmd.resource(dst); + + unsafe { + cmd.device + .cmd_copy_buffer(cmd.handle, src.handle, dst.handle, regions.as_ref()); + } + }) + .end_cmd() + } + + /// Copy data from a buffer into an image. + pub fn copy_buffer_to_image( + &mut self, + src: impl Into, + dst: impl Into, + ) -> &mut Self { + let dst = dst.into(); + let dst_info = self.resource(dst).info; + + self.copy_buffer_to_image_region( + src, + dst, + [vk::BufferImageCopy { + buffer_offset: 0, + buffer_row_length: dst_info.width, + buffer_image_height: dst_info.height, + image_subresource: vk::ImageSubresourceLayers { + aspect_mask: format_aspect_mask(dst_info.fmt), + mip_level: 0, + base_array_layer: 0, + layer_count: 1, + }, + image_offset: Default::default(), + image_extent: vk::Extent3D { + depth: dst_info.depth, + height: dst_info.height, + width: dst_info.width, + }, + }], + ) + } + + /// Copy data from a buffer into an image. + #[profiling::function] + pub fn copy_buffer_to_image_region( + &mut self, + src: impl Into, + dst: impl Into, + regions: impl AsRef<[vk::BufferImageCopy]> + 'static + Send, + ) -> &mut Self { + let src = src.into(); + let dst = dst.into(); + let dst_info = self.resource(dst).info; + + let mut cmd = self.begin_cmd().debug_name("copy buffer to image"); + + for region in regions.as_ref() { + let block_bytes_size = format_texel_block_size(dst_info.fmt); + let (block_height, block_width) = format_texel_block_extent(dst_info.fmt); + let data_size = block_bytes_size + * (region.buffer_row_length / block_width) + * (region.buffer_image_height / block_height); + + cmd.set_subresource_access( + src, + region.buffer_offset..region.buffer_offset + data_size as vk::DeviceSize, + AccessType::TransferRead, + ); + cmd.set_subresource_access( + dst, + image_subresource_range_from_layers(region.image_subresource), + AccessType::TransferWrite, + ); + } + + cmd.record_cmd(move |cmd| { + let src = cmd.resource(src); + let dst = cmd.resource(dst); + + unsafe { + cmd.device.cmd_copy_buffer_to_image( + cmd.handle, + src.handle, + dst.handle, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + regions.as_ref(), + ); + } + }) + .end_cmd() + } + + /// Copy all layers of a source image to a destination image. + pub fn copy_image( + &mut self, + src: impl Into, + dst: impl Into, + ) -> &mut Self { + let src = src.into(); + let src_info = self.resource(src).info; + + let dst = dst.into(); + let dst_info = self.resource(dst).info; + + self.copy_image_region( + src, + dst, + [vk::ImageCopy { + src_subresource: vk::ImageSubresourceLayers { + aspect_mask: format_aspect_mask(src_info.fmt), + mip_level: 0, + base_array_layer: 0, + layer_count: src_info.array_layer_count, + }, + src_offset: vk::Offset3D { x: 0, y: 0, z: 0 }, + dst_subresource: vk::ImageSubresourceLayers { + aspect_mask: format_aspect_mask(dst_info.fmt), + mip_level: 0, + base_array_layer: 0, + layer_count: src_info.array_layer_count, + }, + dst_offset: vk::Offset3D { x: 0, y: 0, z: 0 }, + extent: vk::Extent3D { + depth: src_info.depth.clamp(1, dst_info.depth), + height: src_info.height.clamp(1, dst_info.height), + width: src_info.width.min(dst_info.width), + }, + }], + ) + } + + /// Copy data between images. + #[profiling::function] + pub fn copy_image_region( + &mut self, + src: impl Into, + dst: impl Into, + regions: impl AsRef<[vk::ImageCopy]> + 'static + Send, + ) -> &mut Self { + let src = src.into(); + let dst = dst.into(); + + let mut cmd = self.begin_cmd().debug_name("copy image"); + + for region in regions.as_ref() { + cmd.set_subresource_access( + src, + image_subresource_range_from_layers(region.src_subresource), + AccessType::TransferRead, + ); + cmd.set_subresource_access( + dst, + image_subresource_range_from_layers(region.dst_subresource), + AccessType::TransferWrite, + ); + } + + cmd.record_cmd(move |cmd| { + let src = cmd.resource(src); + let dst = cmd.resource(dst); + + unsafe { + cmd.device.cmd_copy_image( + cmd.handle, + src.handle, + vk::ImageLayout::TRANSFER_SRC_OPTIMAL, + dst.handle, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + regions.as_ref(), + ); + } + }) + .end_cmd() + } + + /// Copy image data into a buffer. + pub fn copy_image_to_buffer( + &mut self, + src: impl Into, + dst: impl Into, + ) -> &mut Self { + let src = src.into(); + let dst = dst.into(); + + let src_info = self.resource(src).info; + + self.copy_image_to_buffer_region( + src, + dst, + [vk::BufferImageCopy { + buffer_offset: 0, + buffer_row_length: src_info.width, + buffer_image_height: src_info.height, + image_subresource: vk::ImageSubresourceLayers { + aspect_mask: format_aspect_mask(src_info.fmt), + mip_level: 0, + base_array_layer: 0, + layer_count: 1, + }, + image_offset: Default::default(), + image_extent: vk::Extent3D { + depth: src_info.depth, + height: src_info.height, + width: src_info.width, + }, + }], + ) + } + + /// Copy image data into a buffer. + #[profiling::function] + pub fn copy_image_to_buffer_region( + &mut self, + src: impl Into, + dst: impl Into, + regions: impl AsRef<[vk::BufferImageCopy]> + 'static + Send, + ) -> &mut Self { + let src = src.into(); + let src_info = self.resource(src).info; + let dst = dst.into(); + + let mut cmd = self.begin_cmd().debug_name("copy image to buffer"); + + for region in regions.as_ref() { + let block_bytes_size = format_texel_block_size(src_info.fmt); + let (block_height, block_width) = format_texel_block_extent(src_info.fmt); + let data_size = block_bytes_size + * (region.buffer_row_length / block_width) + * (region.buffer_image_height / block_height); + + cmd.set_subresource_access( + src, + image_subresource_range_from_layers(region.image_subresource), + AccessType::TransferRead, + ); + cmd.set_subresource_access( + dst, + region.buffer_offset..region.buffer_offset + data_size as vk::DeviceSize, + AccessType::TransferWrite, + ); + } + + cmd.record_cmd(move |cmd| { + let src = cmd.resource(src); + let dst = cmd.resource(dst); + + unsafe { + cmd.device.cmd_copy_image_to_buffer( + cmd.handle, + src.handle, + vk::ImageLayout::TRANSFER_SRC_OPTIMAL, + dst.handle, + regions.as_ref(), + ); + } + }) + .end_cmd() + } + + /// Fill a region of a buffer with a fixed value. + pub fn fill_buffer( + &mut self, + buffer: impl Into, + region: Range, + data: u32, + ) -> &mut Self { + let buffer = buffer.into(); + + self.begin_cmd() + .debug_name("fill buffer") + .subresource_access(buffer, region.clone(), AccessType::TransferWrite) + .record_cmd(move |cmd| { + let buffer = cmd.resource(buffer); + + unsafe { + cmd.device.cmd_fill_buffer( + cmd.handle, + buffer.handle, + region.start, + region.end - region.start, + data, + ); + } + }) + .end_cmd() + } + + /// Returns the index of the first pass which accesses a given node + #[profiling::function] + fn first_node_access_pass_index(&self, resource_node: impl Node) -> Option { + let node_idx = resource_node.index(); + + for (pass_idx, pass) in self.cmds.iter().enumerate() { + for exec in pass.execs.iter() { + if exec.accesses.contains_key(&node_idx) { + return Some(pass_idx); + } + } + } + + None + } + + /// Finalizes the graph and provides an object with functions for submitting the resulting + /// commands. + #[profiling::function] + pub fn into_submission(mut self) -> Submission { + // The final execution of each pass has no function + for cmd in &mut self.cmds { + debug_assert!(cmd.expect_last_exec().func.is_none()); + + cmd.execs.pop(); + } + + Submission::new(self) + } + + /// Returns a borrow of the original Vulkan resource (buffer, image or acceleration structure) + /// which the given bound resource node represents. + pub fn resource(&self, resource_node: N) -> &N::Resource + where + N: Node, + { + resource_node.borrow(&self.resources) + } + + /// Note: `data` must not exceed 65536 bytes. + #[profiling::function] + pub fn update_buffer( + &mut self, + buffer: impl Into, + offset: vk::DeviceSize, + data: impl AsRef<[u8]> + 'static + Send, + ) -> &mut Self { + let buffer = buffer.into(); + let data_end = offset + data.as_ref().len() as vk::DeviceSize; + + #[cfg(debug_assertions)] + { + let buffer_info = self.resource(buffer).info; + + assert!( + data_end <= buffer_info.size, + "data range end ({data_end}) exceeds buffer size ({})", + buffer_info.size + ); + } + + self.begin_cmd() + .debug_name("update buffer") + .subresource_access(buffer, offset..data_end, AccessType::TransferWrite) + .record_cmd(move |cmd| { + let buffer = cmd.resource(buffer); + + unsafe { + cmd.device + .cmd_update_buffer(cmd.handle, buffer.handle, offset, data.as_ref()); + } + }) + .end_cmd() + } +} + +/// A Vulkan resource which has been bound to a [`Graph`]. +/// +/// See [`Graph::bind_resource`]. +pub trait Node { + /// The Vulkan buffer, image, or acceleration struction type. + type Resource; + + #[doc(hidden)] + fn borrow(self, resources: &[AnyResource]) -> &Self::Resource; + + #[doc(hidden)] + fn index(&self) -> NodeIndex; +} + +/// A Vulkan resource which may be bound to a [`Graph`]. +/// +/// See [`Graph::bind_resource`] and +/// [`Command::bind_resource`](crate::cmd::Command::bind_resource). +pub trait Resource { + /// The resource handle type. + type Node; + + #[doc(hidden)] + fn bind_graph(self, _: &mut Graph) -> Self::Node; + + #[deprecated = "use bind_graph function"] + #[doc(hidden)] + fn bind(self, graph: &mut Graph) -> Self::Node + where + Self: Sized, + { + self.bind_graph(graph) + } +} + +impl Resource for SwapchainImage { + type Node = SwapchainImageNode; + + fn bind_graph(self, graph: &mut Graph) -> Self::Node { + // We will return a new node + let node = Self::Node::new(graph.resources.len()); + + //trace!("Node {}: {:?}", res.idx, &self); + + let resource = AnyResource::SwapchainImage(Box::new(self)); + graph.resources.push(resource); + + node + } +} + +macro_rules! resource { + ($name:ident) => { + paste::paste! { + impl Resource for $name { + type Node = [<$name Node>]; + + #[profiling::function] + fn bind_graph(self, graph: &mut Graph) -> Self::Node { + // In this function we are resource a new item (Image or Buffer or etc) + // We will return a new node + let node = Self::Node::new(graph.resources.len()); + + let resource = AnyResource::$name(Arc::new(self)); + graph.resources.push(resource); + + node + } + } + + impl Resource for Arc<$name> { + type Node = [<$name Node>]; + + #[profiling::function] + fn bind_graph(self, graph: &mut Graph) -> Self::Node { + // In this function we are resource an existing resource (Arc or + // Arc or etc) + // We will return an existing node, if possible + // TODO: Could store a sorted list of these shared pointers to avoid the O(N) + for (idx, existing_resource) in graph.resources.iter_mut().enumerate() { + if let AnyResource::$name(existing_resource) = existing_resource + && Arc::ptr_eq(existing_resource, &self) { + return Self::Node::new(idx); + } + } + + // Return a new node + let node = Self::Node::new(graph.resources.len()); + let resource = AnyResource::$name(self); + graph.resources.push(resource); + + node + } + } + + impl<'a> Resource for &'a Arc<$name> { + type Node = [<$name Node>]; + + fn bind_graph(self, graph: &mut Graph) -> Self::Node { + // In this function we are resource a borrowed resource (&Arc or + // &Arc or etc) + + Arc::clone(self).bind_graph(graph) + } + } + + impl Resource for Lease<$name> { + type Node = [<$name LeaseNode>]; + + #[profiling::function] + fn bind_graph(self, graph: &mut Graph) -> Self::Node { + // In this function we are wrapping a newly pooled resource (Lease or + // Lease or etc) + + // We will return a new node + let node = Self::Node::new(graph.resources.len()); + let resource = AnyResource::[<$name Lease>](Arc::new(self)); + graph.resources.push(resource); + + node + } + } + + impl Resource for Arc> { + type Node = [<$name LeaseNode>]; + + #[profiling::function] + fn bind_graph(self, graph: &mut Graph) -> Self::Node { + // In this function we are wrapping an existing pooled resource + // (Arc> or Arc> or etc) + + // We will return an existing node, if possible + // TODO: Could store a sorted list of these shared pointers to avoid the O(N) + for (idx, existing_resource) in graph.resources.iter().enumerate() { + if let AnyResource::[<$name Lease>](existing_resource) = existing_resource + && Arc::ptr_eq(existing_resource, &self) { + return Self::Node::new(idx); + } + } + + // We will return a new node + let node = Self::Node::new(graph.resources.len()); + let resource = AnyResource::[<$name Lease>](self); + graph.resources.push(resource); + + node + } + } + + impl<'a> Resource for &'a Arc> { + type Node = [<$name LeaseNode>]; + + fn bind_graph(self, graph: &mut Graph) -> Self::Node { + // In this function we are resource a borrowed resource (&Arc> or + // &Arc> or etc) + + Arc::clone(self).bind_graph(graph) + } + } + } + }; +} + +resource!(AccelerationStructure); +resource!(Image); +resource!(Buffer); + +#[deprecated] +#[doc(hidden)] +pub mod graph { + #[deprecated = "use vk_graph::node module"] + pub mod node { + #[deprecated = "use vk_graph::node::AccelerationStructureLeaseNode"] + pub type AccelerationStructureLeaseNode = crate::node::AccelerationStructureLeaseNode; + + #[deprecated = "use vk_graph::node::AccelerationStructureNode"] + pub type AccelerationStructureNode = crate::node::AccelerationStructureNode; + + #[deprecated = "use vk_graph::node::AnyAccelerationStructureNode"] + pub type AnyAccelerationStructureNode = crate::node::AnyAccelerationStructureNode; + + #[deprecated = "use vk_graph::node::AnyBufferNode"] + pub type AnyBufferNode = crate::node::AnyBufferNode; + + #[deprecated = "use vk_graph::node::AnyImageNode"] + pub type AnyImageNode = crate::node::AnyImageNode; + + #[deprecated = "use vk_graph::node::BufferLeaseNode"] + pub type BufferLeaseNode = crate::node::BufferLeaseNode; + + #[deprecated = "use vk_graph::node::BufferNode"] + pub type BufferNode = crate::node::BufferNode; + + #[deprecated = "use vk_graph::node::ImageLeaseNode"] + pub type ImageLeaseNode = crate::node::ImageLeaseNode; + + #[deprecated = "use vk_graph::node::ImageNode"] + pub type ImageNode = crate::node::ImageNode; + + #[deprecated = "use vk_graph::node::Node"] + pub type Node = dyn crate::Node; + + #[deprecated = "use vk_graph::node::SwapchainImageNode"] + pub type SwapchainImageNode = crate::node::SwapchainImageNode; + } + + #[deprecated] + #[doc(hidden)] + pub mod pass_ref { + #[deprecated = "use vk_graph::cmd::CommandRef"] + pub type Acceleration<'a> = crate::cmd::CommandRef<'a>; + + #[deprecated = "use vk_graph::cmd::CommandRef"] + pub type AccelerationStructureBuildInfo = crate::cmd::BuildAccelerationStructureInfo; + + #[deprecated = "use vk_graph::cmd::CommandRef"] + pub type AccelerationStructureIndirectBuildInfo = + crate::cmd::BuildAccelerationStructureIndirectInfo; + + #[deprecated = "use vk_graph::cmd::CommandRef"] + pub type AccelerationStructureIndirectUpdateInfo = + crate::cmd::UpdateAccelerationStructureIndirectInfo; + + #[deprecated = "use vk_graph::cmd::CommandRef"] + pub type AccelerationStructureUpdateInfo = crate::cmd::UpdateAccelerationStructureInfo; + + #[deprecated = "use vk_graph::cmd::Binding"] + pub type Descriptor = crate::cmd::Binding; + + #[deprecated = "use vk_graph::cmd::GraphicCommandRef"] + pub type Draw<'a> = crate::cmd::GraphicCommandRef<'a>; + + #[deprecated = "use vk_graph::cmd::CommandRef"] + pub type PassRef<'a> = crate::cmd::Command<'a>; + + #[deprecated = "use vk_graph::cmd::PipelineCommand"] + pub type PipelinePassRef<'a, T> = crate::cmd::PipelineCommand<'a, T>; + + #[deprecated = "use vk_graph::cmd::RayTraceCommandRef"] + pub type RayTrace<'a> = crate::cmd::RayTraceCommandRef<'a>; + + #[deprecated = "use vk_graph::ViewInfo"] + pub type ViewType = crate::cmd::ViewInfo; + + #[deprecated = "remove"] + pub trait View { + type Information; + } + } + + #[deprecated = "use vk_graph::Graph"] + pub type RenderGraph = crate::Graph; + + #[deprecated = "use vk_graph::Submission"] + pub type Resolver = crate::Submission; +} + +#[allow(deprecated)] +#[allow(unused)] +#[doc(hidden)] +pub(crate) mod deprecated { + use { + crate::{ + AnyResource, Graph, Node, Resource, + driver::{ + DriverError, + accel_struct::{AccelerationStructure, AccelerationStructureInfo}, + buffer::{Buffer, BufferInfo}, + cmd_buf::{CommandBuffer, CommandBufferInfo}, + descriptor_set::{DescriptorPool, DescriptorPoolInfo}, + device::Device, + image::{Image, ImageInfo}, + render_pass::{RenderPass, RenderPassInfo}, + swapchain::{Swapchain, SwapchainImage, SwapchainInfo}, }, - }, - graph::{ - Bind, ClearColorValue, RenderGraph, Unbind, node::{ AccelerationStructureLeaseNode, AccelerationStructureNode, AnyAccelerationStructureNode, AnyBufferNode, AnyImageNode, BufferLeaseNode, BufferNode, ImageLeaseNode, ImageNode, SwapchainImageNode, }, - pass_ref::{PassRef, PipelinePassRef}, - }, - pool::{ - Lease, Pool, PoolInfo, PoolInfoBuilder, - alias::{Alias, AliasPool}, - fifo::FifoPool, - hash::HashPool, - lazy::LazyPool, + pool::{Lease, Pool}, }, + ash::vk, + std::{error, fmt, ops::Range, sync::Arc}, }; -} -pub use self::display::{Display, DisplayError, DisplayInfo, DisplayInfoBuilder, ResolverPool}; + /// Specifies a color attachment clear value which can be used to initliaze an image. + #[derive(Clone, Copy, Debug)] + pub struct ClearColorValue(pub [f32; 4]); + + impl From<[f32; 3]> for ClearColorValue { + fn from(color: [f32; 3]) -> Self { + [color[0], color[1], color[2], 1.0].into() + } + } + + impl From<[f32; 4]> for ClearColorValue { + fn from(color: [f32; 4]) -> Self { + Self(color) + } + } + + impl From<[u8; 3]> for ClearColorValue { + fn from(color: [u8; 3]) -> Self { + [color[0], color[1], color[2], u8::MAX].into() + } + } + + impl From<[u8; 4]> for ClearColorValue { + fn from(color: [u8; 4]) -> Self { + [ + color[0] as f32 / u8::MAX as f32, + color[1] as f32 / u8::MAX as f32, + color[2] as f32 / u8::MAX as f32, + color[3] as f32 / u8::MAX as f32, + ] + .into() + } + } + + #[deprecated = "use Swapchain from vk_graph_window crate"] + #[derive(Debug)] + #[doc(hidden)] + pub struct Display { + swapchain_info: SwapchainInfo, + } + + impl Display { + pub fn new( + device: &Arc, + swapchain: Swapchain, + info: impl Into, + ) -> Result { + let _ = device; + let _ = swapchain; + let _ = info.into(); + + Err(DriverError::Unsupported) + } + + pub fn acquire_next_image(&mut self) -> Result, DisplayError> { + Err(DisplayError) + } + + pub fn present_image( + &mut self, + pool: &mut impl ResolverPool, + render_graph: crate::graph::RenderGraph, + swapchain_image: SwapchainImageNode, + queue_index: u32, + ) -> Result<(), DisplayError> { + let _ = pool; + let _ = render_graph; + let _ = swapchain_image; + let _ = queue_index; + + Err(DisplayError) + } + + pub fn set_swapchain_info(&mut self, info: impl Into) { + self.swapchain_info = info.into(); + } + + pub fn swapchain_info(&self) -> SwapchainInfo { + self.swapchain_info + } + } + + #[deprecated = "use vk_graph_window::SwapchainError"] + #[derive(Clone, Copy, Debug, Default)] + #[doc(hidden)] + pub struct DisplayError; + + impl fmt::Display for DisplayError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("deprecated Display API is unsupported; use vk_graph_window swapchain APIs") + } + } + + impl error::Error for DisplayError {} + + #[deprecated = "use vk_graph_window::SwapchainInfo"] + #[derive(Clone, Copy, Debug, Default)] + #[doc(hidden)] + pub struct DisplayInfo; + + #[deprecated = "use vk_graph_window::SwapchainInfoBuilder"] + #[derive(Clone, Copy, Debug, Default)] + #[doc(hidden)] + pub struct DisplayInfoBuilder; + + // General stuff + impl Graph { + #[deprecated = "use begin_cmd function"] + #[doc(hidden)] + pub fn begin_pass(&mut self, name: impl AsRef) -> crate::graph::pass_ref::PassRef<'_> { + self.begin_cmd().debug_name(name.as_ref().to_owned()) + } + + #[deprecated = "use bind_resource function"] + #[doc(hidden)] + pub fn bind_node(&mut self, resource: R) -> R::Node + where + R: Resource, + { + self.bind_resource(resource) + } + + #[deprecated = "use blit_image_region function"] + #[doc(hidden)] + pub fn blit_image_regions( + &mut self, + src_node: impl Into, + dst_node: impl Into, + filter: vk::Filter, + regions: impl AsRef<[vk::ImageBlit]> + 'static + Send, + ) -> &mut Self { + self.blit_image_region(src_node, dst_node, filter, regions) + } + + #[deprecated = "use clear_color_image function"] + #[doc(hidden)] + pub fn clear_color_image_value( + &mut self, + image_node: impl Into, + color_value: impl Into, + ) -> &mut Self { + self.clear_color_image(image_node, color_value.into().0) + } + + #[deprecated = "use clear_depth_stencil_image function"] + #[doc(hidden)] + pub fn clear_depth_stencil_image_value( + &mut self, + image_node: impl Into, + depth: f32, + stencil: u32, + ) -> &mut Self { + self.clear_depth_stencil_image(image_node, depth, stencil) + } + + #[deprecated = "use copy_buffer_region function"] + #[doc(hidden)] + pub fn copy_buffer_regions( + &mut self, + src_node: impl Into, + dst_node: impl Into, + regions: impl AsRef<[vk::BufferCopy]> + 'static + Send, + ) -> &mut Self { + self.copy_buffer_region(src_node, dst_node, regions) + } + + #[deprecated = "use copy_buffer_to_image_region function"] + #[doc(hidden)] + pub fn copy_buffer_to_image_regions( + &mut self, + src_node: impl Into, + dst_node: impl Into, + regions: impl AsRef<[vk::BufferImageCopy]> + 'static + Send, + ) -> &mut Self { + self.copy_buffer_to_image_region(src_node, dst_node, regions) + } + + #[deprecated = "use copy_image_region function"] + #[doc(hidden)] + pub fn copy_image_regions( + &mut self, + src_node: impl Into, + dst_node: impl Into, + regions: impl AsRef<[vk::ImageCopy]> + 'static + Send, + ) -> &mut Self { + self.copy_image_region(src_node, dst_node, regions) + } + + #[deprecated = "use copy_image_to_buffer_region function"] + #[doc(hidden)] + pub fn copy_image_to_buffer_regions( + &mut self, + src_node: impl Into, + dst_node: impl Into, + regions: impl AsRef<[vk::BufferImageCopy]> + 'static + Send, + ) -> &mut Self { + self.copy_image_to_buffer_region(src_node, dst_node, regions) + } + + #[deprecated = "use fill_buffer function"] + #[doc(hidden)] + pub fn fill_buffer_region( + &mut self, + buffer_node: impl Into, + data: u32, + region: Range, + ) -> &mut Self { + self.fill_buffer(buffer_node, region, data) + } + + #[deprecated = "use device_address function of resource function result"] + #[doc(hidden)] + pub fn node_device_address(&self, node: impl Node) -> vk::DeviceAddress { + let idx = node.index(); + + self.resources[idx] + .as_buffer() + .expect("missing buffer resource") + .device_address() + } + + #[deprecated = "dereference info field of resource function result"] + #[doc(hidden)] + pub fn node_info(&self, node: N) -> N::Type + where + N: Node + Info, + { + node.info(&self.resources) + } + + #[deprecated = "use into_submission function"] + #[doc(hidden)] + pub fn resolve(self) -> crate::graph::Resolver { + self.into_submission() + } + + #[deprecated = "use resource and clone functions"] + #[doc(hidden)] + pub fn unbind_node(&mut self, node: N) -> N::Result + where + N: Unbind, + { + node.unbind(&self.resources) + } + + #[deprecated = "use update_buffer function"] + #[doc(hidden)] + pub fn update_buffer_offset( + &mut self, + buffer_node: impl Into, + offset: vk::DeviceSize, + data: impl AsRef<[u8]> + 'static + Send, + ) -> &mut Self { + self.update_buffer(buffer_node, offset, data) + } + } + + pub trait Info { + type Type; + + fn info(&self, _: &[AnyResource]) -> Self::Type + where + Self: Node; + } + + impl Info for SwapchainImageNode { + type Type = ImageInfo; + + fn info(&self, resources: &[AnyResource]) -> Self::Type + where + Self: Node, + { + resources[self.index()] + .as_swapchain_image() + .expect("missing swapchain image") + .info + } + } + + macro_rules! info { + ($name:ident) => { + paste::paste! { + impl Info for [<$name Node>] { + type Type = [<$name Info>]; + + fn info(&self, resources: &[AnyResource]) -> Self::Type + where + Self: Node, + { + let AnyResource::$name(resource) = &resources[self.index()] else { + panic!("invalid node"); + }; + + resource.info + } + } + + impl Info for [] { + type Type = [<$name Info>]; + + fn info(&self, resources: &[AnyResource]) -> Self::Type + where + Self: Node, + { + let AnyResource::$name(resource) = &resources[self.index()] else { + panic!("invalid node"); + }; + + resource.info + } + } + + impl Info for [<$name LeaseNode>] { + type Type = [<$name Info>]; + + fn info(&self, resources: &[AnyResource]) -> Self::Type + where + Self: Node, + { + let AnyResource::[<$name Lease>](resource) = &resources[self.index()] else { + panic!("invalid node"); + }; + + resource.info + } + } + + impl Unbind for [<$name Node>] { + type Result = Arc<$name>; + + fn unbind(&self, resources: &[AnyResource]) -> Self::Result { + let AnyResource::$name(resource) = &resources[self.index()] else { + panic!("invalid node"); + }; + + resource.clone() + } + } + + impl Unbind for [<$name LeaseNode>] { + type Result = Arc>; + + fn unbind(&self, resources: &[AnyResource]) -> Self::Result { + let AnyResource::[<$name Lease>](resource) = &resources[self.index()] else { + panic!("invalid node"); + }; + + resource.clone() + } + } + } + }; + } + + info!(AccelerationStructure); + info!(Buffer); + info!(Image); + + #[deprecated = "remove"] + pub trait ResolverPool: + Pool + + Pool + + Pool + + Send + { + } + + pub trait Unbind: Node { + type Result; + + fn unbind(&self, _: &[AnyResource]) -> Self::Result; + } +} diff --git a/src/node.rs b/src/node.rs new file mode 100644 index 00000000..f86a6299 --- /dev/null +++ b/src/node.rs @@ -0,0 +1,189 @@ +//! Handles for Vulkan smart-pointer resources. + +use std::sync::Arc; + +use crate::{ + Node, + driver::{ + accel_struct::AccelerationStructure, buffer::Buffer, image::Image, + swapchain::SwapchainImage, + }, + pool::Lease, +}; + +use super::{AnyResource, NodeIndex}; + +/// Specifies either an owned acceleration structure or one obtained from a pool. +#[derive(Clone, Copy, Debug)] +pub enum AnyAccelerationStructureNode { + /// An owned acceleration structure. + AccelerationStructure(AccelerationStructureNode), + + /// An acceleration structure obtained from a pool. + AccelerationStructureLease(AccelerationStructureLeaseNode), +} + +impl From for AnyAccelerationStructureNode { + fn from(node: AccelerationStructureNode) -> Self { + Self::AccelerationStructure(node) + } +} + +impl From for AnyAccelerationStructureNode { + fn from(node: AccelerationStructureLeaseNode) -> Self { + Self::AccelerationStructureLease(node) + } +} + +impl Node for AnyAccelerationStructureNode { + type Resource = AccelerationStructure; + + fn borrow(self, resources: &[AnyResource]) -> &Self::Resource { + resources[self.index()].expect_accel_struct() + } + + fn index(&self) -> NodeIndex { + match self { + Self::AccelerationStructure(node) => node.index(), + Self::AccelerationStructureLease(node) => node.index(), + } + } +} + +/// Specifies either an owned buffer or one obtained from a pool. +#[derive(Clone, Copy, Debug)] +pub enum AnyBufferNode { + /// An owned buffer. + Buffer(BufferNode), + + /// A buffer obtained from a pool. + BufferLease(BufferLeaseNode), +} + +impl From for AnyBufferNode { + fn from(node: BufferNode) -> Self { + Self::Buffer(node) + } +} + +impl From for AnyBufferNode { + fn from(node: BufferLeaseNode) -> Self { + Self::BufferLease(node) + } +} + +impl Node for AnyBufferNode { + type Resource = Buffer; + + fn borrow(self, resources: &[AnyResource]) -> &Self::Resource { + resources[self.index()].expect_buffer() + } + + fn index(&self) -> NodeIndex { + match self { + Self::Buffer(node) => node.index(), + Self::BufferLease(node) => node.index(), + } + } +} + +/// Specifies either an owned image or one obtained from a pool. +/// +/// The image may also be a special swapchain type of image. +#[derive(Clone, Copy, Debug)] +pub enum AnyImageNode { + /// An owned image. + Image(ImageNode), + + /// An image obtained from a pool. + ImageLease(ImageLeaseNode), + + /// A special swapchain image. + SwapchainImage(SwapchainImageNode), +} + +impl From for AnyImageNode { + fn from(node: ImageNode) -> Self { + Self::Image(node) + } +} + +impl From for AnyImageNode { + fn from(node: ImageLeaseNode) -> Self { + Self::ImageLease(node) + } +} + +impl From for AnyImageNode { + fn from(node: SwapchainImageNode) -> Self { + Self::SwapchainImage(node) + } +} + +impl Node for AnyImageNode { + type Resource = Image; + + fn borrow(self, resources: &[AnyResource]) -> &Self::Resource { + resources[self.index()].expect_image() + } + + fn index(&self) -> NodeIndex { + match self { + Self::Image(node) => node.index(), + Self::ImageLease(node) => node.index(), + Self::SwapchainImage(node) => node.index(), + } + } +} + +macro_rules! node { + ($name:ident, $resource:ty, $fn_name:ident) => { + paste::paste! { + /// Resource node. + #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] + pub struct [<$name Node>] { + index: NodeIndex, + } + + impl [<$name Node>] { + pub(crate) fn new(index: usize) -> Self { + Self { + index, + } + } + } + + impl Node for [<$name Node>] { + type Resource = $resource; + + fn borrow(self, resources: &[AnyResource]) -> &Self::Resource { + let AnyResource::$name(res) = &resources[self.index] else { + panic!("invalid resource node handle"); + }; + + res + } + + fn index(&self) -> NodeIndex { + self.index + } + } + } + }; +} + +node!( + AccelerationStructure, + Arc, + as_accel_struct +); +node!( + AccelerationStructureLease, + Arc>, + as_accel_struct +); +node!(Buffer, Arc, as_buffer); +node!(BufferLease, Arc>, as_buffer); +node!(Image, Arc, as_image); +node!(ImageLease, Arc>, as_image); +node!(SwapchainImage, SwapchainImage, as_swapchain_image); diff --git a/src/pool/alias.rs b/src/pool/alias.rs deleted file mode 100644 index dad1bf92..00000000 --- a/src/pool/alias.rs +++ /dev/null @@ -1,226 +0,0 @@ -//! Pool wrapper which enables memory-efficient resource aliasing. - -use { - super::{Lease, Pool}, - crate::driver::{ - DriverError, - accel_struct::{ - AccelerationStructure, AccelerationStructureInfo, AccelerationStructureInfoBuilder, - }, - buffer::{Buffer, BufferInfo, BufferInfoBuilder}, - image::{Image, ImageInfo, ImageInfoBuilder}, - }, - log::debug, - std::{ - ops::{Deref, DerefMut}, - sync::{Arc, Weak}, - }, -}; - -/// Allows aliasing of resources using driver information structures. -pub trait Alias { - /// Aliases a resource. - fn alias(&mut self, info: I) -> Result>, DriverError>; -} - -// Enable aliasing items using their info builder type for convenience -macro_rules! alias_builder { - ($info:ident => $item:ident) => { - paste::paste! { - impl Alias<[<$info Builder>], $item> for T where T: Alias<$info, $item> { - fn alias(&mut self, builder: [<$info Builder>]) -> Result>, DriverError> { - let info = builder.build(); - - self.alias(info) - } - } - } - }; -} - -alias_builder!(AccelerationStructureInfo => AccelerationStructure); -alias_builder!(BufferInfo => Buffer); -alias_builder!(ImageInfo => Image); - -/// A memory-efficient resource wrapper for any [`Pool`] type. -/// -/// The information for each alias request is compared against the actively aliased resources for -/// compatibility. If no acceptable resources are aliased for the information provided a new -/// resource is leased and returned. -/// -/// All regular leasing and other functionality of the wrapped pool is available through `Deref` and -/// `DerefMut`. -/// -/// **_NOTE:_** You must call `alias(..)` to use resource aliasing as regular `lease(..)` calls will -/// not inspect or return aliased resources. -/// -/// # Details -/// -/// * Acceleration structures may be larger than requested -/// * Buffers may be larger than requested or have additional usage flags -/// * Images may have additional usage flags -/// -/// # Examples -/// -/// See [`aliasing.rs`](https://github.com/attackgoat/screen-13/blob/master/examples/aliasing.rs) -pub struct AliasPool { - accel_structs: Vec<( - AccelerationStructureInfo, - Weak>, - )>, - buffers: Vec<(BufferInfo, Weak>)>, - images: Vec<(ImageInfo, Weak>)>, - pool: T, -} - -impl AliasPool { - /// Creates a new aliasable wrapper over the given pool. - pub fn new(pool: T) -> Self { - Self { - accel_structs: Default::default(), - buffers: Default::default(), - images: Default::default(), - pool, - } - } -} - -// Enable aliasing items using their info builder type for convenience -macro_rules! lease_pass_through { - ($info:ident => $item:ident) => { - paste::paste! { - impl Pool<$info, $item> for AliasPool where T: Pool<$info, $item> { - fn lease(&mut self, info: $info) -> Result, DriverError> { - self.pool.lease(info) - } - } - } - }; -} - -lease_pass_through!(AccelerationStructureInfo => AccelerationStructure); -lease_pass_through!(BufferInfo => Buffer); -lease_pass_through!(ImageInfo => Image); - -impl Alias for AliasPool -where - T: Pool, -{ - fn alias( - &mut self, - info: AccelerationStructureInfo, - ) -> Result>, DriverError> { - self.accel_structs - .retain(|(_, item)| item.strong_count() > 0); - - { - profiling::scope!("check aliases"); - - for (item_info, item) in &self.accel_structs { - if item_info.ty == info.ty && item_info.size >= info.size { - if let Some(item) = item.upgrade() { - return Ok(item); - } else { - break; - } - } - } - } - - debug!("Leasing new {}", stringify!(AccelerationStructure)); - - let item = Arc::new(self.pool.lease(info)?); - self.accel_structs.push((info, Arc::downgrade(&item))); - - Ok(item) - } -} - -impl Alias for AliasPool -where - T: Pool, -{ - fn alias(&mut self, info: BufferInfo) -> Result>, DriverError> { - self.buffers.retain(|(_, item)| item.strong_count() > 0); - - { - profiling::scope!("check aliases"); - - for (item_info, item) in &self.buffers { - if item_info.mappable == info.mappable - && item_info.alignment >= info.alignment - && item_info.size >= info.size - && item_info.usage.contains(info.usage) - { - if let Some(item) = item.upgrade() { - return Ok(item); - } else { - break; - } - } - } - } - - debug!("Leasing new {}", stringify!(Buffer)); - - let item = Arc::new(self.pool.lease(info)?); - self.buffers.push((info, Arc::downgrade(&item))); - - Ok(item) - } -} - -impl Alias for AliasPool -where - T: Pool, -{ - fn alias(&mut self, info: ImageInfo) -> Result>, DriverError> { - self.images.retain(|(_, item)| item.strong_count() > 0); - - { - profiling::scope!("check aliases"); - - for (item_info, item) in &self.images { - if item_info.array_layer_count == info.array_layer_count - && item_info.depth == info.depth - && item_info.fmt == info.fmt - && item_info.height == info.height - && item_info.mip_level_count == info.mip_level_count - && item_info.sample_count == info.sample_count - && item_info.tiling == info.tiling - && item_info.ty == info.ty - && item_info.width == info.width - && item_info.flags.contains(info.flags) - && item_info.usage.contains(info.usage) - { - if let Some(item) = item.upgrade() { - return Ok(item); - } else { - break; - } - } - } - } - - debug!("Leasing new {}", stringify!(Image)); - - let item = Arc::new(self.pool.lease(info)?); - self.images.push((info, Arc::downgrade(&item))); - - Ok(item) - } -} - -impl Deref for AliasPool { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.pool - } -} - -impl DerefMut for AliasPool { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.pool - } -} diff --git a/src/pool/cache.rs b/src/pool/cache.rs new file mode 100644 index 00000000..67693da9 --- /dev/null +++ b/src/pool/cache.rs @@ -0,0 +1,351 @@ +//! Pool wrapper which enables memory-efficient resource caching. + +use { + super::{Lease, Pool}, + crate::driver::{ + DriverError, + accel_struct::{AccelerationStructure, AccelerationStructureInfo}, + buffer::{Buffer, BufferInfo}, + image::{Image, ImageInfo}, + }, + log::debug, + std::{ + collections::HashMap, + hash::Hash, + ops::{Deref, DerefMut}, + sync::{Arc, Weak}, + }, +}; + +#[derive(Default)] +struct AliasSet { + accel_structs: Vec<( + AccelerationStructureInfo, + Weak>, + )>, + buffers: Vec<(BufferInfo, Weak>)>, + images: Vec<(ImageInfo, Weak>)>, +} + +/// A memory-efficient resource cache for any [`Pool`] type. +/// +/// Use [`Cache::tag`] to create a tag-scoped view that caches resources independently from other +/// tags. Untagged access still behaves like the default cache wrapper. +/// +/// # Examples +/// +/// ```no_run +/// # use vk_graph::driver::device::{Device, DeviceInfo}; +/// # use vk_graph::driver::image::ImageInfo; +/// # use vk_graph::pool::cache::Cache; +/// # use vk_graph::pool::hash::HashPool; +/// # fn main() { +/// # let device = Device::create(DeviceInfo::default()).unwrap(); +/// # let mut cache = Cache::new(HashPool::new(&device)); +/// let mut shadow = cache.tag("shadow"); +/// let image = shadow +/// .resource(ImageInfo::image_2d( +/// 32, +/// 32, +/// ash::vk::Format::R8G8B8A8_UNORM, +/// ash::vk::ImageUsageFlags::SAMPLED, +/// )) +/// .unwrap(); +/// # let _ = image; +/// # } +/// ``` +pub struct Cache { + aliases: HashMap, + pool: T, +} + +/// A tag-scoped cache view. +pub struct TaggedCache<'a, T, Tag> { + cache: &'a mut Cache, + tag: Tag, +} + +impl Cache +where + Tag: Eq + Hash, +{ + /// Creates a new cache wrapper over the given pool. + pub fn new(pool: T) -> Self { + Self { + aliases: Default::default(), + pool, + } + } + + /// Returns a tag-scoped cache view. + pub fn tag(&mut self, tag: Tag) -> TaggedCache<'_, T, Tag> { + TaggedCache { cache: self, tag } + } + + fn alias_set(&mut self, tag: Tag) -> &mut AliasSet { + self.aliases.entry(tag).or_default() + } + + fn resource_accel_struct_tagged( + &mut self, + tag: Tag, + info: AccelerationStructureInfo, + ) -> Result>, DriverError> + where + Tag: Clone, + T: Pool, + { + let mut result = None; + + { + let state = self.alias_set(tag.clone()); + state + .accel_structs + .retain(|(_, item)| item.strong_count() > 0); + + profiling::scope!("check aliases"); + + for (item_info, item) in &state.accel_structs { + if item_info.ty == info.ty + && item_info.size >= info.size + && let Some(item) = item.upgrade() + { + result = Some(item); + break; + } + } + } + + if let Some(item) = result { + return Ok(item); + } + + debug!("Leasing new {}", stringify!(AccelerationStructure)); + + let item = Arc::new(self.pool.resource(info)?); + self.alias_set(tag) + .accel_structs + .push((info, Arc::downgrade(&item))); + + Ok(item) + } + + fn resource_buffer_tagged( + &mut self, + tag: Tag, + info: BufferInfo, + ) -> Result>, DriverError> + where + Tag: Clone, + T: Pool, + { + let mut result = None; + + { + let state = self.alias_set(tag.clone()); + state.buffers.retain(|(_, item)| item.strong_count() > 0); + + profiling::scope!("check aliases"); + + for (item_info, item) in &state.buffers { + if (item_info.dedicated & info.dedicated) == info.dedicated + && item_info.host_read == info.host_read + && item_info.host_write == info.host_write + && item_info.alignment >= info.alignment + && item_info.size >= info.size + && item_info.usage.contains(info.usage) + && let Some(item) = item.upgrade() + { + result = Some(item); + break; + } + } + } + + if let Some(item) = result { + return Ok(item); + } + + debug!("Leasing new {}", stringify!(Buffer)); + + let item = Arc::new(self.pool.resource(info)?); + self.alias_set(tag) + .buffers + .push((info, Arc::downgrade(&item))); + + Ok(item) + } + + fn resource_image_tagged( + &mut self, + tag: Tag, + info: ImageInfo, + ) -> Result>, DriverError> + where + Tag: Clone, + T: Pool, + { + let mut result = None; + + { + let state = self.alias_set(tag.clone()); + state.images.retain(|(_, item)| item.strong_count() > 0); + + profiling::scope!("check aliases"); + + for (item_info, item) in &state.images { + if item_info.array_layer_count == info.array_layer_count + && item_info.dedicated == info.dedicated + && item_info.depth == info.depth + && item_info.fmt == info.fmt + && item_info.height == info.height + && item_info.mip_level_count == info.mip_level_count + && item_info.sample_count == info.sample_count + && item_info.tiling == info.tiling + && item_info.ty == info.ty + && item_info.width == info.width + && item_info.flags.contains(info.flags) + && item_info.usage.contains(info.usage) + && let Some(item) = item.upgrade() + { + result = Some(item); + break; + } + } + } + + if let Some(item) = result { + return Ok(item); + } + + debug!("Leasing new {}", stringify!(Image)); + + let item = Arc::new(self.pool.resource(info)?); + self.alias_set(tag) + .images + .push((info, Arc::downgrade(&item))); + + Ok(item) + } +} + +impl Cache +where + T: Pool + + Pool + + Pool, +{ + /// Alias an acceleration structure using the default tag. + pub fn accel_struct( + &mut self, + info: AccelerationStructureInfo, + ) -> Result>, DriverError> { + self.resource_accel_struct_tagged((), info) + } + + /// Alias a buffer using the default tag. + pub fn buffer(&mut self, info: BufferInfo) -> Result>, DriverError> { + self.resource_buffer_tagged((), info) + } + + /// Alias an image using the default tag. + pub fn image(&mut self, info: ImageInfo) -> Result>, DriverError> { + self.resource_image_tagged((), info) + } +} + +impl<'a, T, Tag> TaggedCache<'a, T, Tag> +where + Tag: Eq + Hash + Clone, +{ + /// Alias a resource using this cache tag. + pub fn resource(&mut self, info: I) -> Result>, DriverError> + where + I: TaggedCacheResource, + T: Pool, + { + I::resource(self.cache, self.tag.clone(), info) + } +} + +#[doc(hidden)] +pub trait TaggedCacheResource: Sized { + type Item; + + fn resource( + cache: &mut Cache, + tag: Tag, + info: Self, + ) -> Result>, DriverError> + where + Tag: Eq + Hash + Clone, + T: Pool; +} + +impl TaggedCacheResource for AccelerationStructureInfo +where + Tag: Eq + Hash + Clone, +{ + type Item = AccelerationStructure; + + fn resource( + cache: &mut Cache, + tag: Tag, + info: Self, + ) -> Result>, DriverError> + where + T: Pool, + { + cache.resource_accel_struct_tagged(tag, info) + } +} + +impl TaggedCacheResource for BufferInfo +where + Tag: Eq + Hash + Clone, +{ + type Item = Buffer; + + fn resource( + cache: &mut Cache, + tag: Tag, + info: Self, + ) -> Result>, DriverError> + where + T: Pool, + { + cache.resource_buffer_tagged(tag, info) + } +} + +impl TaggedCacheResource for ImageInfo +where + Tag: Eq + Hash + Clone, +{ + type Item = Image; + + fn resource( + cache: &mut Cache, + tag: Tag, + info: Self, + ) -> Result>, DriverError> + where + T: Pool, + { + cache.resource_image_tagged(tag, info) + } +} + +impl Deref for Cache { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.pool + } +} + +impl DerefMut for Cache { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.pool + } +} diff --git a/src/pool/fifo.rs b/src/pool/fifo.rs index 9e825bb2..e3a10363 100644 --- a/src/pool/fifo.rs +++ b/src/pool/fifo.rs @@ -1,14 +1,16 @@ -//! Pool which leases from a single bucket per resource type. +//! Pool which requests from a single bucket per resource type. use { - super::{Cache, Lease, Pool, PoolInfo, lease_command_buffer}, + super::{Cache, Lease, Pool, PoolConfig, lease_command_buffer, with_cache}, crate::driver::{ - CommandBuffer, CommandBufferInfo, DescriptorPool, DescriptorPoolInfo, DriverError, - RenderPass, RenderPassInfo, + DriverError, accel_struct::{AccelerationStructure, AccelerationStructureInfo}, buffer::{Buffer, BufferInfo}, + cmd_buf::{CommandBuffer, CommandBufferInfo}, + descriptor_set::{DescriptorPool, DescriptorPoolInfo}, device::Device, image::{Image, ImageInfo}, + render_pass::{RenderPass, RenderPassInfo}, }, log::debug, std::{collections::HashMap, sync::Arc}, @@ -16,7 +18,7 @@ use { /// A memory-efficient resource allocator. /// -/// The information for each lease request is compared against the stored resources for +/// The information for each resource request is compared against the stored resources for /// compatibility. If no acceptable resources are stored for the information provided a new resource /// is created and returned. /// @@ -31,8 +33,8 @@ use { /// All resources are stored in a single bucket per resource type, regardless of their individual /// attributes. /// -/// In practice this means that for a [`PoolInfo::image_capacity`] of `4`, a maximum of `4` images -/// will be stored. Requests to lease an image or other resource will first look for a compatible +/// In practice this means that for a [`PoolConfig::image_capacity`] of `4`, a maximum of `4` images +/// will be stored. Requests to obtain an image or other resource will first look for a compatible /// resource in the bucket and create a new resource as needed. /// /// # Memory Management @@ -41,35 +43,48 @@ use { /// of stored resources, however you may call [`FifoPool::clear`] or the other memory management /// functions at any time to discard stored resources. #[derive(Debug)] +#[read_only::cast] pub struct FifoPool { accel_struct_cache: Cache, buffer_cache: Cache, command_buffer_cache: HashMap>, descriptor_pool_cache: Cache, - device: Arc, + + /// The device which owns this pool. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub device: Device, + image_cache: Cache, - info: PoolInfo, + + /// Information used to create this pool. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub info: PoolConfig, + render_pass_cache: HashMap>, } impl FifoPool { /// Constructs a new `FifoPool`. - pub fn new(device: &Arc) -> Self { - Self::with_capacity(device, PoolInfo::default()) + pub fn new(device: &Device) -> Self { + Self::with_capacity(device, PoolConfig::default()) } /// Constructs a new `FifoPool` with the given capacity information. - pub fn with_capacity(device: &Arc, info: impl Into) -> Self { - let info: PoolInfo = info.into(); - let device = Arc::clone(device); + pub fn with_capacity(device: &Device, info: impl Into) -> Self { + let info: PoolConfig = info.into(); + let device = device.clone(); Self { - accel_struct_cache: PoolInfo::explicit_cache(info.accel_struct_capacity), - buffer_cache: PoolInfo::explicit_cache(info.buffer_capacity), + accel_struct_cache: PoolConfig::explicit_cache(info.accel_struct_capacity), + buffer_cache: PoolConfig::explicit_cache(info.buffer_capacity), command_buffer_cache: Default::default(), - descriptor_pool_cache: PoolInfo::default_cache(), + descriptor_pool_cache: PoolConfig::default_cache(), device, - image_cache: PoolInfo::explicit_cache(info.image_capacity), + image_cache: PoolConfig::explicit_cache(info.image_capacity), info, render_pass_cache: Default::default(), } @@ -84,23 +99,23 @@ impl FifoPool { /// Clears the pool of acceleration structure resources. pub fn clear_accel_structs(&mut self) { - self.accel_struct_cache = PoolInfo::explicit_cache(self.info.accel_struct_capacity); + self.accel_struct_cache = PoolConfig::explicit_cache(self.info.accel_struct_capacity); } /// Clears the pool of buffer resources. pub fn clear_buffers(&mut self) { - self.buffer_cache = PoolInfo::explicit_cache(self.info.buffer_capacity); + self.buffer_cache = PoolConfig::explicit_cache(self.info.buffer_capacity); } /// Clears the pool of image resources. pub fn clear_images(&mut self) { - self.image_cache = PoolInfo::explicit_cache(self.info.image_capacity); + self.image_cache = PoolConfig::explicit_cache(self.info.image_capacity); } } impl Pool for FifoPool { #[profiling::function] - fn lease( + fn resource( &mut self, info: AccelerationStructureInfo, ) -> Result, DriverError> { @@ -109,20 +124,20 @@ impl Pool for FifoPool { { profiling::scope!("check cache"); - #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] - let mut cache = self.accel_struct_cache.lock(); - - #[cfg(not(feature = "parking_lot"))] - let mut cache = cache.unwrap(); + if let Some(item) = with_cache(&self.accel_struct_cache, |cache| { + // Look for a compatible acceleration structure (big enough and same type) + for idx in 0..cache.len() { + let item = unsafe { cache.get_unchecked(idx) }; + if item.info.size >= info.size && item.info.ty == info.ty { + let item = cache.swap_remove(idx); - // Look for a compatible acceleration structure (big enough and same type) - for idx in 0..cache.len() { - let item = unsafe { cache.get_unchecked(idx) }; - if item.info.size >= info.size && item.info.ty == info.ty { - let item = cache.swap_remove(idx); - - return Ok(Lease::new(cache_ref, item)); + return Some(Lease::new(cache_ref.clone(), item)); + } } + + None + }) { + return Ok(item); } } @@ -136,31 +151,33 @@ impl Pool for FifoPool { impl Pool for FifoPool { #[profiling::function] - fn lease(&mut self, info: BufferInfo) -> Result, DriverError> { + fn resource(&mut self, info: BufferInfo) -> Result, DriverError> { let cache_ref = Arc::downgrade(&self.buffer_cache); { profiling::scope!("check cache"); - #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] - let mut cache = self.buffer_cache.lock(); - - #[cfg(not(feature = "parking_lot"))] - let mut cache = cache.unwrap(); - - // Look for a compatible buffer (compatible alignment, same mapping mode, big enough and - // superset of usage flags) - for idx in 0..cache.len() { - let item = unsafe { cache.get_unchecked(idx) }; - if item.info.alignment >= info.alignment - && item.info.mappable == info.mappable - && item.info.size >= info.size - && item.info.usage.contains(info.usage) - { - let item = cache.swap_remove(idx); - - return Ok(Lease::new(cache_ref, item)); + if let Some(item) = with_cache(&self.buffer_cache, |cache| { + // Look for a compatible buffer (compatible alignment, same mapping mode, big enough + // and superset of usage flags) + for idx in 0..cache.len() { + let item = unsafe { cache.get_unchecked(idx) }; + if (item.info.dedicated & info.dedicated) == info.dedicated + && item.info.host_read == info.host_read + && item.info.host_write == info.host_write + && item.info.alignment >= info.alignment + && item.info.size >= info.size + && item.info.usage.contains(info.usage) + { + let item = cache.swap_remove(idx); + + return Some(Lease::new(cache_ref.clone(), item)); + } } + + None + }) { + return Ok(item); } } @@ -174,30 +191,22 @@ impl Pool for FifoPool { impl Pool for FifoPool { #[profiling::function] - fn lease(&mut self, info: CommandBufferInfo) -> Result, DriverError> { + fn resource(&mut self, info: CommandBufferInfo) -> Result, DriverError> { let cache_ref = self .command_buffer_cache .entry(info.queue_family_index) - .or_insert_with(PoolInfo::default_cache); - - let mut item = { - #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] - let mut cache = cache_ref.lock(); + .or_insert_with(PoolConfig::default_cache); - #[cfg(not(feature = "parking_lot"))] - let mut cache = cache.unwrap(); + let item = with_cache(cache_ref, lease_command_buffer) + .map(Ok) + .unwrap_or_else(|| { + debug!("Creating new {}", stringify!(CommandBuffer)); - lease_command_buffer(&mut cache) - } - .map(Ok) - .unwrap_or_else(|| { - debug!("Creating new {}", stringify!(CommandBuffer)); - - CommandBuffer::create(&self.device, info) - })?; + CommandBuffer::create(&self.device, info) + })?; // Drop anything we were holding from the last submission - CommandBuffer::drop_fenced(&mut item); + //item.wait_until_executed()?; Ok(Lease::new(Arc::downgrade(cache_ref), item)) } @@ -205,39 +214,43 @@ impl Pool for FifoPool { impl Pool for FifoPool { #[profiling::function] - fn lease(&mut self, info: DescriptorPoolInfo) -> Result, DriverError> { + fn resource(&mut self, info: DescriptorPoolInfo) -> Result, DriverError> { let cache_ref = Arc::downgrade(&self.descriptor_pool_cache); { profiling::scope!("check cache"); - #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] - let mut cache = self.descriptor_pool_cache.lock(); - - #[cfg(not(feature = "parking_lot"))] - let mut cache = cache.unwrap(); - - // Look for a compatible descriptor pool (has enough sets and descriptors) - for idx in 0..cache.len() { - let item = unsafe { cache.get_unchecked(idx) }; - if item.info.max_sets >= info.max_sets - && item.info.acceleration_structure_count >= info.acceleration_structure_count - && item.info.combined_image_sampler_count >= info.combined_image_sampler_count - && item.info.input_attachment_count >= info.input_attachment_count - && item.info.sampled_image_count >= info.sampled_image_count - && item.info.sampler_count >= info.sampler_count - && item.info.storage_buffer_count >= info.storage_buffer_count - && item.info.storage_buffer_dynamic_count >= info.storage_buffer_dynamic_count - && item.info.storage_image_count >= info.storage_image_count - && item.info.storage_texel_buffer_count >= info.storage_texel_buffer_count - && item.info.uniform_buffer_count >= info.uniform_buffer_count - && item.info.uniform_buffer_dynamic_count >= info.uniform_buffer_dynamic_count - && item.info.uniform_texel_buffer_count >= info.uniform_texel_buffer_count - { - let item = cache.swap_remove(idx); - - return Ok(Lease::new(cache_ref, item)); + if let Some(item) = with_cache(&self.descriptor_pool_cache, |cache| { + // Look for a compatible descriptor pool (has enough sets and descriptors) + for idx in 0..cache.len() { + let item = unsafe { cache.get_unchecked(idx) }; + if item.info.max_sets >= info.max_sets + && item.info.acceleration_structure_count + >= info.acceleration_structure_count + && item.info.combined_image_sampler_count + >= info.combined_image_sampler_count + && item.info.input_attachment_count >= info.input_attachment_count + && item.info.sampled_image_count >= info.sampled_image_count + && item.info.sampler_count >= info.sampler_count + && item.info.storage_buffer_count >= info.storage_buffer_count + && item.info.storage_buffer_dynamic_count + >= info.storage_buffer_dynamic_count + && item.info.storage_image_count >= info.storage_image_count + && item.info.storage_texel_buffer_count >= info.storage_texel_buffer_count + && item.info.uniform_buffer_count >= info.uniform_buffer_count + && item.info.uniform_buffer_dynamic_count + >= info.uniform_buffer_dynamic_count + && item.info.uniform_texel_buffer_count >= info.uniform_texel_buffer_count + { + let item = cache.swap_remove(idx); + + return Some(Lease::new(cache_ref.clone(), item)); + } } + + None + }) { + return Ok(item); } } @@ -251,38 +264,39 @@ impl Pool for FifoPool { impl Pool for FifoPool { #[profiling::function] - fn lease(&mut self, info: ImageInfo) -> Result, DriverError> { + fn resource(&mut self, info: ImageInfo) -> Result, DriverError> { let cache_ref = Arc::downgrade(&self.image_cache); { profiling::scope!("check cache"); - #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] - let mut cache = self.image_cache.lock(); - - #[cfg(not(feature = "parking_lot"))] - let mut cache = cache.unwrap(); - - // Look for a compatible image (same properties, superset of creation flags and usage - // flags) - for idx in 0..cache.len() { - let item = unsafe { cache.get_unchecked(idx) }; - if item.info.array_layer_count == info.array_layer_count - && item.info.depth == info.depth - && item.info.fmt == info.fmt - && item.info.height == info.height - && item.info.mip_level_count == info.mip_level_count - && item.info.sample_count == info.sample_count - && item.info.tiling == info.tiling - && item.info.ty == info.ty - && item.info.width == info.width - && item.info.flags.contains(info.flags) - && item.info.usage.contains(info.usage) - { - let item = cache.swap_remove(idx); - - return Ok(Lease::new(cache_ref, item)); + if let Some(item) = with_cache(&self.image_cache, |cache| { + // Look for a compatible image (same properties, superset of creation flags and + // usage flags) + for idx in 0..cache.len() { + let item = unsafe { cache.get_unchecked(idx) }; + if item.info.array_layer_count == info.array_layer_count + && item.info.dedicated == info.dedicated + && item.info.depth == info.depth + && item.info.fmt == info.fmt + && item.info.height == info.height + && item.info.mip_level_count == info.mip_level_count + && item.info.sample_count == info.sample_count + && item.info.tiling == info.tiling + && item.info.ty == info.ty + && item.info.width == info.width + && item.info.flags.contains(info.flags) + && item.info.usage.contains(info.usage) + { + let item = cache.swap_remove(idx); + + return Some(Lease::new(cache_ref.clone(), item)); + } } + + None + }) { + return Ok(item); } } @@ -296,30 +310,22 @@ impl Pool for FifoPool { impl Pool for FifoPool { #[profiling::function] - fn lease(&mut self, info: RenderPassInfo) -> Result, DriverError> { + fn resource(&mut self, info: RenderPassInfo) -> Result, DriverError> { let cache_ref = if let Some(cache) = self.render_pass_cache.get(&info) { cache } else { // We tried to get the cache first in order to avoid this clone self.render_pass_cache .entry(info.clone()) - .or_insert_with(PoolInfo::default_cache) + .or_insert_with(PoolConfig::default_cache) }; - let item = { - #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] - let mut cache = cache_ref.lock(); - - #[cfg(not(feature = "parking_lot"))] - let mut cache = cache.unwrap(); - - cache.pop() - } - .map(Ok) - .unwrap_or_else(|| { - debug!("Creating new {}", stringify!(RenderPass)); + let item = with_cache(cache_ref, |cache| cache.pop()) + .map(Ok) + .unwrap_or_else(|| { + debug!("Creating new {}", stringify!(RenderPass)); - RenderPass::create(&self.device, info) - })?; + RenderPass::create(&self.device, info) + })?; Ok(Lease::new(Arc::downgrade(cache_ref), item)) } diff --git a/src/pool/hash.rs b/src/pool/hash.rs index ed752c42..a4c14beb 100644 --- a/src/pool/hash.rs +++ b/src/pool/hash.rs @@ -1,14 +1,16 @@ -//! Pool which leases by exactly matching the information before creating new resources. +//! Pool which requests by exactly matching the information before creating new resources. use { - super::{Cache, Lease, Pool, PoolInfo, lease_command_buffer}, + super::{Cache, Lease, Pool, PoolConfig, lease_command_buffer}, crate::driver::{ - CommandBuffer, CommandBufferInfo, DescriptorPool, DescriptorPoolInfo, DriverError, - RenderPass, RenderPassInfo, + DriverError, accel_struct::{AccelerationStructure, AccelerationStructureInfo}, buffer::{Buffer, BufferInfo}, + cmd_buf::{CommandBuffer, CommandBufferInfo}, + descriptor_set::{DescriptorPool, DescriptorPoolInfo}, device::Device, image::{Image, ImageInfo}, + render_pass::{RenderPass, RenderPassInfo}, }, log::debug, paste::paste, @@ -25,39 +27,52 @@ use std::sync::Mutex; /// /// # Bucket Strategy /// -/// The information for each lease request is the key for a `HashMap` of buckets. If no bucket +/// The information for each resource request is the key for a `HashMap` of buckets. If no bucket /// exists with the exact information provided a new bucket is created. /// -/// In practice this means that for a [`PoolInfo::image_capacity`] of `4`, requests for a 1024x1024 -/// image with certain attributes will store a maximum of `4` such images. Requests for any image -/// having a different size or attributes will store an additional maximum of `4` images. +/// In practice this means that for a [`PoolConfig::image_capacity`] of `4`, requests for a +/// 1024x1024 image with certain attributes will store a maximum of `4` such images. Requests for +/// any image having a different size or attributes will store an additional maximum of `4` images. /// /// # Memory Management /// /// If requests for varying resources is common [`HashPool::clear_images_by_info`] and other memory /// management functions are nessecery in order to avoid using all available device memory. #[derive(Debug)] +#[read_only::cast] pub struct HashPool { acceleration_structure_cache: HashMap>, buffer_cache: HashMap>, command_buffer_cache: HashMap>, descriptor_pool_cache: HashMap>, - device: Arc, + + /// The device which owns this pool. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub device: Device, + image_cache: HashMap>, - info: PoolInfo, + + /// Information used to create this pool. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub info: PoolConfig, + render_pass_cache: HashMap>, } impl HashPool { /// Constructs a new `HashPool`. - pub fn new(device: &Arc) -> Self { - Self::with_capacity(device, PoolInfo::default()) + pub fn new(device: &Device) -> Self { + Self::with_capacity(device, PoolConfig::default()) } /// Constructs a new `HashPool` with the given capacity information. - pub fn with_capacity(device: &Arc, info: impl Into) -> Self { - let info: PoolInfo = info.into(); - let device = Arc::clone(device); + pub fn with_capacity(device: &Device, info: impl Into) -> Self { + let info: PoolConfig = info.into(); + let device = device.clone(); Self { acceleration_structure_cache: Default::default(), @@ -128,17 +143,17 @@ resource_mgmt_fns!("images", "image", ImageInfo, image_cache); impl Pool for HashPool { #[profiling::function] - fn lease(&mut self, info: CommandBufferInfo) -> Result, DriverError> { + fn resource(&mut self, info: CommandBufferInfo) -> Result, DriverError> { let cache_ref = self .command_buffer_cache .entry(info.queue_family_index) - .or_insert_with(PoolInfo::default_cache); - let mut item = { + .or_insert_with(PoolConfig::default_cache); + let item = { #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] let mut cache = cache_ref.lock(); #[cfg(not(feature = "parking_lot"))] - let mut cache = cache.unwrap(); + let mut cache = cache.expect("poisoned cache lock"); lease_command_buffer(&mut cache) } @@ -150,7 +165,7 @@ impl Pool for HashPool { })?; // Drop anything we were holding from the last submission - CommandBuffer::drop_fenced(&mut item); + //item.wait_until_executed()?; Ok(Lease::new(Arc::downgrade(cache_ref), item)) } @@ -158,17 +173,17 @@ impl Pool for HashPool { impl Pool for HashPool { #[profiling::function] - fn lease(&mut self, info: DescriptorPoolInfo) -> Result, DriverError> { + fn resource(&mut self, info: DescriptorPoolInfo) -> Result, DriverError> { let cache_ref = self .descriptor_pool_cache .entry(info.clone()) - .or_insert_with(PoolInfo::default_cache); + .or_insert_with(PoolConfig::default_cache); let item = { #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] let mut cache = cache_ref.lock(); #[cfg(not(feature = "parking_lot"))] - let mut cache = cache.unwrap(); + let mut cache = cache.expect("poisoned cache lock"); cache.pop() } @@ -185,21 +200,21 @@ impl Pool for HashPool { impl Pool for HashPool { #[profiling::function] - fn lease(&mut self, info: RenderPassInfo) -> Result, DriverError> { + fn resource(&mut self, info: RenderPassInfo) -> Result, DriverError> { let cache_ref = if let Some(cache) = self.render_pass_cache.get(&info) { cache } else { // We tried to get the cache first in order to avoid this clone self.render_pass_cache .entry(info.clone()) - .or_insert_with(PoolInfo::default_cache) + .or_insert_with(PoolConfig::default_cache) }; let item = { #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] let mut cache = cache_ref.lock(); #[cfg(not(feature = "parking_lot"))] - let mut cache = cache.unwrap(); + let mut cache = cache.expect("poisoned cache lock"); cache.pop() } @@ -214,13 +229,13 @@ impl Pool for HashPool { } } -// Enable leasing items using their basic info +// Enable requesting items using their basic info macro_rules! lease { ($info:ident => $item:ident, $capacity:ident) => { paste::paste! { impl Pool<$info, $item> for HashPool { #[profiling::function] - fn lease(&mut self, info: $info) -> Result, DriverError> { + fn resource(&mut self, info: $info) -> Result, DriverError> { let cache_ref = self.[<$item:snake _cache>].entry(info) .or_insert_with(|| { Cache::new(Mutex::new(Vec::with_capacity(self.info.$capacity))) @@ -230,7 +245,7 @@ macro_rules! lease { let mut cache = cache_ref.lock(); #[cfg(not(feature = "parking_lot"))] - let mut cache = cache.unwrap(); + let mut cache = cache.expect("poisoned cache lock"); cache.pop() } diff --git a/src/pool/lazy.rs b/src/pool/lazy.rs index 0c9f8c09..002b46ea 100644 --- a/src/pool/lazy.rs +++ b/src/pool/lazy.rs @@ -1,14 +1,16 @@ -//! Pool which leases by looking for compatibile information before creating new resources. +//! Pool which requests by looking for compatibile information before creating new resources. use { - super::{Cache, Lease, Pool, PoolInfo, lease_command_buffer}, + super::{Cache, Lease, Pool, PoolConfig, lease_command_buffer}, crate::driver::{ - CommandBuffer, CommandBufferInfo, DescriptorPool, DescriptorPoolInfo, DriverError, - RenderPass, RenderPassInfo, + DriverError, accel_struct::{AccelerationStructure, AccelerationStructureInfo}, buffer::{Buffer, BufferInfo}, + cmd_buf::{CommandBuffer, CommandBufferInfo}, + descriptor_set::{DescriptorPool, DescriptorPoolInfo}, device::Device, image::{Image, ImageInfo, SampleCount}, + render_pass::{RenderPass, RenderPassInfo}, }, ash::vk, log::debug, @@ -46,7 +48,7 @@ impl From for ImageKey { /// A balanced resource allocator. /// -/// The information for each lease request is compared against the stored resources for +/// The information for each resource request is compared against the stored resources for /// compatibility. If no acceptable resources are stored for the information provided a new resource /// is created and returned. /// @@ -58,46 +60,59 @@ impl From for ImageKey { /// /// # Bucket Strategy /// -/// The information for each lease request is the key for a `HashMap` of buckets. If no bucket +/// The information for each resource request is the key for a `HashMap` of buckets. If no bucket /// exists with compatible information a new bucket is created. /// -/// In practice this means that for a [`PoolInfo::image_capacity`] of `4`, requests for a 1024x1024 -/// image with certain attributes will store a maximum of `4` such images. Requests for any image -/// having a different size or incompatible attributes will store an additional maximum of `4` -/// images. +/// In practice this means that for a [`PoolConfig::image_capacity`] of `4`, requests for a +/// 1024x1024 image with certain attributes will store a maximum of `4` such images. Requests for +/// any image having a different size or incompatible attributes will store an additional maximum of +/// `4` images. /// /// # Memory Management /// /// If requests for varying resources is common [`LazyPool::clear_images_by_info`] and other memory /// management functions are nessecery in order to avoid using all available device memory. #[derive(Debug)] +#[read_only::cast] pub struct LazyPool { accel_struct_cache: HashMap>, buffer_cache: HashMap<(bool, vk::DeviceSize), Cache>, command_buffer_cache: HashMap>, descriptor_pool_cache: Cache, - device: Arc, + + /// The device which owns this pool. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub device: Device, + image_cache: HashMap>, - info: PoolInfo, + + /// Information used to create this pool. + /// + /// _Note:_ This field is read-only. + #[readonly] + pub info: PoolConfig, + render_pass_cache: HashMap>, } impl LazyPool { /// Constructs a new `LazyPool`. - pub fn new(device: &Arc) -> Self { - Self::with_capacity(device, PoolInfo::default()) + pub fn new(device: &Device) -> Self { + Self::with_capacity(device, PoolConfig::default()) } /// Constructs a new `LazyPool` with the given capacity information. - pub fn with_capacity(device: &Arc, info: impl Into) -> Self { - let info: PoolInfo = info.into(); - let device = Arc::clone(device); + pub fn with_capacity(device: &Device, info: impl Into) -> Self { + let info: PoolConfig = info.into(); + let device = device.clone(); Self { accel_struct_cache: Default::default(), buffer_cache: Default::default(), command_buffer_cache: Default::default(), - descriptor_pool_cache: PoolInfo::default_cache(), + descriptor_pool_cache: PoolConfig::default_cache(), device, image_cache: Default::default(), info, @@ -158,14 +173,14 @@ impl LazyPool { impl Pool for LazyPool { #[profiling::function] - fn lease( + fn resource( &mut self, info: AccelerationStructureInfo, ) -> Result, DriverError> { let cache = self .accel_struct_cache .entry(info.ty) - .or_insert_with(|| PoolInfo::explicit_cache(self.info.accel_struct_capacity)); + .or_insert_with(|| PoolConfig::explicit_cache(self.info.accel_struct_capacity)); let cache_ref = Arc::downgrade(cache); { @@ -175,7 +190,7 @@ impl Pool for LazyPool { let mut cache = cache.lock(); #[cfg(not(feature = "parking_lot"))] - let mut cache = cache.unwrap(); + let mut cache = cache.expect("poisoned cache lock"); // Look for a compatible acceleration structure (big enough) for idx in 0..cache.len() { @@ -198,11 +213,11 @@ impl Pool for LazyPool { impl Pool for LazyPool { #[profiling::function] - fn lease(&mut self, info: BufferInfo) -> Result, DriverError> { + fn resource(&mut self, info: BufferInfo) -> Result, DriverError> { let cache = self .buffer_cache - .entry((info.mappable, info.alignment)) - .or_insert_with(|| PoolInfo::explicit_cache(self.info.buffer_capacity)); + .entry((info.host_read | info.host_write, info.alignment)) + .or_insert_with(|| PoolConfig::explicit_cache(self.info.buffer_capacity)); let cache_ref = Arc::downgrade(cache); { @@ -212,12 +227,18 @@ impl Pool for LazyPool { let mut cache = cache.lock(); #[cfg(not(feature = "parking_lot"))] - let mut cache = cache.unwrap(); + let mut cache = cache.expect("poisoned cache lock"); // Look for a compatible buffer (big enough and superset of usage flags) for idx in 0..cache.len() { let item = unsafe { cache.get_unchecked(idx) }; - if item.info.size >= info.size && item.info.usage.contains(info.usage) { + if (item.info.dedicated & info.dedicated) == info.dedicated + && (item.info.host_read & info.host_read) == info.host_read + && (item.info.host_write & info.host_write) == info.host_write + && item.info.alignment >= info.alignment + && item.info.size >= info.size + && item.info.usage.contains(info.usage) + { let item = cache.swap_remove(idx); return Ok(Lease::new(cache_ref, item)); @@ -235,17 +256,17 @@ impl Pool for LazyPool { impl Pool for LazyPool { #[profiling::function] - fn lease(&mut self, info: CommandBufferInfo) -> Result, DriverError> { + fn resource(&mut self, info: CommandBufferInfo) -> Result, DriverError> { let cache_ref = self .command_buffer_cache .entry(info.queue_family_index) - .or_insert_with(PoolInfo::default_cache); - let mut item = { + .or_insert_with(PoolConfig::default_cache); + let item = { #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] let mut cache = cache_ref.lock(); #[cfg(not(feature = "parking_lot"))] - let mut cache = cache.unwrap(); + let mut cache = cache.expect("poisoned cache lock"); lease_command_buffer(&mut cache) } @@ -257,7 +278,7 @@ impl Pool for LazyPool { })?; // Drop anything we were holding from the last submission - CommandBuffer::drop_fenced(&mut item); + //item.wait_until_executed()?; Ok(Lease::new(Arc::downgrade(cache_ref), item)) } @@ -265,7 +286,7 @@ impl Pool for LazyPool { impl Pool for LazyPool { #[profiling::function] - fn lease(&mut self, info: DescriptorPoolInfo) -> Result, DriverError> { + fn resource(&mut self, info: DescriptorPoolInfo) -> Result, DriverError> { let cache_ref = Arc::downgrade(&self.descriptor_pool_cache); { @@ -275,7 +296,7 @@ impl Pool for LazyPool { let mut cache = self.descriptor_pool_cache.lock(); #[cfg(not(feature = "parking_lot"))] - let mut cache = cache.unwrap(); + let mut cache = cache.expect("poisoned cache lock"); // Look for a compatible descriptor pool (has enough sets and descriptors) for idx in 0..cache.len() { @@ -311,11 +332,11 @@ impl Pool for LazyPool { impl Pool for LazyPool { #[profiling::function] - fn lease(&mut self, info: ImageInfo) -> Result, DriverError> { + fn resource(&mut self, info: ImageInfo) -> Result, DriverError> { let cache = self .image_cache .entry(info.into()) - .or_insert_with(|| PoolInfo::explicit_cache(self.info.image_capacity)); + .or_insert_with(|| PoolConfig::explicit_cache(self.info.image_capacity)); let cache_ref = Arc::downgrade(cache); { @@ -325,7 +346,7 @@ impl Pool for LazyPool { let mut cache = cache.lock(); #[cfg(not(feature = "parking_lot"))] - let mut cache = cache.unwrap(); + let mut cache = cache.expect("poisoned cache lock"); // Look for a compatible image (superset of creation flags and usage flags) for idx in 0..cache.len() { @@ -348,21 +369,21 @@ impl Pool for LazyPool { impl Pool for LazyPool { #[profiling::function] - fn lease(&mut self, info: RenderPassInfo) -> Result, DriverError> { + fn resource(&mut self, info: RenderPassInfo) -> Result, DriverError> { let cache_ref = if let Some(cache) = self.render_pass_cache.get(&info) { cache } else { // We tried to get the cache first in order to avoid this clone self.render_pass_cache .entry(info.clone()) - .or_insert_with(PoolInfo::default_cache) + .or_insert_with(PoolConfig::default_cache) }; let item = { #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] let mut cache = cache_ref.lock(); #[cfg(not(feature = "parking_lot"))] - let mut cache = cache.unwrap(); + let mut cache = cache.expect("poisoned cache lock"); cache.pop() } diff --git a/src/pool/mod.rs b/src/pool/mod.rs index e5d82959..f3662647 100644 --- a/src/pool/mod.rs +++ b/src/pool/mod.rs @@ -1,11 +1,11 @@ -//! Resource leasing and pooling types. +//! Resource pooling, requesting, and caching types. //! -//! _Screen 13_ provides caching for acceleration structure, buffer and image resources which may be -//! leased from configurable pools using their corresponding information structure. Most programs -//! will do fine with a single [`FifoPool`](self::fifo::FifoPool). +//! Resource pools provide caching for buffer, image, and acceleration structure resources. Pooled +//! resources may be requested from a pool using their corresponding information structure. //! -//! Leased resources may be bound directly to a render graph and used in the same manner as regular -//! resources. After rendering has finished, the leased resources will return to the pool for reuse. +//! Leased resources may be bound directly to a [`Graph`](crate::Graph) and used in the same manner +//! as regular resources. After execution has completed pooled resources are automatically returned +//! to their pool for reuse. //! //! # Buckets //! @@ -13,7 +13,7 @@ //! offering a different strategy which balances performance (_more buckets_) with memory efficiency //! (_fewer buckets_). //! -//! _Screen 13_'s pools can be grouped into two major categories: +//! _vk-graph_'s pools can be grouped into two major categories: //! //! * Single-bucket: [`FifoPool`](self::fifo::FifoPool) //! * Multi-bucket: [`LazyPool`](self::lazy::LazyPool), [`HashPool`](self::hash::HashPool) @@ -25,17 +25,17 @@ //! ```no_run //! # use std::sync::Arc; //! # use ash::vk; -//! # use screen_13::driver::DriverError; -//! # use screen_13::driver::device::{Device, DeviceInfo}; -//! # use screen_13::driver::image::{ImageInfo}; -//! # use screen_13::pool::{Pool}; -//! # use screen_13::pool::lazy::{LazyPool}; +//! # use vk_graph::driver::DriverError; +//! # use vk_graph::driver::device::{Device, DeviceInfo}; +//! # use vk_graph::driver::image::{ImageInfo}; +//! # use vk_graph::pool::{Pool}; +//! # use vk_graph::pool::lazy::{LazyPool}; //! # fn main() -> Result<(), DriverError> { -//! # let device = Arc::new(Device::create_headless(DeviceInfo::default())?); +//! # let device = Device::new(DeviceInfo::default())?; //! let mut pool = LazyPool::new(&device); //! //! let info = ImageInfo::image_2d(8, 8, vk::Format::R8G8B8A8_UNORM, vk::ImageUsageFlags::STORAGE); -//! let my_image = pool.lease(info)?; +//! let my_image = pool.resource(info)?; //! //! assert!(my_image.info.usage.contains(vk::ImageUsageFlags::STORAGE)); //! # Ok(()) } @@ -57,20 +57,19 @@ //! * High performance is most important //! * Resources have consistent attributes each frame //! -//! # When Should You Use Resource Aliasing? +//! # When Should You Use Resource Caching? //! -//! Wrapping any pool using [`AliasPool::new`](self::alias::AliasPool::new) enables resource -//! aliasing, which prevents excess resources from being created even when different parts of your -//! code request new resources. +//! Wrapping any pool using [`cache::Cache::new`] enables resource caching, which prevents excess +//! resources from being created even when different parts of your code request compatible +//! resources. //! -//! **_NOTE:_** Render graph submission will automatically attempt to re-order submitted passes to +//! **_NOTE:_** Graph submission will automatically attempt to re-order submitted commands to //! reduce contention between individual resources. //! -//! **_NOTE:_** In cases where multiple aliased resources using identical request information are -//! used in the same render graph pass you must ensure the resources are aliased from different -//! pools. There is currently no tagging or filter which would prevent "ping-pong" rendering of such -//! resources from being the same actual resources; this causes Vulkan validation warnings when -//! reading from and writing to the same images, or whatever your operations may be. +//! **_NOTE:_** In cases where multiple cached resources using identical request information are +//! used in the same graph command, ensure they come from different cache tags or different pool +//! wrappers. Otherwise, two requests may resolve to the same underlying resource and trigger +//! Vulkan validation warnings when reading from and writing to the same images. //! //! ### Pros: //! @@ -80,22 +79,23 @@ //! //! ### Cons: //! -//! * Non-zero cost: Atomic load and compatibility check per active alias +//! * Non-zero cost: atomic load and compatibility check per active cached resource //! * May cause GPU stalling if there is not enough work being submitted -//! * Aliased resources are typed `Arc>` and are not guaranteed to be mutable or unique +//! * Cached resources are typed `Arc>` and are not guaranteed to be mutable or unique -pub mod alias; +pub mod cache; pub mod fifo; pub mod hash; pub mod lazy; use { crate::driver::{ - CommandBuffer, DriverError, + DriverError, accel_struct::{ AccelerationStructure, AccelerationStructureInfo, AccelerationStructureInfoBuilder, }, buffer::{Buffer, BufferInfo, BufferInfoBuilder}, + cmd_buf::CommandBuffer, image::{Image, ImageInfo, ImageInfoBuilder}, }, derive_builder::{Builder, UninitializedFieldError}, @@ -120,14 +120,11 @@ type CacheRef = Weak>>; fn lease_command_buffer(cache: &mut Vec) -> Option { for idx in 0..cache.len() { if unsafe { - let cmd_buf = cache.get_unchecked(idx); + let cmd = cache.get_unchecked(idx); // Don't lease this command buffer if it is unsignalled; we'll create a new one // and wait for this, and those behind it, to signal. - cmd_buf - .device - .get_fence_status(cmd_buf.fence) - .unwrap_or_default() + cmd.device.get_fence_status(cmd.fence).unwrap_or_default() } { return Some(cache.swap_remove(idx)); } @@ -136,10 +133,21 @@ fn lease_command_buffer(cache: &mut Vec) -> Option None } -/// Holds a leased resource and implements `Drop` in order to return the resource. +fn with_cache(cache: &Cache, f: impl FnOnce(&mut Vec) -> R) -> R { + let cache = cache.lock(); + + #[cfg(not(feature = "parking_lot"))] + let cache = cache.expect("poisoned cache lock"); + + let mut cache = cache; + + f(&mut cache) +} + +/// Holds a pooled resource and implements `Drop` in order to return the resource. /// /// This simple wrapper type implements only the `AsRef`, `AsMut`, `Deref` and `DerefMut` traits -/// and provides no other functionality. A freshly leased resource is guaranteed to have no other +/// and provides no other functionality. A freshly obtained resource is guaranteed to have no other /// owners and may be mutably accessed. #[derive(Debug)] pub struct Lease { @@ -147,25 +155,42 @@ pub struct Lease { item: ManuallyDrop, } -impl Lease { - #[inline(always)] - fn new(cache_ref: CacheRef, item: T) -> Self { - Self { - cache_ref, - item: ManuallyDrop::new(item), - } +// The following debug_name functions take a self of Lease and return Self. +// This allows pooled resources to have the same `.debug_name("bugs")` chaining + +impl Lease { + /// Sets the debugging name assigned to this acceleration structure. + pub fn debug_name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + + self } } -impl AsRef for Lease { - fn as_ref(&self) -> &T { - &self.item +impl Lease { + /// Sets the debugging name assigned to this buffer. + pub fn debug_name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + + self } } -impl AsMut for Lease { - fn as_mut(&mut self) -> &mut T { - &mut self.item +impl Lease { + /// Sets the debugging name assigned to this image. + pub fn debug_name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + + self + } +} + +impl Lease { + fn new(cache_ref: CacheRef, item: T) -> Self { + Self { + cache_ref, + item: ManuallyDrop::new(item), + } } } @@ -193,17 +218,13 @@ impl Drop for Lease { // If the pool cache has been dropped we must manually drop the item, otherwise it goes back // into the pool. if let Some(cache) = self.cache_ref.upgrade() { - #[cfg_attr(not(feature = "parking_lot"), allow(unused_mut))] - let mut cache = cache.lock(); - - #[cfg(not(feature = "parking_lot"))] - let mut cache = cache.unwrap(); - - if cache.len() == cache.capacity() { - cache.pop(); - } + with_cache(&cache, |cache| { + if cache.len() >= cache.capacity() { + cache.pop(); + } - cache.push(unsafe { ManuallyDrop::take(&mut self.item) }); + cache.push(unsafe { ManuallyDrop::take(&mut self.item) }); + }); } else { unsafe { ManuallyDrop::drop(&mut self.item); @@ -212,21 +233,30 @@ impl Drop for Lease { } } -/// Allows leasing of resources using driver information structures. +/// Allows requesting resources using driver information structures. pub trait Pool { - /// Lease a resource. - fn lease(&mut self, info: I) -> Result, DriverError>; + #[deprecated = "use resource function"] + #[doc(hidden)] + fn lease(&mut self, info: I) -> Result, DriverError> { + self.resource(info) + } + + /// Request a resource. + fn resource(&mut self, info: I) -> Result, DriverError>; } -// Enable leasing items using their info builder type for convenience +// Enable requesting items using their info builder type for convenience macro_rules! lease_builder { ($info:ident => $item:ident) => { paste::paste! { impl Pool<[<$info Builder>], $item> for T where T: Pool<$info, $item> { - fn lease(&mut self, builder: [<$info Builder>]) -> Result, DriverError> { + fn resource( + &mut self, + builder: [<$info Builder>], + ) -> Result, DriverError> { let info = builder.build(); - self.lease(info) + self.resource(info) } } } @@ -239,60 +269,63 @@ lease_builder!(ImageInfo => Image); /// Information used to create a [`FifoPool`](self::fifo::FifoPool), /// [`HashPool`](self::hash::HashPool) or [`LazyPool`](self::lazy::LazyPool) instance. -#[derive(Builder, Clone, Copy, Debug)] +#[derive(Builder, Clone, Copy, Debug, Eq, PartialEq)] #[builder( - build_fn(private, name = "fallible_build", error = "PoolInfoBuilderError"), + build_fn(private, name = "fallible_build", error = "UninitializedFieldError"), derive(Clone, Copy, Debug), pattern = "owned" )] -#[non_exhaustive] -pub struct PoolInfo { +pub struct PoolConfig { /// The maximum size of a single bucket of acceleration structure resource instances. The - /// default value is [`PoolInfo::DEFAULT_RESOURCE_CAPACITY`]. + /// default value is [`PoolConfig::DEFAULT_RESOURCE_CAPACITY`]. /// /// # Note /// - /// Individual [`Pool`] implementations store varying numbers of buckets. Read the documentation - /// of each implementation to understand how this affects total number of stored acceleration - /// structure instances. - #[builder(default = "PoolInfo::DEFAULT_RESOURCE_CAPACITY", setter(strip_option))] + /// Individual [`Pool`] implementations store varying numbers of buckets. Read the + /// documentation of each implementation to understand how this affects total number of + /// stored acceleration structure instances. + #[builder( + default = "PoolConfig::DEFAULT_RESOURCE_CAPACITY", + setter(strip_option) + )] pub accel_struct_capacity: usize, /// The maximum size of a single bucket of buffer resource instances. The default value is - /// [`PoolInfo::DEFAULT_RESOURCE_CAPACITY`]. + /// [`PoolConfig::DEFAULT_RESOURCE_CAPACITY`]. /// /// # Note /// - /// Individual [`Pool`] implementations store varying numbers of buckets. Read the documentation - /// of each implementation to understand how this affects total number of stored buffer - /// instances. - #[builder(default = "PoolInfo::DEFAULT_RESOURCE_CAPACITY", setter(strip_option))] + /// Individual [`Pool`] implementations store varying numbers of buckets. Read the + /// documentation of each implementation to understand how this affects total number of + /// stored buffer instances. + #[builder( + default = "PoolConfig::DEFAULT_RESOURCE_CAPACITY", + setter(strip_option) + )] pub buffer_capacity: usize, /// The maximum size of a single bucket of image resource instances. The default value is - /// [`PoolInfo::DEFAULT_RESOURCE_CAPACITY`]. + /// [`PoolConfig::DEFAULT_RESOURCE_CAPACITY`]. /// /// # Note /// - /// Individual [`Pool`] implementations store varying numbers of buckets. Read the documentation - /// of each implementation to understand how this affects total number of stored image - /// instances. - #[builder(default = "PoolInfo::DEFAULT_RESOURCE_CAPACITY", setter(strip_option))] + /// Individual [`Pool`] implementations store varying numbers of buckets. Read the + /// documentation of each implementation to understand how this affects total number of + /// stored image instances. + #[builder( + default = "PoolConfig::DEFAULT_RESOURCE_CAPACITY", + setter(strip_option) + )] pub image_capacity: usize, } -impl PoolInfo { +impl PoolConfig { /// The maximum size of a single bucket of resource instances. pub const DEFAULT_RESOURCE_CAPACITY: usize = 16; - /// Constructs a new `PoolInfo` with the given acceleration structure, buffer and image resource - /// capacity for any single bucket. - pub const fn with_capacity(resource_capacity: usize) -> Self { - Self { - accel_struct_capacity: resource_capacity, - buffer_capacity: resource_capacity, - image_capacity: resource_capacity, - } + /// Creates a default `PoolConfigBuilder`. + pub fn builder() -> PoolConfigBuilder { + Default::default() } fn default_cache() -> Cache { @@ -304,21 +337,46 @@ impl PoolInfo { fn explicit_cache(capacity: usize) -> Cache { Cache::new(Mutex::new(Vec::with_capacity(capacity))) } + + /// Converts a `PoolConfig` into a `PoolConfigBuilder`. + pub fn into_builder(self) -> PoolConfigBuilder { + PoolConfigBuilder { + accel_struct_capacity: Some(self.accel_struct_capacity), + buffer_capacity: Some(self.buffer_capacity), + image_capacity: Some(self.image_capacity), + } + } + + #[deprecated = "use into_builder function"] + #[doc(hidden)] + pub fn to_builder(self) -> PoolConfigBuilder { + self.into_builder() + } + + /// Constructs a new `PoolConfig` with the given acceleration structure, buffer and image + /// resource capacity for any single bucket. + pub const fn with_capacity(resource_capacity: usize) -> Self { + Self { + accel_struct_capacity: resource_capacity, + buffer_capacity: resource_capacity, + image_capacity: resource_capacity, + } + } } -impl Default for PoolInfo { +impl Default for PoolConfig { fn default() -> Self { - PoolInfoBuilder::default().into() + PoolConfigBuilder::default().into() } } -impl From for PoolInfo { - fn from(info: PoolInfoBuilder) -> Self { +impl From for PoolConfig { + fn from(info: PoolConfigBuilder) -> Self { info.build() } } -impl From for PoolInfo { +impl From for PoolConfig { fn from(value: usize) -> Self { Self { accel_struct_capacity: value, @@ -329,19 +387,84 @@ impl From for PoolInfo { } // HACK: https://github.com/colin-kiegel/rust-derive-builder/issues/56 -impl PoolInfoBuilder { - /// Builds a new `PoolInfo`. - pub fn build(self) -> PoolInfo { - self.fallible_build() - .expect("All required fields set at initialization") +impl PoolConfigBuilder { + /// Builds a new `PoolConfig`. + pub fn build(self) -> PoolConfig { + self.fallible_build().expect("invalid pool config") } } -#[derive(Debug)] -struct PoolInfoBuilderError; +#[doc(hidden)] +#[deprecated = "use PoolConfig instead"] +pub type PoolInfo = PoolConfig; + +#[doc(hidden)] +#[deprecated = "use PoolConfigBuilder instead"] +pub type PoolInfoBuilder = PoolConfigBuilder; + +mod deprecated { + use { + crate::pool::Lease, + std::convert::{AsMut, AsRef}, + }; + + impl Lease { + #[allow(clippy::should_implement_trait)] + #[deprecated = "use Deref impl"] + #[doc(hidden)] + pub fn as_ref(&self) -> &T { + &self.item + } + + #[allow(clippy::should_implement_trait)] + #[deprecated = "use DerefMut impl"] + #[doc(hidden)] + pub fn as_mut(&mut self) -> &mut T { + &mut self.item + } + } + + impl AsRef for Lease { + fn as_ref(&self) -> &T { + &self.item + } + } + + impl AsMut for Lease { + fn as_mut(&mut self) -> &mut T { + &mut self.item + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + type Info = PoolConfig; + type Builder = PoolConfigBuilder; + + #[test] + pub fn pool_info() { + let info = Info::default(); + let builder = info.into_builder().build(); + + assert_eq!(info, builder); + } -impl From for PoolInfoBuilderError { - fn from(_: UninitializedFieldError) -> Self { - Self + #[test] + pub fn pool_info_builder() { + let info = Info { + accel_struct_capacity: 1, + buffer_capacity: 2, + image_capacity: 3, + }; + let builder = Builder::default() + .accel_struct_capacity(1) + .buffer_capacity(2) + .image_capacity(3) + .build(); + + assert_eq!(info, builder); } } diff --git a/src/graph/resolver.rs b/src/submission.rs similarity index 80% rename from src/graph/resolver.rs rename to src/submission.rs index d54952e6..9f831cd1 100644 --- a/src/graph/resolver.rs +++ b/src/submission.rs @@ -1,43 +1,54 @@ use { super::{ - Area, Attachment, Binding, Bindings, ExecutionPipeline, Node, NodeIndex, Pass, RenderGraph, - node::SwapchainImageNode, - pass_ref::{Subresource, SubresourceAccess}, + AnyResource, Attachment, CommandData, ExecutionPipeline, Graph, Node, NodeIndex, + cmd::{SubresourceAccess, SubresourceRange}, }, crate::{ driver::{ - AttachmentInfo, AttachmentRef, CommandBuffer, CommandBufferInfo, Descriptor, - DescriptorInfo, DescriptorPool, DescriptorPoolInfo, DescriptorSet, DriverError, - FramebufferAttachmentImageInfo, FramebufferInfo, RenderPass, RenderPassInfo, - SubpassDependency, SubpassInfo, + AttachmentInfo, AttachmentRef, Descriptor, DescriptorInfo, DescriptorSet, DriverError, + FramebufferAttachmentImageInfo, FramebufferInfo, SubpassDependency, SubpassInfo, accel_struct::AccelerationStructure, buffer::Buffer, + cmd_buf::{CommandBuffer, CommandBufferInfo}, + descriptor_set::{DescriptorPool, DescriptorPoolInfo}, + device::Device, format_aspect_mask, - graphic::{DepthStencilMode, GraphicPipeline}, + graphic::{DepthStencilInfo, GraphicPipeline}, image::{Image, ImageAccess}, - image_access_layout, initial_image_layout_access, is_read_access, is_write_access, + initial_image_layout_access, is_read_access, is_write_access, pipeline_stage_access_flags, - swapchain::SwapchainImage, + render_pass::{RenderPass, RenderPassInfo}, }, pool::{Lease, Pool}, }, ash::vk, log::{ Level::{Debug, Trace}, - debug, log_enabled, trace, + debug, log_enabled, trace, warn, }, std::{ cell::RefCell, collections::{BTreeMap, HashMap, VecDeque}, iter::repeat_n, ops::Range, + slice, + }, + vk_sync::{ + AccessType, BufferBarrier, GlobalBarrier, ImageBarrier, ImageLayout, cmd::pipeline_barrier, }, - vk_sync::{AccessType, BufferBarrier, GlobalBarrier, ImageBarrier, cmd::pipeline_barrier}, }; #[cfg(not(debug_assertions))] use std::hint::unreachable_unchecked; +const fn image_access_layout(access: AccessType) -> ImageLayout { + if matches!(access, AccessType::Present | AccessType::ComputeShaderWrite) { + ImageLayout::General + } else { + ImageLayout::Optimal + } +} + #[derive(Default)] struct AccessCache { accesses: Vec, @@ -88,8 +99,8 @@ impl AccessCache { .flat_map(move |node_idx| self.dependent_passes(node_idx, end_pass_idx)) } - fn update(&mut self, graph: &RenderGraph, end_pass_idx: usize) { - self.binding_count = graph.bindings.len(); + fn update(&mut self, graph: &Graph, end_pass_idx: usize) { + self.binding_count = graph.resources.len(); let cache_len = self.binding_count * end_pass_idx; @@ -112,7 +123,7 @@ impl AccessCache { nodes.fill(true); nodes.resize(self.binding_count, true); - for (pass_idx, pass) in graph.passes[0..end_pass_idx].iter().enumerate() { + for (pass_idx, pass) in graph.cmds[0..end_pass_idx].iter().enumerate() { let pass_start = pass_idx * self.binding_count; let mut read_count = 0; @@ -120,7 +131,9 @@ impl AccessCache { { self.accesses[pass_start + node_idx] = true; - if nodes[node_idx] && is_read_access(accesses.first().unwrap().access) { + if nodes[node_idx] + && is_read_access(accesses.first().expect("missing resource access").access) + { self.reads[pass_start + read_count] = node_idx; nodes[node_idx] = false; read_count += 1; @@ -162,6 +175,15 @@ struct PhysicalPass { render_pass: Option>, } +impl PhysicalPass { + /// # Panics + /// + /// Panics if the physical pass has no render pass. + fn expect_render_pass_mut(&mut self) -> &mut Lease { + self.render_pass.as_mut().expect("missing render pass") + } +} + impl Drop for PhysicalPass { fn drop(&mut self) { self.exec_descriptor_sets.clear(); @@ -169,19 +191,21 @@ impl Drop for PhysicalPass { } } -/// A structure which can read and execute render graphs. This pattern was derived from: +/// A structure which can optimize and submit [`Graph`] instances. +/// +/// This pattern was derived from: /// /// /// #[derive(Debug)] -pub struct Resolver { - pub(super) graph: RenderGraph, +pub struct Submission { + graph: Graph, physical_passes: Vec, } -impl Resolver { - pub(super) fn new(graph: RenderGraph) -> Self { - let physical_passes = Vec::with_capacity(graph.passes.len()); +impl Submission { + pub(super) fn new(graph: Graph) -> Self { + let physical_passes = Vec::with_capacity(graph.cmds.len()); Self { graph, @@ -190,8 +214,8 @@ impl Resolver { } #[profiling::function] - fn allow_merge_passes(lhs: &Pass, rhs: &Pass) -> bool { - fn first_graphic_pipeline(pass: &Pass) -> Option<&GraphicPipeline> { + fn allow_merge_passes(lhs: &CommandData, rhs: &CommandData) -> bool { + fn first_graphic_pipeline(pass: &CommandData) -> Option<&GraphicPipeline> { pass.execs .first() .and_then(|exec| exec.pipeline.as_ref().map(ExecutionPipeline::as_graphic)) @@ -204,14 +228,14 @@ impl Resolver { let lhs_pipeline = first_graphic_pipeline(lhs); if lhs_pipeline.is_none() { - trace!(" {} is not graphic", lhs.name,); + trace!(" {} is not graphic", lhs.name()); return false; } let rhs_pipeline = first_graphic_pipeline(rhs); if rhs_pipeline.is_none() { - trace!(" {} is not graphic", rhs.name,); + trace!(" {} is not graphic", rhs.name()); return false; } @@ -220,11 +244,13 @@ impl Resolver { let rhs_pipeline = unsafe { rhs_pipeline.unwrap_unchecked() }; // Must be same general rasterization modes - if lhs_pipeline.info.blend != rhs_pipeline.info.blend - || lhs_pipeline.info.cull_mode != rhs_pipeline.info.cull_mode - || lhs_pipeline.info.front_face != rhs_pipeline.info.front_face - || lhs_pipeline.info.polygon_mode != rhs_pipeline.info.polygon_mode - || lhs_pipeline.info.samples != rhs_pipeline.info.samples + let lhs_info = lhs_pipeline.inner.info; + let rhs_info = rhs_pipeline.inner.info; + if lhs_info.blend != rhs_info.blend + || lhs_info.cull_mode != rhs_info.cull_mode + || lhs_info.front_face != rhs_info.front_face + || lhs_info.polygon_mode != rhs_info.polygon_mode + || lhs_info.samples != rhs_info.samples { trace!(" different rasterization modes",); @@ -324,7 +350,7 @@ impl Resolver { } // Keep input on tile - if !rhs_pipeline.input_attachments.is_empty() { + if !rhs_pipeline.inner.input_attachments.is_empty() { trace!(" merging due to subpass input"); return true; @@ -336,7 +362,6 @@ impl Resolver { false } - // See https://vulkan.lunarg.com/doc/view/1.3.204.1/linux/1.3-extensions/vkspec.html#attachment-type-imagelayout fn attachment_layout( aspect_mask: vk::ImageAspectFlags, is_random_access: bool, @@ -384,17 +409,26 @@ impl Resolver { } } + fn expect_attachment_image<'a>( + bindings: &'a [AnyResource], + attachment: &Attachment, + ) -> &'a Image { + bindings[attachment.target] + .as_image() + .expect("invalid attachment target image") + } + #[profiling::function] fn begin_render_pass( - cmd_buf: &CommandBuffer, - bindings: &[Binding], - pass: &Pass, + cmd: &CommandBuffer, + bindings: &[AnyResource], + pass: &CommandData, physical_pass: &mut PhysicalPass, - render_area: Area, + render_area: vk::Rect2D, ) -> Result<(), DriverError> { trace!(" begin render pass"); - let render_pass = physical_pass.render_pass.as_mut().unwrap(); + let render_pass = physical_pass.expect_render_pass_mut(); let attachment_count = render_pass.info.attachments.len(); let mut attachments = Vec::with_capacity(attachment_count); @@ -411,7 +445,10 @@ impl Resolver { ); thread_local! { - static CLEARS_VIEWS: RefCell<(Vec, Vec)> = Default::default(); + static CLEARS_VIEWS: RefCell<( + Vec, + Vec, + )> = Default::default(); } CLEARS_VIEWS.with_borrow_mut(|(clear_values, image_views)| { @@ -427,11 +464,11 @@ impl Resolver { { clear_values[*attachment_idx as usize] = vk::ClearValue { color: vk::ClearColorValue { - float32: clear_value.0, + float32: *clear_value, }, }; - let image = bindings[attachment.target].as_driver_image().unwrap(); + let image = Self::expect_attachment_image(bindings, attachment); attachment_image.flags = image.info.flags; attachment_image.usage = image.info.usage; @@ -459,7 +496,7 @@ impl Resolver { .view_formats .binary_search(&attachment.format) { - let image = bindings[attachment.target].as_driver_image().unwrap(); + let image = Self::expect_attachment_image(bindings, attachment); attachment_image.flags = image.info.flags; attachment_image.usage = image.info.usage; @@ -485,7 +522,7 @@ impl Resolver { depth_stencil: *clear_value, }; - let image = bindings[attachment.target].as_driver_image().unwrap(); + let image = Self::expect_attachment_image(bindings, attachment); attachment_image.flags = image.info.flags; attachment_image.usage = image.info.usage; @@ -511,7 +548,7 @@ impl Resolver { .view_formats .binary_search(&attachment.format) { - let image = bindings[attachment.target].as_driver_image().unwrap(); + let image = Self::expect_attachment_image(bindings, &attachment); attachment_image.flags = image.info.flags; attachment_image.usage = image.info.usage; @@ -535,7 +572,7 @@ impl Resolver { .view_formats .binary_search(&attachment.format) { - let image = bindings[attachment.target].as_driver_image().unwrap(); + let image = Self::expect_attachment_image(bindings, &attachment); attachment_image.flags = image.info.flags; attachment_image.usage = image.info.usage; @@ -554,21 +591,12 @@ impl Resolver { RenderPass::framebuffer(render_pass, FramebufferInfo { attachments })?; unsafe { - cmd_buf.device.cmd_begin_render_pass( - **cmd_buf, + cmd.device.cmd_begin_render_pass( + cmd.handle, &vk::RenderPassBeginInfo::default() - .render_pass(***render_pass) + .render_pass(render_pass.handle) .framebuffer(framebuffer) - .render_area(vk::Rect2D { - offset: vk::Offset2D { - x: render_area.x, - y: render_area.y, - }, - extent: vk::Extent2D { - width: render_area.width, - height: render_area.height, - }, - }) + .render_area(render_area) .clear_values(clear_values) .push_next( &mut vk::RenderPassAttachmentBeginInfoKHR::default() @@ -584,7 +612,7 @@ impl Resolver { #[profiling::function] fn bind_descriptor_sets( - cmd_buf: &CommandBuffer, + cmd: &CommandBuffer, pipeline: &ExecutionPipeline, physical_pass: &PhysicalPass, exec_idx: usize, @@ -609,8 +637,8 @@ impl Resolver { trace!(" bind descriptor sets {:?}", descriptor_sets); unsafe { - cmd_buf.device.cmd_bind_descriptor_sets( - **cmd_buf, + cmd.device.cmd_bind_descriptor_sets( + cmd.handle, pipeline.bind_point(), pipeline.layout(), 0, @@ -624,22 +652,22 @@ impl Resolver { #[profiling::function] fn bind_pipeline( - cmd_buf: &mut CommandBuffer, + cmd: &mut CommandBuffer, physical_pass: &mut PhysicalPass, exec_idx: usize, pipeline: &mut ExecutionPipeline, - depth_stencil: Option, + depth_stencil: Option, ) -> Result<(), DriverError> { if log_enabled!(Trace) { let (ty, name, vk_pipeline) = match pipeline { ExecutionPipeline::Compute(pipeline) => { - ("compute", pipeline.name.as_ref(), ***pipeline) + ("compute", pipeline.debug_name(), pipeline.handle()) } ExecutionPipeline::Graphic(pipeline) => { - ("graphic", pipeline.name.as_ref(), vk::Pipeline::null()) + ("graphic", pipeline.debug_name(), vk::Pipeline::null()) } ExecutionPipeline::RayTrace(pipeline) => { - ("ray trace", pipeline.name.as_ref(), ***pipeline) + ("ray trace", pipeline.debug_name(), pipeline.handle()) } }; if let Some(name) = name { @@ -652,30 +680,29 @@ impl Resolver { // We store a shared reference to this pipeline inside the command buffer! let pipeline_bind_point = pipeline.bind_point(); let pipeline = match pipeline { - ExecutionPipeline::Compute(pipeline) => ***pipeline, - ExecutionPipeline::Graphic(pipeline) => RenderPass::graphic_pipeline( - physical_pass.render_pass.as_mut().unwrap(), + ExecutionPipeline::Compute(pipeline) => pipeline.handle(), + ExecutionPipeline::Graphic(pipeline) => RenderPass::pipeline_handle( + physical_pass.expect_render_pass_mut(), pipeline, depth_stencil, exec_idx as _, )?, - ExecutionPipeline::RayTrace(pipeline) => ***pipeline, + ExecutionPipeline::RayTrace(pipeline) => pipeline.handle(), }; unsafe { - cmd_buf - .device - .cmd_bind_pipeline(**cmd_buf, pipeline_bind_point, pipeline); + cmd.device + .cmd_bind_pipeline(cmd.handle, pipeline_bind_point, pipeline); } Ok(()) } - fn end_render_pass(&mut self, cmd_buf: &CommandBuffer) { + fn end_render_pass(&mut self, cmd: &CommandBuffer) { trace!(" end render pass"); unsafe { - cmd_buf.device.cmd_end_render_pass(**cmd_buf); + cmd.device.cmd_end_render_pass(cmd.handle); } } @@ -684,15 +711,15 @@ impl Resolver { /// A fully-resolved graph contains no additional work and may be discarded, although doing so /// will stall the GPU while the fences are waited on. It is preferrable to wait a few frame so /// that the fences will have already been signalled. - pub fn is_resolved(&self) -> bool { - self.graph.passes.is_empty() + pub fn is_submitted(&self) -> bool { + self.graph.cmds.is_empty() } #[allow(clippy::type_complexity)] #[profiling::function] fn lease_descriptor_pool

( pool: &mut P, - pass: &Pass, + pass: &CommandData, ) -> Result>, DriverError> where P: Pool, @@ -711,51 +738,57 @@ impl Resolver { }; // Find the total count of descriptors per type (there may be multiple pipelines!) - for pool_sizes in pass.descriptor_pools_sizes() { - for pool_size in pool_sizes.values() { - for (&descriptor_ty, &descriptor_count) in pool_size { - debug_assert_ne!(descriptor_count, 0); - - match descriptor_ty { - vk::DescriptorType::ACCELERATION_STRUCTURE_KHR => { - info.acceleration_structure_count += descriptor_count; - } - vk::DescriptorType::COMBINED_IMAGE_SAMPLER => { - info.combined_image_sampler_count += descriptor_count; - } - vk::DescriptorType::INPUT_ATTACHMENT => { - info.input_attachment_count += descriptor_count; - } - vk::DescriptorType::SAMPLED_IMAGE => { - info.sampled_image_count += descriptor_count; - } - vk::DescriptorType::SAMPLER => { - info.sampler_count += descriptor_count; - } - vk::DescriptorType::STORAGE_BUFFER => { - info.storage_buffer_count += descriptor_count; - } - vk::DescriptorType::STORAGE_BUFFER_DYNAMIC => { - info.storage_buffer_dynamic_count += descriptor_count; - } - vk::DescriptorType::STORAGE_IMAGE => { - info.storage_image_count += descriptor_count; - } - vk::DescriptorType::STORAGE_TEXEL_BUFFER => { - info.storage_texel_buffer_count += descriptor_count; - } - vk::DescriptorType::UNIFORM_BUFFER => { - info.uniform_buffer_count += descriptor_count; - } - vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC => { - info.uniform_buffer_dynamic_count += descriptor_count; - } - vk::DescriptorType::UNIFORM_TEXEL_BUFFER => { - info.uniform_texel_buffer_count += descriptor_count; - } - _ => unimplemented!("{descriptor_ty:?}"), - }; - } + for pool_size in pass.descriptor_pools_sizes() { + for (&descriptor_ty, &descriptor_count) in pool_size { + debug_assert_ne!(descriptor_count, 0); + + match descriptor_ty { + vk::DescriptorType::ACCELERATION_STRUCTURE_KHR => { + info.acceleration_structure_count += descriptor_count; + } + vk::DescriptorType::COMBINED_IMAGE_SAMPLER => { + info.combined_image_sampler_count += descriptor_count; + } + vk::DescriptorType::INPUT_ATTACHMENT => { + info.input_attachment_count += descriptor_count; + } + vk::DescriptorType::SAMPLED_IMAGE => { + info.sampled_image_count += descriptor_count; + } + vk::DescriptorType::SAMPLER => { + info.sampler_count += descriptor_count; + } + vk::DescriptorType::STORAGE_BUFFER => { + info.storage_buffer_count += descriptor_count; + } + vk::DescriptorType::STORAGE_BUFFER_DYNAMIC => { + info.storage_buffer_dynamic_count += descriptor_count; + } + vk::DescriptorType::STORAGE_IMAGE => { + info.storage_image_count += descriptor_count; + } + vk::DescriptorType::STORAGE_TEXEL_BUFFER => { + info.storage_texel_buffer_count += descriptor_count; + } + vk::DescriptorType::UNIFORM_BUFFER => { + info.uniform_buffer_count += descriptor_count; + } + vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC => { + info.uniform_buffer_dynamic_count += descriptor_count; + } + vk::DescriptorType::UNIFORM_TEXEL_BUFFER => { + info.uniform_texel_buffer_count += descriptor_count; + } + _ => { + warn!( + "unsupported descriptor type {:?} for pass {}", + descriptor_ty, + pass.name(), + ); + + return Err(DriverError::Unsupported); + } + }; } } @@ -787,7 +820,7 @@ impl Resolver { // debug!("{:#?}", info); - Ok(Some(pool.lease(info)?)) + Ok(Some(pool.resource(info)?)) } #[profiling::function] @@ -799,7 +832,7 @@ impl Resolver { where P: Pool, { - let pass = &self.graph.passes[pass_idx]; + let pass = &self.graph.cmds[pass_idx]; let (mut color_attachment_count, mut depth_stencil_attachment_count) = (0, 0); for exec in &pass.execs { color_attachment_count = color_attachment_count @@ -1020,7 +1053,9 @@ impl Resolver { if !depth_stencil_resolve_set && let Some((resolved_attachment, ..)) = exec.depth_stencil_resolve { - let attachment = attachments.last_mut().unwrap(); + let attachment = attachments + .last_mut() + .expect("missing depth stencil resolve attachment"); attachment.fmt = resolved_attachment.format; attachment.sample_count = resolved_attachment.sample_count; attachment.final_layout = if resolved_attachment @@ -1056,15 +1091,15 @@ impl Resolver { let pipeline = exec .pipeline .as_ref() - .map(|pipeline| pipeline.unwrap_graphic()) - .unwrap(); + .expect("missing graphic pipeline") + .expect_graphic(); let mut subpass_info = SubpassInfo::with_capacity(attachment_count); // Add input attachments - for attachment_idx in pipeline.input_attachments.iter() { + for attachment_idx in pipeline.inner.input_attachments.iter() { debug_assert!( !exec.color_clears.contains_key(attachment_idx), - "cannot clear color attachment index {attachment_idx} because it uses subpass input", + "cannot clear color attachment {attachment_idx} because it uses subpass input", ); let exec_attachment = exec @@ -1072,7 +1107,7 @@ impl Resolver { .get(attachment_idx) .or_else(|| exec.color_loads.get(attachment_idx)) .or_else(|| exec.color_stores.get(attachment_idx)) - .expect("subpass input attachment index not attached, loaded, or stored"); + .expect("missing input attachment"); let is_random_access = exec.color_stores.contains_key(attachment_idx); subpass_info.input_attachments.push(AttachmentRef { attachment: *attachment_idx, @@ -1208,8 +1243,9 @@ impl Resolver { for (exec_idx, exec) in pass.execs.iter().enumerate() { // Check accesses 'accesses: for (node_idx, accesses) in exec.accesses.iter() { - let (mut curr_stages, mut curr_access) = - pipeline_stage_access_flags(accesses.first().unwrap().access); + let (mut curr_stages, mut curr_access) = pipeline_stage_access_flags( + accesses.first().expect("missing resource access").access, + ); if curr_stages.contains(vk::PipelineStageFlags::ALL_COMMANDS) { curr_stages |= vk::PipelineStageFlags::ALL_GRAPHICS; curr_stages &= !vk::PipelineStageFlags::ALL_COMMANDS; @@ -1221,8 +1257,9 @@ impl Resolver { { if let Some(accesses) = prev_exec.accesses.get(node_idx) { for &SubresourceAccess { access, .. } in accesses { - // Is this previous execution access dependent on anything the current - // execution access is dependent upon? + // Is this previous execution access dependent on anything the + // current execution access + // is dependent upon? let (mut prev_stages, prev_access) = pipeline_stage_access_flags(access); if prev_stages.contains(vk::PipelineStageFlags::ALL_COMMANDS) { @@ -1267,8 +1304,8 @@ impl Resolver { curr_stages &= !common_stages; curr_access &= !prev_access; - // Have we found all dependencies for this stage? If so no need to - // check external passes + // Have we found all dependencies for this stage? If so no need + // to check external passes if curr_stages.is_empty() { continue 'accesses; } @@ -1277,15 +1314,16 @@ impl Resolver { } // Second look in previous passes of the entire render graph - for prev_subpass in self.graph.passes[0..pass_idx] + for prev_subpass in self.graph.cmds[0..pass_idx] .iter() .rev() .flat_map(|pass| pass.execs.iter().rev()) { if let Some(accesses) = prev_subpass.accesses.get(node_idx) { for &SubresourceAccess { access, .. } in accesses { - // Is this previous subpass access dependent on anything the current - // subpass access is dependent upon? + // Is this previous subpass access dependent on anything the + // current subpass access is + // dependent upon? let (prev_stages, prev_access) = pipeline_stage_access_flags(access); let common_stages = curr_stages & prev_stages; @@ -1326,8 +1364,8 @@ impl Resolver { curr_stages &= !common_stages; curr_access &= !prev_access; - // If we found all dependencies for this stage there is no need to check - // external passes + // If we found all dependencies for this stage there is no need + // to check external passes if curr_stages.is_empty() { continue 'accesses; } @@ -1647,7 +1685,7 @@ impl Resolver { // trace!("{:#?}", info); - pool.lease(RenderPassInfo { + pool.resource(RenderPassInfo { attachments, dependencies, subpasses, @@ -1667,9 +1705,9 @@ impl Resolver { // At the time this function runs the pass will already have been optimized into a // larger pass made out of anything that might have been merged into it - so we // only care about one pass at a time here - let pass = &mut self.graph.passes[pass_idx]; + let pass = &mut self.graph.cmds[pass_idx]; - trace!("leasing [{pass_idx}: {}]", pass.name); + trace!("requesting [{pass_idx}: {}]", pass.name()); let descriptor_pool = Self::lease_descriptor_pool(pool, pass)?; let mut exec_descriptor_sets = HashMap::with_capacity( @@ -1705,13 +1743,19 @@ impl Resolver { // starts with input we just blow up b/c we can't provide it, or at least shouldn't. debug_assert!(!pass.execs.is_empty()); debug_assert!( - pass.execs[0].pipeline.is_none() - || !pass.execs[0].pipeline.as_ref().unwrap().is_graphic() - || pass.execs[0] + pass.expect_first_exec().pipeline.is_none() + || !pass + .expect_first_exec() + .pipeline + .as_ref() + .is_some_and(|pipeline| pipeline.is_graphic()) + || pass + .expect_first_exec() .pipeline .as_ref() - .unwrap() - .unwrap_graphic() + .expect("missing graphic pipeline") + .expect_graphic() + .inner .descriptor_info .pool_sizes .values() @@ -1721,7 +1765,8 @@ impl Resolver { ); // Also the renderpass may just be None if the pass contained no graphic ops. - let render_pass = if pass.execs[0] + let render_pass = if pass + .expect_first_exec() .pipeline .as_ref() .map(|pipeline| pipeline.is_graphic()) @@ -1747,30 +1792,35 @@ impl Resolver { #[profiling::function] fn merge_scheduled_passes(&mut self, schedule: &mut Vec) { thread_local! { - static PASSES: RefCell>> = Default::default(); + static PASSES: RefCell>> = Default::default(); } PASSES.with_borrow_mut(|passes| { debug_assert!(passes.is_empty()); - passes.extend(self.graph.passes.drain(..).map(Some)); + passes.extend(self.graph.cmds.drain(..).map(Some)); let mut idx = 0; // debug!("attempting to merge {} passes", schedule.len(),); while idx < schedule.len() { - let mut pass = passes[schedule[idx]].take().unwrap(); + let mut pass = passes[schedule[idx]] + .take() + .expect("missing scheduled pass"); // Find candidates let start = idx + 1; let mut end = start; while end < schedule.len() { - let other = passes[schedule[end]].as_ref().unwrap(); + let other = passes[schedule[end]] + .as_ref() + .expect("missing scheduled pass"); debug!( "attempting to merge [{idx}: {}] with [{end}: {}]", - pass.name, other.name + pass.name(), + other.name() ); if Self::allow_merge_passes(&pass, other) { @@ -1781,36 +1831,48 @@ impl Resolver { } if log_enabled!(Trace) && start != end { - trace!("merging {} passes into [{idx}: {}]", end - start, pass.name); + trace!( + "merging {} passes into [{idx}: {}]", + end - start, + pass.name() + ); } + let mut name = pass.name.take().unwrap_or_default(); + // Grow the merged pass once, not per merge { let mut name_additional = 0; let mut execs_additional = 0; for idx in start..end { - let other = passes[schedule[idx]].as_ref().unwrap(); - name_additional += other.name.len() + 3; + let other = passes[schedule[idx]] + .as_ref() + .expect("missing scheduled pass"); + name_additional += other.name().len() + 3; execs_additional += other.execs.len(); } - pass.name.reserve(name_additional); + name.reserve(name_additional); pass.execs.reserve(execs_additional); } for idx in start..end { - let mut other = passes[schedule[idx]].take().unwrap(); - pass.name.push_str(" + "); - pass.name.push_str(other.name.as_str()); + let mut other = passes[schedule[idx]] + .take() + .expect("missing scheduled pass"); + name.push_str(" + "); + name.push_str(other.name()); pass.execs.append(&mut other.execs); } - self.graph.passes.push(pass); + pass.name = Some(name); + + self.graph.cmds.push(pass); idx += 1 + end - start; } // Reschedule passes - schedule.truncate(self.graph.passes.len()); + schedule.truncate(self.graph.cmds.len()); for (idx, pass_idx) in schedule.iter_mut().enumerate() { *pass_idx = idx; @@ -1818,18 +1880,17 @@ impl Resolver { // Add the remaining passes back into the graph for later for pass in passes.drain(..).flatten() { - self.graph.passes.push(pass); + self.graph.cmds.push(pass); } }); } - fn next_subpass(cmd_buf: &CommandBuffer) { + fn next_subpass(cmd: &CommandBuffer) { trace!("next_subpass"); unsafe { - cmd_buf - .device - .cmd_next_subpass(**cmd_buf, vk::SubpassContents::INLINE); + cmd.device + .cmd_next_subpass(cmd.handle, vk::SubpassContents::INLINE); } } @@ -1838,11 +1899,11 @@ impl Resolver { /// Note that this value must be retrieved before resolving a node as there will be no /// data left to inspect afterwards! #[profiling::function] - pub fn node_pipeline_stages(&self, node: impl Node) -> vk::PipelineStageFlags { + pub fn node_stages(&self, node: impl Node) -> vk::PipelineStageFlags { let node_idx = node.index(); let mut res = Default::default(); - 'pass: for pass in self.graph.passes.iter() { + 'pass: for pass in self.graph.cmds.iter() { for exec in pass.execs.iter() { if exec.accesses.contains_key(&node_idx) { res |= pass @@ -1870,12 +1931,10 @@ impl Resolver { #[profiling::function] fn record_execution_barriers<'a>( - cmd_buf: &CommandBuffer, - bindings: &mut [Binding], + cmd: &CommandBuffer, + resources: &mut [AnyResource], accesses: impl Iterator)>, ) { - use std::slice::from_ref; - // We store a Barriers in TLS to save an alloc; contents are POD thread_local! { static TLS: RefCell = Default::default(); @@ -1913,16 +1972,16 @@ impl Resolver { tls.next_accesses.clear(); tls.prev_accesses.clear(); - // Map remaining accesses into vk_sync barriers (some accesses may have been removed by the - // render pass leasing function) + // Map remaining accesses into vk_sync barriers (some accesses may have been removed by + // the render pass request function) for (node_idx, accesses) in accesses { - let binding = &bindings[*node_idx]; + let resource = &resources[*node_idx]; - match binding { - Binding::AccelerationStructure(..) - | Binding::AccelerationStructureLease(..) => { - let Some(accel_struct) = binding.as_driver_acceleration_structure() else { + match resource { + AnyResource::AccelerationStructure(..) + | AnyResource::AccelerationStructureLease(..) => { + let Some(accel_struct) = resource.as_accel_struct() else { #[cfg(debug_assertions)] unreachable!(); @@ -1934,7 +1993,7 @@ impl Resolver { let prev_access = AccelerationStructure::access( accel_struct, - accesses.last().unwrap().access, + accesses.last().expect("missing resource access").access, ); tls.next_accesses.extend( @@ -1944,8 +2003,8 @@ impl Resolver { ); tls.prev_accesses.push(prev_access); } - Binding::Buffer(..) | Binding::BufferLease(..) => { - let Some(buffer) = binding.as_driver_buffer() else { + AnyResource::Buffer(..) | AnyResource::BufferLease(..) => { + let Some(buffer) = resource.as_buffer() else { #[cfg(debug_assertions)] unreachable!(); @@ -1960,7 +2019,7 @@ impl Resolver { subresource, } in accesses { - let Subresource::Buffer(range) = subresource else { + let SubresourceRange::Buffer(range) = subresource else { unreachable!() }; @@ -1969,7 +2028,7 @@ impl Resolver { next_access: access, prev_access, resource: BufferResource { - buffer: **buffer, + buffer: buffer.handle, offset: range.start as _, size: (range.end - range.start) as _, }, @@ -1977,8 +2036,10 @@ impl Resolver { } } } - Binding::Image(..) | Binding::ImageLease(..) | Binding::SwapchainImage(..) => { - let Some(image) = binding.as_driver_image() else { + AnyResource::Image(..) + | AnyResource::ImageLease(..) + | AnyResource::SwapchainImage(..) => { + let Some(image) = resource.as_image() else { #[cfg(debug_assertions)] unreachable!(); @@ -1993,7 +2054,7 @@ impl Resolver { subresource, } in accesses { - let Subresource::Image(range) = subresource else { + let SubresourceRange::Image(range) = subresource else { unreachable!() }; @@ -2002,7 +2063,7 @@ impl Resolver { next_access: access, prev_access, resource: ImageResource { - image: **image, + image: image.handle, range, }, }) @@ -2047,8 +2108,8 @@ impl Resolver { ); BufferBarrier { - next_accesses: from_ref(next_access), - previous_accesses: from_ref(prev_access), + next_accesses: slice::from_ref(next_access), + previous_accesses: slice::from_ref(prev_access), src_queue_family_index: vk::QUEUE_FAMILY_IGNORED, dst_queue_family_index: vk::QUEUE_FAMILY_IGNORED, buffer, @@ -2094,9 +2155,9 @@ impl Resolver { ); ImageBarrier { - next_accesses: from_ref(next_access), + next_accesses: slice::from_ref(next_access), next_layout: image_access_layout(*next_access), - previous_accesses: from_ref(prev_access), + previous_accesses: slice::from_ref(prev_access), previous_layout: image_access_layout(*prev_access), discard_contents: *prev_access == AccessType::Nothing || is_write_access(*next_access), @@ -2109,23 +2170,21 @@ impl Resolver { ); pipeline_barrier( - &cmd_buf.device, - **cmd_buf, + &cmd.device, + cmd.handle, global_barrier, - &buffer_barriers.collect::>(), - &image_barriers.collect::>(), + &buffer_barriers.collect::>(), + &image_barriers.collect::>(), ); }); } #[profiling::function] fn record_image_layout_transitions( - cmd_buf: &CommandBuffer, - bindings: &mut [Binding], - pass: &mut Pass, + cmd: &CommandBuffer, + resources: &mut [AnyResource], + pass: &mut CommandData, ) { - use std::slice::from_ref; - // We store a Barriers in TLS to save an alloc; contents are POD thread_local! { static TLS: RefCell = Default::default(); @@ -2154,17 +2213,17 @@ impl Resolver { .flat_map(|exec| exec.accesses.iter()) .map(|(node_idx, accesses)| (*node_idx, accesses)) { - debug_assert!(bindings.get(node_idx).is_some()); + debug_assert!(resources.get(node_idx).is_some()); - let binding = unsafe { - // PassRef enforces this using assert_bound_graph_node - bindings.get_unchecked(node_idx) + let resource = unsafe { + // CommandRef enforces this during push_resource_access + resources.get_unchecked(node_idx) }; - match binding { - Binding::AccelerationStructure(..) - | Binding::AccelerationStructureLease(..) => { - let Some(accel_struct) = binding.as_driver_acceleration_structure() else { + match resource { + AnyResource::AccelerationStructure(..) + | AnyResource::AccelerationStructureLease(..) => { + let Some(accel_struct) = resource.as_accel_struct() else { #[cfg(debug_assertions)] unreachable!(); @@ -2176,8 +2235,8 @@ impl Resolver { AccelerationStructure::access(accel_struct, AccessType::Nothing); } - Binding::Buffer(..) | Binding::BufferLease(..) => { - let Some(buffer) = binding.as_driver_buffer() else { + AnyResource::Buffer(..) | AnyResource::BufferLease(..) => { + let Some(buffer) = resource.as_buffer() else { #[cfg(debug_assertions)] unreachable!(); @@ -2189,7 +2248,7 @@ impl Resolver { for subresource_access in accesses { let &SubresourceAccess { - subresource: Subresource::Buffer(access_range), + subresource: SubresourceRange::Buffer(access_range), .. } = subresource_access else { @@ -2198,8 +2257,9 @@ impl Resolver { #[cfg(not(debug_assertions))] unsafe { - // This cannot be reached because PassRef enforces the subrange is - // of type N::Subresource where N is the image node type + // This cannot be reached because PassRef enforces the subrange + // is of type N::Subresource + // where N is the image node type unreachable_unchecked() } }; @@ -2207,8 +2267,10 @@ impl Resolver { for _ in Buffer::access(buffer, AccessType::Nothing, access_range) {} } } - Binding::Image(..) | Binding::ImageLease(..) | Binding::SwapchainImage(..) => { - let Some(image) = binding.as_driver_image() else { + AnyResource::Image(..) + | AnyResource::ImageLease(..) + | AnyResource::SwapchainImage(..) => { + let Some(image) = resource.as_image() else { #[cfg(debug_assertions)] unreachable!(); @@ -2226,7 +2288,7 @@ impl Resolver { for subresource_access in accesses { let &SubresourceAccess { access, - subresource: Subresource::Image(access_range), + subresource: SubresourceRange::Image(access_range), } = subresource_access else { #[cfg(debug_assertions)] @@ -2234,8 +2296,9 @@ impl Resolver { #[cfg(not(debug_assertions))] unsafe { - // This cannot be reached because PassRef enforces the subrange is - // of type N::Subresource where N is the image node type + // This cannot be reached because PassRef enforces the subrange + // is of type N::Subresource + // where N is the image node type unreachable_unchecked() } }; @@ -2248,7 +2311,7 @@ impl Resolver { { if initial_layout { tls.images.push(ImageResourceBarrier { - image: **image, + image: image.handle, next_access: initial_image_layout_access(access), prev_access, range, @@ -2282,9 +2345,9 @@ impl Resolver { *prev_access == AccessType::Nothing || !is_read_access(*next_access); ImageBarrier { - next_accesses: from_ref(next_access), + next_accesses: slice::from_ref(next_access), next_layout: image_access_layout(*next_access), - previous_accesses: from_ref(prev_access), + previous_accesses: slice::from_ref(prev_access), previous_layout: image_access_layout(*prev_access), discard_contents, src_queue_family_index: vk::QUEUE_FAMILY_IGNORED, @@ -2296,8 +2359,8 @@ impl Resolver { ); pipeline_barrier( - &cmd_buf.device, - **cmd_buf, + &cmd.device, + cmd.handle, None, &[], &image_barriers.collect::>(), @@ -2305,62 +2368,11 @@ impl Resolver { }); } - /// Records any pending render graph passes that are required by the given node, but does not - /// record any passes that actually contain the given node. - /// - /// As a side effect, the graph is optimized for the given node. Future calls may further optimize - /// the graph, but only on top of the existing optimizations. This only matters if you are pulling - /// multiple images out and you care - in that case pull the "most important" image first. - #[profiling::function] - pub fn record_node_dependencies

( - &mut self, - pool: &mut P, - cmd_buf: &mut CommandBuffer, - node: impl Node, - ) -> Result<(), DriverError> - where - P: Pool + Pool, - { - let node_idx = node.index(); - - debug_assert!(self.graph.bindings.get(node_idx).is_some()); - - // We record up to but not including the first pass which accesses the target node - if let Some(end_pass_idx) = self.graph.first_node_access_pass_index(node) { - self.record_node_passes(pool, cmd_buf, node_idx, end_pass_idx)?; - } - - Ok(()) - } - - /// Records any pending render graph passes that the given node requires. - #[profiling::function] - pub fn record_node

( - &mut self, - pool: &mut P, - cmd_buf: &mut CommandBuffer, - node: impl Node, - ) -> Result<(), DriverError> - where - P: Pool + Pool, - { - let node_idx = node.index(); - - debug_assert!(self.graph.bindings.get(node_idx).is_some()); - - if self.graph.passes.is_empty() { - return Ok(()); - } - - let end_pass_idx = self.graph.passes.len(); - self.record_node_passes(pool, cmd_buf, node_idx, end_pass_idx) - } - #[profiling::function] fn record_node_passes

( &mut self, pool: &mut P, - cmd_buf: &mut CommandBuffer, + cmd: &mut CommandBuffer, node_idx: usize, end_pass_idx: usize, ) -> Result<(), DriverError> @@ -2376,7 +2388,7 @@ impl Resolver { schedule.passes.clear(); self.schedule_node_passes(node_idx, end_pass_idx, schedule); - self.record_scheduled_passes(pool, cmd_buf, schedule, end_pass_idx) + self.record_scheduled_passes(pool, cmd, schedule, end_pass_idx) }) } @@ -2384,7 +2396,7 @@ impl Resolver { fn record_scheduled_passes

( &mut self, pool: &mut P, - cmd_buf: &mut CommandBuffer, + cmd: &mut CommandBuffer, schedule: &mut Schedule, end_pass_idx: usize, ) -> Result<(), DriverError> @@ -2395,44 +2407,47 @@ impl Resolver { return Ok(()); } - // Print some handy details or hit a breakpoint if you set the flag - #[cfg(debug_assertions)] - if log_enabled!(Debug) && self.graph.debug { - debug!("resolving the following graph:\n\n{:#?}\n\n", self.graph); - } + // // Print some handy details or hit a breakpoint if you set the flag + // #[cfg(debug_assertions)] + // if log_enabled!(Debug) && self.graph.debug { + // debug!("resolving the following graph:\n\n{:#?}\n\n", self.graph); + // } debug_assert!( schedule.passes.windows(2).all(|w| w[0] <= w[1]), "Unsorted schedule" ); - // Optimize the schedule; leasing the required stuff it needs + // Optimize the schedule; requesting the required resources it needs Self::reorder_scheduled_passes(schedule, end_pass_idx); self.merge_scheduled_passes(&mut schedule.passes); self.lease_scheduled_resources(pool, &schedule.passes)?; for pass_idx in schedule.passes.iter().copied() { - let pass = &mut self.graph.passes[pass_idx]; + let pass = &mut self.graph.cmds[pass_idx]; - profiling::scope!("Pass", &pass.name); + profiling::scope!("Pass", pass.name()); let physical_pass = &mut self.physical_passes[pass_idx]; let is_graphic = physical_pass.render_pass.is_some(); - trace!("recording pass [{}: {}]", pass_idx, pass.name); + trace!("recording pass [{}: {}]", pass_idx, pass.name()); if !physical_pass.exec_descriptor_sets.is_empty() { - Self::write_descriptor_sets(cmd_buf, &self.graph.bindings, pass, physical_pass)?; + Self::write_descriptor_sets(cmd, &self.graph.resources, pass, physical_pass)?; } let render_area = if is_graphic { - Self::record_image_layout_transitions(cmd_buf, &mut self.graph.bindings, pass); + Self::record_image_layout_transitions(cmd, &mut self.graph.resources, pass); - let render_area = Self::render_area(&self.graph.bindings, pass); + let render_area = vk::Rect2D { + offset: vk::Offset2D { x: 0, y: 0 }, + extent: Self::render_extent(&self.graph.resources, pass), + }; Self::begin_render_pass( - cmd_buf, - &self.graph.bindings, + cmd, + &self.graph.resources, pass, physical_pass, render_area, @@ -2447,18 +2462,18 @@ impl Resolver { let render_area = is_graphic.then(|| { pass.execs[exec_idx] .render_area - .unwrap_or(render_area.unwrap()) + .unwrap_or(render_area.expect("missing render area")) }); let exec = &mut pass.execs[exec_idx]; if is_graphic && exec_idx > 0 { - Self::next_subpass(cmd_buf); + Self::next_subpass(cmd); } if let Some(pipeline) = exec.pipeline.as_mut() { Self::bind_pipeline( - cmd_buf, + cmd, physical_pass, exec_idx, pipeline, @@ -2466,15 +2481,15 @@ impl Resolver { )?; if is_graphic { - let render_area = render_area.unwrap(); + let render_area = render_area.expect("missing render area"); // In this case we set the viewport and scissor for the user Self::set_viewport( - cmd_buf, - render_area.x as _, - render_area.y as _, - render_area.width as _, - render_area.height as _, + cmd, + render_area.offset.x as _, + render_area.offset.y as _, + render_area.extent.width as _, + render_area.extent.height as _, exec.depth_stencil .map(|depth_stencil| { let min = depth_stencil.min.0; @@ -2484,21 +2499,21 @@ impl Resolver { .unwrap_or(0.0..1.0), ); Self::set_scissor( - cmd_buf, - render_area.x, - render_area.y, - render_area.width, - render_area.height, + cmd, + render_area.offset.x, + render_area.offset.y, + render_area.extent.width, + render_area.extent.height, ); } - Self::bind_descriptor_sets(cmd_buf, pipeline, physical_pass, exec_idx); + Self::bind_descriptor_sets(cmd, pipeline, physical_pass, exec_idx); } if !is_graphic { Self::record_execution_barriers( - cmd_buf, - &mut self.graph.bindings, + cmd, + &mut self.graph.resources, exec.accesses.iter(), ); } @@ -2508,22 +2523,23 @@ impl Resolver { { profiling::scope!("Execute callback"); - let exec_func = exec.func.take().unwrap().0; - exec_func( - &cmd_buf.device, - **cmd_buf, - Bindings::new(&self.graph.bindings, exec), - ); + let exec_func = exec.func.take().expect("missing command function").0; + exec_func(crate::cmd::CommandRef::new( + cmd, + &self.graph.resources, + #[cfg(debug_assertions)] + exec, + )); } } if is_graphic { - self.end_render_pass(cmd_buf); + self.end_render_pass(cmd); } } thread_local! { - static PASSES: RefCell> = Default::default(); + static PASSES: RefCell> = Default::default(); } PASSES.with_borrow_mut(|passes| { @@ -2532,17 +2548,18 @@ impl Resolver { // We have to keep the bindings and pipelines alive until the gpu is done schedule.passes.sort_unstable(); while let Some(schedule_idx) = schedule.passes.pop() { - debug_assert!(!self.graph.passes.is_empty()); + debug_assert!(!self.graph.cmds.is_empty()); - while let Some(pass) = self.graph.passes.pop() { - let pass_idx = self.graph.passes.len(); + while let Some(pass) = self.graph.cmds.pop() { + let pass_idx = self.graph.cmds.len(); if pass_idx == schedule_idx { // This was a scheduled pass - store it! - CommandBuffer::push_fenced_drop( - cmd_buf, - (pass, self.physical_passes.pop().unwrap()), - ); + + cmd.drop_after_executed(( + pass, + self.physical_passes.pop().expect("missing physical pass"), + )); break; } else { debug_assert!(pass_idx > schedule_idx); @@ -2555,7 +2572,7 @@ impl Resolver { debug_assert!(self.physical_passes.is_empty()); // Put the other passes back for future resolves - self.graph.passes.extend(passes.drain(..).rev()); + self.graph.cmds.extend(passes.drain(..).rev()); }); log::trace!("Recorded passes"); @@ -2563,40 +2580,11 @@ impl Resolver { Ok(()) } - /// Records any pending render graph passes that have not been previously scheduled. #[profiling::function] - pub fn record_unscheduled_passes

( - &mut self, - pool: &mut P, - cmd_buf: &mut CommandBuffer, - ) -> Result<(), DriverError> - where - P: Pool + Pool, - { - if self.graph.passes.is_empty() { - return Ok(()); - } - - thread_local! { - static SCHEDULE: RefCell = Default::default(); - } - - SCHEDULE.with_borrow_mut(|schedule| { - schedule - .access_cache - .update(&self.graph, self.graph.passes.len()); - schedule.passes.clear(); - schedule.passes.extend(0..self.graph.passes.len()); - - self.record_scheduled_passes(pool, cmd_buf, schedule, self.graph.passes.len()) - }) - } - - #[profiling::function] - fn render_area(bindings: &[Binding], pass: &Pass) -> Area { + fn render_extent(bindings: &[AnyResource], pass: &CommandData) -> vk::Extent2D { // set_render_area was not specified so we're going to guess using the minimum common // attachment extents - let first_exec = pass.execs.first().unwrap(); + let first_exec = pass.expect_first_exec(); // We must be able to find the render area because render passes require at least one // image to be attached @@ -2616,7 +2604,7 @@ impl Resolver { .chain(first_exec.depth_stencil_load) .chain(first_exec.depth_stencil_store) .map(|attachment| { - let info = bindings[attachment.target].as_driver_image().unwrap().info; + let info = Self::expect_attachment_image(bindings, &attachment).info; ( info.width >> attachment.base_mip_level, @@ -2628,12 +2616,7 @@ impl Resolver { height = height.min(attachment_height); } - Area { - height, - width, - x: 0, - y: 0, - } + vk::Extent2D { height, width } } #[profiling::function] @@ -2674,7 +2657,8 @@ impl Resolver { .interdependent_passes(*pass_idx, end_pass_idx) { if unscheduled[other_pass_idx] { - // This pass can't be the candidate because it depends on unfinished work + // This pass can't be the candidate because it depends on unfinished + // work break; } @@ -2694,6 +2678,15 @@ impl Resolver { }); } + /// Returns a borrow of the original Vulkan resource (buffer, image or acceleration structure) + /// which the given node represents. + pub fn resource(&self, resource_node: N) -> &N::Resource + where + N: Node, + { + self.graph.resource(resource_node) + } + /// Returns a vec of pass indexes that are required to be executed, in order, for the given /// node. #[profiling::function] @@ -2701,7 +2694,9 @@ impl Resolver { type UnscheduledUnresolvedUnchecked = (Vec, Vec, VecDeque<(usize, usize)>); thread_local! { - static UNSCHEDULED_UNRESOLVED_UNCHECKED: RefCell = Default::default(); + static UNSCHEDULED_UNRESOLVED_UNCHECKED: RefCell< + UnscheduledUnresolvedUnchecked, + > = Default::default(); } UNSCHEDULED_UNRESOLVED_UNCHECKED.with_borrow_mut(|(unscheduled, unresolved, unchecked)| { @@ -2709,9 +2704,9 @@ impl Resolver { unscheduled.fill(true); unscheduled.resize(end_pass_idx, true); - unresolved.truncate(self.graph.bindings.len()); + unresolved.truncate(self.graph.resources.len()); unresolved.fill(true); - unresolved.resize(self.graph.bindings.len(), true); + unresolved.resize(self.graph.resources.len(), true); debug_assert!(unchecked.is_empty()); @@ -2726,7 +2721,7 @@ impl Resolver { { trace!( " pass [{pass_idx}: {}] is dependent", - self.graph.passes[pass_idx].name + self.graph.cmds[pass_idx].name() ); debug_assert!(unscheduled[pass_idx]); @@ -2762,7 +2757,7 @@ impl Resolver { trace!( " pass [{pass_idx}: {}] is dependent", - self.graph.passes[pass_idx].name + self.graph.cmds[pass_idx].name() ); for node_idx in schedule.access_cache.dependent_nodes(pass_idx) { @@ -2789,7 +2784,7 @@ impl Resolver { .passes .iter() .copied() - .map(|idx| format!("[{}: {}]", idx, self.graph.passes[idx].name)) + .map(|idx| format!("[{}: {}]", idx, self.graph.cmds[idx].name())) .collect::>() .join(", ") ); @@ -2808,24 +2803,24 @@ impl Resolver { unscheduled .iter() .copied() - .map(|idx| format!("[{}: {}]", idx, self.graph.passes[idx].name)) + .map(|idx| format!("[{}: {}]", idx, self.graph.cmds[idx].name())) .collect::>() .join(", ") ); } - if end_pass_idx < self.graph.passes.len() { + if end_pass_idx < self.graph.cmds.len() { // These passes existing on the graph but are not being considered right // now because we've been told to stop work at the "end_pass_idx" point trace!( "ignoring: {}", - self.graph.passes[end_pass_idx..] + self.graph.cmds[end_pass_idx..] .iter() .enumerate() .map(|(idx, pass)| format!( "[{}: {}]", idx + end_pass_idx, - pass.name + pass.name() )) .collect::>() .join(", ") @@ -2836,14 +2831,12 @@ impl Resolver { }); } - fn set_scissor(cmd_buf: &CommandBuffer, x: i32, y: i32, width: u32, height: u32) { - use std::slice::from_ref; - + fn set_scissor(cmd: &CommandBuffer, x: i32, y: i32, width: u32, height: u32) { unsafe { - cmd_buf.device.cmd_set_scissor( - **cmd_buf, + cmd.device.cmd_set_scissor( + cmd.handle, 0, - from_ref(&vk::Rect2D { + slice::from_ref(&vk::Rect2D { extent: vk::Extent2D { width, height }, offset: vk::Offset2D { x, y }, }), @@ -2852,20 +2845,18 @@ impl Resolver { } fn set_viewport( - cmd_buf: &CommandBuffer, + cmd: &CommandBuffer, x: f32, y: f32, width: f32, height: f32, depth: Range, ) { - use std::slice::from_ref; - unsafe { - cmd_buf.device.cmd_set_viewport( - **cmd_buf, + cmd.device.cmd_set_viewport( + cmd.handle, 0, - from_ref(&vk::Viewport { + slice::from_ref(&vk::Viewport { x, y, width, @@ -2879,93 +2870,149 @@ impl Resolver { /// Submits the remaining commands stored in this instance. #[profiling::function] - pub fn submit

( + pub fn queue_submit

( mut self, pool: &mut P, - queue_family_index: usize, - queue_index: usize, + queue_family_index: u32, + queue_index: u32, ) -> Result, DriverError> where P: Pool + Pool + Pool, { - use std::slice::from_ref; - trace!("submit"); - let mut cmd_buf = pool.lease(CommandBufferInfo::new(queue_family_index as _))?; + let mut cmd = pool.resource(CommandBufferInfo::new(queue_family_index as _))?; - debug_assert!( - queue_family_index < cmd_buf.device.physical_device.queue_families.len(), - "Queue family index must be within the range of the available queues created by the device." - ); - debug_assert!( - queue_index - < cmd_buf.device.physical_device.queue_families[queue_family_index].queue_count - as usize, - "Queue index must be within the range of the available queues created by the device." - ); + cmd.wait_until_executed()?; - CommandBuffer::wait_until_executed(&mut cmd_buf)?; + Device::begin_command_buffer( + &cmd.device, + cmd.handle, + &vk::CommandBufferBeginInfo::default() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT), + )?; - unsafe { - cmd_buf - .device - .begin_command_buffer( - **cmd_buf, - &vk::CommandBufferBeginInfo::default() - .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT), - ) - .map_err(|_| DriverError::OutOfMemory)?; - } + self.submit_cmd_buf(pool, &mut cmd)?; - self.record_unscheduled_passes(pool, &mut cmd_buf)?; + Device::with_queue(&cmd.device, queue_family_index, queue_index, |queue| { + Device::end_command_buffer(&cmd.device, cmd.handle)?; - unsafe { - cmd_buf - .device - .end_command_buffer(**cmd_buf) - .map_err(|_| DriverError::OutOfMemory)?; - cmd_buf - .device - .reset_fences(from_ref(&cmd_buf.fence)) - .map_err(|_| DriverError::OutOfMemory)?; - cmd_buf - .device - .queue_submit( - cmd_buf.device.queues[queue_family_index][queue_index], - from_ref(&vk::SubmitInfo::default().command_buffers(from_ref(&cmd_buf))), - cmd_buf.fence, - ) - .map_err(|_| DriverError::OutOfMemory)?; - } + unsafe { + cmd.device + .reset_fences(slice::from_ref(&cmd.fence)) + .map_err(|_| DriverError::OutOfMemory)?; + } - cmd_buf.waiting = true; + Device::queue_submit( + &cmd.device, + queue, + slice::from_ref( + &vk::SubmitInfo::default().command_buffers(slice::from_ref(&cmd.handle)), + ), + cmd.fence, + )?; + + Ok(()) + })?; // This graph contains references to buffers, images, and other resources which must be kept // alive until this graph execution completes on the GPU. Once those references are dropped // they will return to the pool for other things to use. The drop will happen the next time // someone tries to lease a command buffer and we notice this one has returned and the fence // has been signalled. - CommandBuffer::push_fenced_drop(&mut cmd_buf, self); + cmd.drop_after_executed(self); - Ok(cmd_buf) + Ok(cmd) } - pub(crate) fn swapchain_image(&mut self, node: SwapchainImageNode) -> &SwapchainImage { - let Some(swapchain_image) = self.graph.bindings[node.idx].as_swapchain_image() else { - panic!("invalid swapchain image node"); - }; + /// Records any pending render graph passes that have not been previously scheduled. + #[profiling::function] + pub fn submit_cmd_buf

( + &mut self, + pool: &mut P, + cmd: &mut CommandBuffer, + ) -> Result<(), DriverError> + where + P: Pool + Pool, + { + if self.graph.cmds.is_empty() { + return Ok(()); + } - swapchain_image + thread_local! { + static SCHEDULE: RefCell = Default::default(); + } + + SCHEDULE.with_borrow_mut(|schedule| { + schedule + .access_cache + .update(&self.graph, self.graph.cmds.len()); + schedule.passes.clear(); + schedule.passes.extend(0..self.graph.cmds.len()); + + self.record_scheduled_passes(pool, cmd, schedule, self.graph.cmds.len()) + }) + } + + /// Records any pending render graph passes that the given node requires. + #[profiling::function] + pub fn queue_resource

( + &mut self, + resource_node: impl Node, + pool: &mut P, + cmd: &mut CommandBuffer, + ) -> Result<(), DriverError> + where + P: Pool + Pool, + { + let node_idx = resource_node.index(); + + debug_assert!(self.graph.resources.get(node_idx).is_some()); + + if self.graph.cmds.is_empty() { + return Ok(()); + } + + let end_pass_idx = self.graph.cmds.len(); + self.record_node_passes(pool, cmd, node_idx, end_pass_idx) + } + + /// Records any pending render graph passes that are required by the given node, but does not + /// record any passes that actually contain the given node. + /// + /// As a side effect, the graph is optimized for the given node. Future calls may further + /// optimize the graph, but only on top of the existing optimizations. This only matters if + /// you are pulling multiple images out and you care - in that case pull the "most + /// important" image first. + #[profiling::function] + pub fn queue_resource_dependencies

( + &mut self, + resource_node: impl Node, + pool: &mut P, + cmd: &mut CommandBuffer, + ) -> Result<(), DriverError> + where + P: Pool + Pool, + { + let node_idx = resource_node.index(); + + debug_assert!(self.graph.resources.get(node_idx).is_some()); + + // We record up to but not including the first pass which accesses the target node + if let Some(end_pass_idx) = self.graph.first_node_access_pass_index(resource_node) { + self.record_node_passes(pool, cmd, node_idx, end_pass_idx)?; + } + + Ok(()) } #[profiling::function] fn write_descriptor_sets( - cmd_buf: &CommandBuffer, - bindings: &[Binding], - pass: &Pass, + cmd: &CommandBuffer, + bindings: &[AnyResource], + pass: &CommandData, physical_pass: &PhysicalPass, ) -> Result<(), DriverError> { struct IndexWrite<'a> { @@ -3002,15 +3049,23 @@ impl Resolver { // Write the manually bound things (access, read, and write functions) for (descriptor, (node_idx, view_info)) in exec.bindings.iter() { let (descriptor_set_idx, dst_binding, binding_offset) = descriptor.into_tuple(); - let (descriptor_info, _) = pipeline - .descriptor_bindings() - .get(&Descriptor { set: descriptor_set_idx, binding: dst_binding }) - .unwrap_or_else(|| panic!("descriptor {descriptor_set_idx}.{dst_binding}[{binding_offset}] specified in recorded execution of pass \"{}\" was not discovered through shader reflection", &pass.name)); + let Some((descriptor_info, _)) = pipeline.descriptor_bindings().get(&Descriptor { + set: descriptor_set_idx, + binding: dst_binding, + }) else { + warn!( + "binding {}.{}[{}] not found in shader reflection for pass \"{}\"", + descriptor_set_idx, + dst_binding, + binding_offset, + pass.name(), + ); + return Err(DriverError::InvalidData); + }; let descriptor_type = descriptor_info.descriptor_type(); let bound_node = &bindings[*node_idx]; - if let Some(image) = bound_node.as_driver_image() { - let view_info = view_info.as_ref().unwrap(); - let mut image_view_info = *view_info.as_image().unwrap(); + if let Some(image) = bound_node.as_image() { + let mut image_view_info = *view_info.expect_image(); // Handle default views which did not specify a particaular aspect if image_view_info.aspect_mask.is_empty() { @@ -3040,7 +3095,17 @@ impl Resolver { } } vk::DescriptorType::STORAGE_IMAGE => vk::ImageLayout::GENERAL, - _ => unimplemented!("{descriptor_type:?}"), + _ => { + warn!( + "invalid image descriptor type at binding {}.{}[{}] in pass \"{}\"", + descriptor_set_idx, + dst_binding, + binding_offset, + pass.name() + ); + + return Err(DriverError::InvalidData); + } }; if binding_offset == 0 { @@ -3055,7 +3120,11 @@ impl Resolver { }, }); } else { - tls.image_writes.last_mut().unwrap().write.descriptor_count += 1; + tls.image_writes + .last_mut() + .expect("missing image descriptor write") + .write + .descriptor_count += 1; } tls.image_infos.push( @@ -3063,9 +3132,8 @@ impl Resolver { .image_layout(image_layout) .image_view(image_view), ); - } else if let Some(buffer) = bound_node.as_driver_buffer() { - let view_info = view_info.as_ref().unwrap(); - let buffer_view_info = view_info.as_buffer().unwrap(); + } else if let Some(buffer) = bound_node.as_buffer() { + let buffer_view_info = view_info.expect_buffer(); if binding_offset == 0 { tls.buffer_writes.push(IndexWrite { @@ -3079,16 +3147,20 @@ impl Resolver { }, }); } else { - tls.buffer_writes.last_mut().unwrap().write.descriptor_count += 1; + tls.buffer_writes + .last_mut() + .expect("missing buffer descriptor write") + .write + .descriptor_count += 1; } tls.buffer_infos.push( vk::DescriptorBufferInfo::default() - .buffer(**buffer) + .buffer(buffer.handle) .offset(buffer_view_info.start) .range(buffer_view_info.end - buffer_view_info.start), ); - } else if let Some(accel_struct) = bound_node.as_driver_acceleration_structure() { + } else if let Some(accel_struct) = bound_node.as_accel_struct() { if binding_offset == 0 { tls.accel_struct_writes.push(IndexWrite { idx: tls.accel_struct_infos.len(), @@ -3101,17 +3173,25 @@ impl Resolver { } else { tls.accel_struct_writes .last_mut() - .unwrap() + .expect("missing acceleration structure descriptor write") .write .descriptor_count += 1; } tls.accel_struct_infos.push( vk::WriteDescriptorSetAccelerationStructureKHR::default() - .acceleration_structures(std::slice::from_ref(accel_struct)), + .acceleration_structures(std::slice::from_ref(&accel_struct.handle)), ); } else { - unimplemented!(); + warn!( + "invalid bound resource kind at descriptor {}.{}[{}] in pass \"{}\"", + descriptor_set_idx, + dst_binding, + binding_offset, + pass.name() + ); + + return Err(DriverError::InvalidData); } } @@ -3124,36 +3204,66 @@ impl Resolver { binding: dst_binding, }, (descriptor_info, _), - ) in &pipeline.descriptor_bindings + ) in &pipeline.inner.descriptor_bindings { if let DescriptorInfo::InputAttachment(_, attachment_idx) = *descriptor_info { let is_random_access = exec.color_stores.contains_key(&attachment_idx) || exec.color_resolves.contains_key(&attachment_idx); + let current_attachment = exec + .color_attachments + .get(&attachment_idx) + .copied() + .or_else(|| { + exec.color_clears + .get(&attachment_idx) + .map(|(attachment, _)| *attachment) + }) + .or_else(|| exec.color_loads.get(&attachment_idx).copied()) + .or_else(|| exec.color_stores.get(&attachment_idx).copied()) + .or_else(|| { + exec.color_resolves + .get(&attachment_idx) + .map(|(attachment, _)| *attachment) + }) + .expect("missing input attachment target"); let (attachment, write_exec) = pass.execs[0..exec_idx] .iter() .rev() .find_map(|exec| { - exec.color_stores + exec.color_attachments .get(&attachment_idx) .copied() - .map(|attachment| (attachment, exec)) + .or_else(|| { + exec.color_clears + .get(&attachment_idx) + .map(|(attachment, _)| *attachment) + }) + .or_else(|| exec.color_loads.get(&attachment_idx).copied()) + .or_else(|| exec.color_stores.get(&attachment_idx).copied()) .or_else(|| { exec.color_resolves.get(&attachment_idx).map( - |(resolved_attachment, _)| { - (*resolved_attachment, exec) - }, + |(resolved_attachment, _)| *resolved_attachment, ) }) + .filter(|attachment| { + Attachment::are_compatible( + Some(current_attachment), + Some(*attachment), + ) + }) + .map(|attachment| (attachment, exec)) }) .expect("input attachment not written"); - let late = &write_exec.accesses[&attachment.target].last().unwrap(); - let image_range = late.subresource.as_image().unwrap(); + let late = &write_exec.accesses[&attachment.target] + .last() + .expect("missing input attachment access"); + let image_range = late.subresource.expect_image(); let image_binding = &bindings[attachment.target]; - let image = image_binding.as_driver_image().unwrap(); + let image = image_binding.expect_image(); let image_view_info = attachment .image_view_info(image.info) - .to_builder() + .into_builder() .array_layer_count(image_range.layer_count) .base_array_layer(image_range.base_array_layer) .base_mip_level(image_range.base_mip_level) @@ -3218,8 +3328,7 @@ impl Resolver { ); unsafe { - cmd_buf - .device + cmd.device .update_descriptor_sets(tls.descriptors.as_slice(), &[]); } } @@ -3228,8 +3337,65 @@ impl Resolver { } } +impl From for Submission { + fn from(val: Graph) -> Self { + val.into_submission() + } +} + #[derive(Default)] struct Schedule { access_cache: AccessCache, passes: Vec, } + +#[allow(private_bounds)] +#[allow(unused)] +mod derecated { + use crate::{ + Node, Submission, + driver::{ + DriverError, + cmd_buf::CommandBuffer, + descriptor_set::{DescriptorPool, DescriptorPoolInfo}, + render_pass::{RenderPass, RenderPassInfo}, + }, + pool::Pool, + }; + + impl Submission { + #[deprecated = "use is_submitted function"] + #[doc(hidden)] + pub fn is_resolved(&self) -> bool { + self.is_submitted() + } + + #[deprecated = "use submit_resource function"] + #[doc(hidden)] + pub fn record_node

( + &mut self, + pool: &mut P, + cmd: &mut CommandBuffer, + node: impl Node, + ) -> Result<(), DriverError> + where + P: Pool + Pool, + { + self.queue_resource(node, pool, cmd) + } + + #[deprecated = "use submit_resource function"] + #[doc(hidden)] + pub fn record_node_dependencies

( + &mut self, + pool: &mut P, + cmd: &mut CommandBuffer, + node: impl Node, + ) -> Result<(), DriverError> + where + P: Pool + Pool, + { + self.queue_resource_dependencies(node, pool, cmd) + } + } +} diff --git a/tests/gpu_smoke.rs b/tests/gpu_smoke.rs new file mode 100644 index 00000000..ef741f3f --- /dev/null +++ b/tests/gpu_smoke.rs @@ -0,0 +1,25 @@ +use std::{env, ffi::OsString, process::Command}; + +fn run_cpu_readback_example(extra_args: &[&str]) { + let cargo = env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")); + let output = Command::new(cargo) + .args(["run", "--quiet", "--example", "cpu_readback", "--"]) + .args(extra_args) + .current_dir(env!("CARGO_MANIFEST_DIR")) + .output() + .expect("unable to run cpu_readback example"); + + assert!( + output.status.success(), + "cpu_readback example failed with status {:?}\nstdout:\n{}\nstderr:\n{}", + output.status.code(), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); +} + +#[test] +#[ignore = "requires a working Vulkan device"] +fn cpu_readback() { + run_cpu_readback_example(&[]); +}