Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e06b6a853 | |||
|
6cd23bf02a
|
|||
|
cbc8ef36f8
|
|||
|
7c7e91c857
|
|||
|
7c3b3a53f5
|
|||
|
0b8776b850
|
|||
|
e572558d5f
|
|||
|
5a60671bd6
|
|||
|
5aff0b64e8
|
|||
|
757a975d8c
|
|||
|
d146953bcc
|
|||
|
b617e2958d
|
|||
|
607a64ca8d
|
|||
|
e79d26ea68
|
|||
|
e3c74485f1
|
|||
|
ad21704bcc
|
|||
|
95391a05c6
|
|||
|
14ea45d49a
|
|||
|
0caa36d923
|
|||
|
25d26a87a4
|
|||
|
8f0dcd7f4d
|
|||
|
97f56c56ba
|
|||
|
70813154f2
|
|||
|
e40349b204
|
|||
|
61638e083b
|
|||
|
17c3038a36
|
|||
|
b473b100c8
|
|||
|
3c32215665
|
|||
|
f91378b884
|
|||
|
8f8fa426d5
|
|||
|
4d0cb594a7
|
|||
|
07e30cd040
|
|||
|
079e531166
|
|||
|
6a2adbe160
|
|||
|
b8933dd43a
|
|||
|
26efa13a01
|
|||
|
dcd5417af2
|
|||
|
b6b47ae6f6
|
|||
|
1eead8d597
|
|||
|
ce3e5ad813
|
|||
|
d0552922d9
|
|||
|
0de5118575
|
|||
|
0d139b1aae
|
|||
|
72f6c06eca
|
|||
|
0a78fc2460
|
|||
|
5c4fbff2af
|
|||
|
fd8b03c0bc
|
|||
|
0b0ed87650
|
|||
|
6a6393038e
|
|||
|
ec6645a21f
|
|||
|
d1b7b43dce
|
|||
|
adc6c6149c
|
|||
|
5950c62cec
|
|||
|
247f86aa09
|
|||
|
4d8068aef0
|
|||
|
53715d0d9c
|
|||
|
c8876d65eb
|
|||
|
0a2d1bcc32
|
|||
|
ba69bdb6ea
|
|||
|
5cc2c5819f
|
+98
-29
@@ -2,19 +2,21 @@ name: fparkan-ci
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [devel, main]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [devel, main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
msrv-backend-neutral:
|
msrv-backend-neutral:
|
||||||
name: MSRV backend-neutral crates
|
name: MSRV backend-neutral crates
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@67ef31d5b988238dd797d409d6f9574278e20537
|
||||||
with:
|
with:
|
||||||
toolchain: 1.87.0
|
toolchain: 1.87.0
|
||||||
- name: Test backend-neutral crates
|
- name: Test backend-neutral crates
|
||||||
@@ -46,45 +48,112 @@ jobs:
|
|||||||
--locked
|
--locked
|
||||||
|
|
||||||
stage0-matrix:
|
stage0-matrix:
|
||||||
name: Stage 0-2 CI (${{ matrix.os }})
|
name: Stage 0 CI (${{ matrix.os }})
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 30
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: macos-15
|
||||||
smoke_platform: linux
|
|
||||||
- os: windows-latest
|
|
||||||
smoke_platform: windows
|
|
||||||
- os: macos-latest
|
|
||||||
smoke_platform: macos
|
smoke_platform: macos
|
||||||
|
runner_arch: arm64
|
||||||
|
moltenvk_version: 1.4.1
|
||||||
|
vulkan_sdk_version: 1.4.350.1
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@67ef31d5b988238dd797d409d6f9574278e20537
|
||||||
with:
|
with:
|
||||||
toolchain-file: rust-toolchain.toml
|
toolchain: 1.87.0
|
||||||
|
components: clippy,rustfmt
|
||||||
|
- name: Provision macOS Vulkan runtime
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
brew install molten-vk vulkan-loader vulkan-tools vulkan-validationlayers
|
||||||
|
test "$(uname -m)" = "${{ matrix.runner_arch }}"
|
||||||
|
ruby <<'RUBY'
|
||||||
|
require "json"
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"molten-vk" => "${{ matrix.moltenvk_version }}",
|
||||||
|
"vulkan-loader" => "${{ matrix.vulkan_sdk_version }}",
|
||||||
|
"vulkan-tools" => "${{ matrix.vulkan_sdk_version }}",
|
||||||
|
"vulkan-validationlayers" => "${{ matrix.vulkan_sdk_version }}",
|
||||||
|
}
|
||||||
|
payload = JSON.parse(`brew info --json=v2 #{expected.keys.join(" ")}`)
|
||||||
|
actual = payload.fetch("formulae").to_h do |formula|
|
||||||
|
[formula.fetch("name"), formula.fetch("versions").fetch("stable")]
|
||||||
|
end
|
||||||
|
mismatches = expected.each_with_object({}) do |(name, version), out|
|
||||||
|
actual_version = actual[name]
|
||||||
|
next if actual_version == version
|
||||||
|
|
||||||
|
out[name] = {
|
||||||
|
"expected" => version,
|
||||||
|
"actual" => actual_version,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
unless mismatches.empty?
|
||||||
|
warn JSON.pretty_generate(mismatches)
|
||||||
|
abort "unexpected macOS Vulkan formula version"
|
||||||
|
end
|
||||||
|
RUBY
|
||||||
|
HOMEBREW_PREFIX="$(brew --prefix)"
|
||||||
|
VK_ICD_FILENAMES="$HOMEBREW_PREFIX/opt/molten-vk/etc/vulkan/icd.d/MoltenVK_icd.json"
|
||||||
|
VK_LAYER_PATH="$HOMEBREW_PREFIX/opt/vulkan-validationlayers/share/vulkan/explicit_layer.d"
|
||||||
|
DYLD_FALLBACK_LIBRARY_PATH="$HOMEBREW_PREFIX/opt/vulkan-loader/lib:$HOMEBREW_PREFIX/opt/molten-vk/lib"
|
||||||
|
test -f "$VK_ICD_FILENAMES"
|
||||||
|
test -d "$VK_LAYER_PATH"
|
||||||
|
echo "VK_ICD_FILENAMES=$VK_ICD_FILENAMES" >> "$GITHUB_ENV"
|
||||||
|
echo "VK_LAYER_PATH=$VK_LAYER_PATH" >> "$GITHUB_ENV"
|
||||||
|
echo "DYLD_FALLBACK_LIBRARY_PATH=$DYLD_FALLBACK_LIBRARY_PATH" >> "$GITHUB_ENV"
|
||||||
- name: Install cargo-deny
|
- name: Install cargo-deny
|
||||||
run: cargo install cargo-deny --locked
|
run: cargo install cargo-deny --version 0.19.9 --locked
|
||||||
|
- name: Verify shader provenance
|
||||||
|
run: cargo xtask shader-provenance
|
||||||
- name: Run canonical CI gate
|
- name: Run canonical CI gate
|
||||||
run: cargo xtask ci
|
run: cargo xtask ci
|
||||||
- name: Record native Vulkan smoke status
|
- name: Run native Vulkan smoke
|
||||||
if: always()
|
|
||||||
shell: bash
|
|
||||||
run: >
|
run: >
|
||||||
cargo run -p fparkan-vulkan-smoke --locked --
|
cargo run -p fparkan-vulkan-smoke --locked --
|
||||||
--platform "${{ matrix.smoke_platform }}"
|
--out "target/fparkan/native-smoke/${{ matrix.smoke_platform }}.json"
|
||||||
--out "target/fparkan/native-smoke/${{ runner.os }}.json"
|
--timeout-seconds 120
|
||||||
--status blocked
|
- name: Upload acceptance audit
|
||||||
--probe-surface
|
|
||||||
--reason "native Vulkan smoke runner is not enabled on this CI lane yet"
|
|
||||||
- name: Upload acceptance evidence
|
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||||
with:
|
with:
|
||||||
name: stage-0-2-acceptance-${{ matrix.os }}
|
name: stage-0-acceptance-${{ matrix.os }}
|
||||||
path: |
|
path: target/fparkan/acceptance/stage-0-audit.json
|
||||||
target/fparkan/acceptance/stage-0-2-audit.json
|
if-no-files-found: error
|
||||||
target/fparkan/native-smoke/*.json
|
- name: Upload native smoke report
|
||||||
if-no-files-found: ignore
|
if: always()
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||||
|
with:
|
||||||
|
name: native-smoke-${{ matrix.smoke_platform }}
|
||||||
|
path: target/fparkan/native-smoke/*.json
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
native-smoke-audit:
|
||||||
|
name: Native smoke audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
needs: stage0-matrix
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
|
- uses: dtolnay/rust-toolchain@67ef31d5b988238dd797d409d6f9574278e20537
|
||||||
|
with:
|
||||||
|
toolchain: 1.87.0
|
||||||
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||||
|
with:
|
||||||
|
pattern: native-smoke-*
|
||||||
|
path: target/fparkan/native-smoke-artifacts
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Aggregate native smoke reports
|
||||||
|
run: >
|
||||||
|
cargo xtask native-smoke audit
|
||||||
|
--dir target/fparkan/native-smoke-artifacts
|
||||||
|
|||||||
Generated
+428
-13
@@ -106,6 +106,12 @@ version = "1.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@@ -188,22 +194,38 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cargo-platform"
|
name = "cargo-platform"
|
||||||
version = "0.3.3"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba"
|
checksum = "84982c6c0ae343635a3a4ee6dedef965513735c8b183caa7289fa6e27399ebd4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_core",
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cargo-util-schemas"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dc1a6f7b5651af85774ae5a34b4e8be397d9cf4bc063b7e6dbd99a841837830"
|
||||||
|
dependencies = [
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
|
"serde-untagged",
|
||||||
|
"serde-value",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"toml 0.8.23",
|
||||||
|
"unicode-xid",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cargo_metadata"
|
name = "cargo_metadata"
|
||||||
version = "0.23.1"
|
version = "0.21.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9"
|
checksum = "5cfca2aaa699835ba88faf58a06342a314a950d2b9686165e038286c30316868"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"camino",
|
"camino",
|
||||||
"cargo-platform",
|
"cargo-platform",
|
||||||
|
"cargo-util-schemas",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -350,6 +372,17 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
|
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "displaydoc"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dlib"
|
name = "dlib"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@@ -386,6 +419,17 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "erased-serde"
|
||||||
|
version = "0.4.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"typeid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
@@ -439,6 +483,15 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "form_urlencoded"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fparkan-animation"
|
name = "fparkan-animation"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -630,6 +683,8 @@ dependencies = [
|
|||||||
"fparkan-binary",
|
"fparkan-binary",
|
||||||
"fparkan-platform",
|
"fparkan-platform",
|
||||||
"fparkan-render",
|
"fparkan-render",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -717,6 +772,9 @@ dependencies = [
|
|||||||
"fparkan-platform",
|
"fparkan-platform",
|
||||||
"fparkan-platform-winit",
|
"fparkan-platform-winit",
|
||||||
"fparkan-render-vulkan",
|
"fparkan-render-vulkan",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"winit",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -784,6 +842,109 @@ version = "0.5.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_collections"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"potential_utf",
|
||||||
|
"utf8_iter",
|
||||||
|
"yoke",
|
||||||
|
"zerofrom",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_locale_core"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"litemap",
|
||||||
|
"tinystr",
|
||||||
|
"writeable",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_normalizer"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
|
||||||
|
dependencies = [
|
||||||
|
"icu_collections",
|
||||||
|
"icu_normalizer_data",
|
||||||
|
"icu_properties",
|
||||||
|
"icu_provider",
|
||||||
|
"smallvec",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_normalizer_data"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_properties"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
|
||||||
|
dependencies = [
|
||||||
|
"icu_collections",
|
||||||
|
"icu_locale_core",
|
||||||
|
"icu_properties_data",
|
||||||
|
"icu_provider",
|
||||||
|
"zerotrie",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_properties_data"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_provider"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"icu_locale_core",
|
||||||
|
"writeable",
|
||||||
|
"yoke",
|
||||||
|
"zerofrom",
|
||||||
|
"zerotrie",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||||
|
dependencies = [
|
||||||
|
"idna_adapter",
|
||||||
|
"smallvec",
|
||||||
|
"utf8_iter",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna_adapter"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
|
||||||
|
dependencies = [
|
||||||
|
"icu_normalizer",
|
||||||
|
"icu_properties",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.14.0"
|
version = "2.14.0"
|
||||||
@@ -919,6 +1080,12 @@ version = "0.12.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "litemap"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.33"
|
version = "0.4.33"
|
||||||
@@ -989,6 +1156,15 @@ dependencies = [
|
|||||||
"jni-sys 0.3.1",
|
"jni-sys 0.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_enum"
|
name = "num_enum"
|
||||||
version = "0.7.6"
|
version = "0.7.6"
|
||||||
@@ -1239,6 +1415,15 @@ dependencies = [
|
|||||||
"libredox",
|
"libredox",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-float"
|
||||||
|
version = "2.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owned_ttf_parser"
|
name = "owned_ttf_parser"
|
||||||
version = "0.25.1"
|
version = "0.25.1"
|
||||||
@@ -1306,13 +1491,22 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "potential_utf"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
|
||||||
|
dependencies = [
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "3.5.0"
|
version = "3.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"toml_edit",
|
"toml_edit 0.25.12+spec-1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1473,6 +1667,28 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde-untagged"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058"
|
||||||
|
dependencies = [
|
||||||
|
"erased-serde",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"typeid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde-value"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
|
||||||
|
dependencies = [
|
||||||
|
"ordered-float",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_core"
|
name = "serde_core"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
@@ -1506,6 +1722,15 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -1589,6 +1814,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stable_deref_trait"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strict-num"
|
name = "strict-num"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -1606,6 +1837,17 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "synstructure"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
@@ -1671,6 +1913,28 @@ dependencies = [
|
|||||||
"strict-num",
|
"strict-num",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinystr"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.8.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned 0.6.9",
|
||||||
|
"toml_datetime 0.6.11",
|
||||||
|
"toml_edit 0.22.27",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "1.1.2+spec-1.1.0"
|
version = "1.1.2+spec-1.1.0"
|
||||||
@@ -1679,11 +1943,20 @@ checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_spanned",
|
"serde_spanned 1.1.1",
|
||||||
"toml_datetime",
|
"toml_datetime 1.1.1+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"toml_writer",
|
"toml_writer",
|
||||||
"winnow",
|
"winnow 1.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1695,6 +1968,20 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.22.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned 0.6.9",
|
||||||
|
"toml_datetime 0.6.11",
|
||||||
|
"toml_write",
|
||||||
|
"winnow 0.7.15",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.25.12+spec-1.1.0"
|
version = "0.25.12+spec-1.1.0"
|
||||||
@@ -1702,9 +1989,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
|
checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"toml_datetime",
|
"toml_datetime 1.1.1+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"winnow",
|
"winnow 1.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1713,9 +2000,15 @@ version = "1.1.2+spec-1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow",
|
"winnow 1.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_write"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_writer"
|
name = "toml_writer"
|
||||||
version = "1.1.1+spec-1.1.0"
|
version = "1.1.1+spec-1.1.0"
|
||||||
@@ -1744,6 +2037,12 @@ version = "0.25.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typeid"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
@@ -1756,6 +2055,30 @@ version = "1.13.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "url"
|
||||||
|
version = "2.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"idna",
|
||||||
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8_iter"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
@@ -2123,6 +2446,15 @@ dependencies = [
|
|||||||
"xkbcommon-dl",
|
"xkbcommon-dl",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -2138,6 +2470,12 @@ version = "0.57.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "writeable"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "x11-dl"
|
name = "x11-dl"
|
||||||
version = "2.21.0"
|
version = "2.21.0"
|
||||||
@@ -2203,7 +2541,30 @@ dependencies = [
|
|||||||
"fparkan-corpus",
|
"fparkan-corpus",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"toml",
|
"toml 1.1.2+spec-1.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yoke"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
|
||||||
|
dependencies = [
|
||||||
|
"stable_deref_trait",
|
||||||
|
"yoke-derive",
|
||||||
|
"zerofrom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yoke-derive"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2226,6 +2587,60 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerofrom"
|
||||||
|
version = "0.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
|
||||||
|
dependencies = [
|
||||||
|
"zerofrom-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerofrom-derive"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"synstructure",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerotrie"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"yoke",
|
||||||
|
"zerofrom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerovec"
|
||||||
|
version = "0.11.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
|
||||||
|
dependencies = [
|
||||||
|
"yoke",
|
||||||
|
"zerofrom",
|
||||||
|
"zerovec-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerovec-derive"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
|
|||||||
@@ -63,6 +63,51 @@ FPARKAN_CORPORA_MANIFEST=/private/tmp/fparkan-corpora.toml \
|
|||||||
cargo xtask acceptance report --suite licensed --stage 5
|
cargo xtask acceptance report --suite licensed --stage 5
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Stage 0 Vulkan smoke
|
||||||
|
|
||||||
|
Локальный Stage 0 smoke запускает реальный `winit` lifecycle и Vulkan triangle path с включёнными validation layers. Успешный прогон обязан:
|
||||||
|
|
||||||
|
- отрисовать 300 кадров;
|
||||||
|
- выполнить как минимум один реальный resize;
|
||||||
|
- пересоздать swapchain после resize;
|
||||||
|
- завершиться без validation warnings/errors.
|
||||||
|
|
||||||
|
Команда запуска:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run -p fparkan-vulkan-smoke --locked -- \
|
||||||
|
--out target/fparkan/native-smoke/local.json \
|
||||||
|
--timeout-seconds 120
|
||||||
|
```
|
||||||
|
|
||||||
|
Перед запуском убедитесь, что на машине доступен Vulkan loader и рабочий ICD:
|
||||||
|
|
||||||
|
- macOS: используйте ту же схему, что и GitHub CI (`macos-15` arm64):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install molten-vk vulkan-loader vulkan-tools vulkan-validationlayers
|
||||||
|
export VK_ICD_FILENAMES="$(brew --prefix)/opt/molten-vk/etc/vulkan/icd.d/MoltenVK_icd.json"
|
||||||
|
export VK_LAYER_PATH="$(brew --prefix)/opt/vulkan-validationlayers/share/vulkan/explicit_layer.d"
|
||||||
|
export DYLD_FALLBACK_LIBRARY_PATH="$(brew --prefix)/opt/vulkan-loader/lib:$(brew --prefix)/opt/molten-vk/lib"
|
||||||
|
vulkaninfo --summary
|
||||||
|
```
|
||||||
|
|
||||||
|
Workflow fail-closed проверяет exact formula versions и ожидает наличие `VK_LAYER_KHRONOS_validation`.
|
||||||
|
- Linux: установлен `libvulkan` и драйвер/ICD (`mesa-vulkan-drivers`, Lavapipe или vendor GPU stack); smoke нужно запускать из активной графической сессии X11/Wayland.
|
||||||
|
- Windows: установлен Vulkan runtime от GPU vendor или LunarG Vulkan SDK; validation layer должен быть доступен из активного runtime.
|
||||||
|
|
||||||
|
Для полного локального closure gate используйте:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo xtask ci
|
||||||
|
```
|
||||||
|
|
||||||
|
В текущем macOS-only цикле GitHub workflow собирает только macOS report и проверяет его через `native-smoke audit`. Windows и Linux smoke stages сознательно не входят в этот closure:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo xtask native-smoke audit --dir target/fparkan/native-smoke-artifacts
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing & Support
|
## Contributing & Support
|
||||||
|
|
||||||
Проект активно поддерживается и открыт для contribution. Issues и pull requests можно создавать в обоих репозиториях:
|
Проект активно поддерживается и открыт для contribution. Issues и pull requests можно создавать в обоих репозиториях:
|
||||||
|
|||||||
@@ -27,15 +27,14 @@ use fparkan_platform::{
|
|||||||
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
|
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::sync::OnceLock;
|
||||||
use winit::application::ApplicationHandler;
|
use std::time::Instant;
|
||||||
use winit::dpi::PhysicalSize as WinitPhysicalSize;
|
|
||||||
use winit::event::{Event, MouseButton, WindowEvent};
|
use winit::event::{Event, MouseButton, WindowEvent};
|
||||||
use winit::event_loop::{ActiveEventLoop, EventLoop};
|
|
||||||
use winit::platform::scancode::PhysicalKeyExtScancode;
|
use winit::platform::scancode::PhysicalKeyExtScancode;
|
||||||
use winit::window::{Window, WindowId};
|
use winit::window::Window;
|
||||||
|
|
||||||
static NEXT_WINDOW_HANDLE_ID: AtomicU64 = AtomicU64::new(1);
|
static NEXT_WINDOW_HANDLE_ID: AtomicU64 = AtomicU64::new(1);
|
||||||
|
static CLOCK_START: OnceLock<Instant> = OnceLock::new();
|
||||||
const DEFAULT_SMOKE_WIDTH: u32 = 1280;
|
const DEFAULT_SMOKE_WIDTH: u32 = 1280;
|
||||||
const DEFAULT_SMOKE_HEIGHT: u32 = 720;
|
const DEFAULT_SMOKE_HEIGHT: u32 = 720;
|
||||||
|
|
||||||
@@ -49,10 +48,8 @@ pub struct WinitClock;
|
|||||||
|
|
||||||
impl MonotonicClock for WinitClock {
|
impl MonotonicClock for WinitClock {
|
||||||
fn now(&self) -> MonotonicInstant {
|
fn now(&self) -> MonotonicInstant {
|
||||||
let duration = SystemTime::now()
|
let elapsed = CLOCK_START.get_or_init(Instant::now).elapsed();
|
||||||
.duration_since(UNIX_EPOCH)
|
MonotonicInstant(elapsed.as_millis().try_into().unwrap_or(u64::MAX))
|
||||||
.unwrap_or_default();
|
|
||||||
MonotonicInstant(duration.as_millis().try_into().unwrap_or(u64::MAX))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +57,9 @@ impl MonotonicClock for WinitClock {
|
|||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct WinitEventSource {
|
pub struct WinitEventSource {
|
||||||
queue: VecDeque<PlatformEvent>,
|
queue: VecDeque<PlatformEvent>,
|
||||||
|
cursor_position: Option<(f64, f64)>,
|
||||||
|
minimized: Option<bool>,
|
||||||
|
occluded: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WinitEventSource {
|
impl WinitEventSource {
|
||||||
@@ -68,6 +68,9 @@ impl WinitEventSource {
|
|||||||
pub const fn new() -> Self {
|
pub const fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
queue: VecDeque::new(),
|
queue: VecDeque::new(),
|
||||||
|
cursor_position: None,
|
||||||
|
minimized: None,
|
||||||
|
occluded: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,20 +83,24 @@ impl WinitEventSource {
|
|||||||
pub fn push_window_event(&mut self, event: &WindowEvent) {
|
pub fn push_window_event(&mut self, event: &WindowEvent) {
|
||||||
match event {
|
match event {
|
||||||
WindowEvent::KeyboardInput { event, .. } => {
|
WindowEvent::KeyboardInput { event, .. } => {
|
||||||
self.queue.push_back(PlatformEvent::KeyboardInput {
|
if let Some(scancode) = event.physical_key.to_scancode() {
|
||||||
scancode: event.physical_key.to_scancode().unwrap_or(0),
|
self.queue.push_back(PlatformEvent::KeyboardInput {
|
||||||
pressed: event.state.is_pressed(),
|
scancode,
|
||||||
});
|
pressed: event.state.is_pressed(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
WindowEvent::MouseInput { state, button, .. } => {
|
WindowEvent::MouseInput { state, button, .. } => {
|
||||||
|
let (x, y) = self.cursor_position.unwrap_or((0.0, 0.0));
|
||||||
self.queue.push_back(PlatformEvent::MouseInput {
|
self.queue.push_back(PlatformEvent::MouseInput {
|
||||||
button: mouse_button_code(*button),
|
button: mouse_button_code(*button),
|
||||||
pressed: state.is_pressed(),
|
pressed: state.is_pressed(),
|
||||||
x: 0.0,
|
x,
|
||||||
y: 0.0,
|
y,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
WindowEvent::CursorMoved { position, .. } => {
|
WindowEvent::CursorMoved { position, .. } => {
|
||||||
|
self.cursor_position = Some((position.x, position.y));
|
||||||
self.queue.push_back(PlatformEvent::CursorMoved {
|
self.queue.push_back(PlatformEvent::CursorMoved {
|
||||||
x: position.x,
|
x: position.x,
|
||||||
y: position.y,
|
y: position.y,
|
||||||
@@ -104,11 +111,24 @@ impl WinitEventSource {
|
|||||||
width: size.width,
|
width: size.width,
|
||||||
height: size.height,
|
height: size.height,
|
||||||
});
|
});
|
||||||
|
let minimized = size.width == 0 || size.height == 0;
|
||||||
|
if self.minimized != Some(minimized) {
|
||||||
|
self.minimized = Some(minimized);
|
||||||
|
self.queue.push_back(PlatformEvent::Minimized { minimized });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
WindowEvent::Focused(focused) => {
|
WindowEvent::Focused(focused) => {
|
||||||
self.queue
|
self.queue
|
||||||
.push_back(PlatformEvent::FocusChanged { focused: *focused });
|
.push_back(PlatformEvent::FocusChanged { focused: *focused });
|
||||||
}
|
}
|
||||||
|
WindowEvent::Occluded(occluded) => {
|
||||||
|
if self.occluded != Some(*occluded) {
|
||||||
|
self.occluded = Some(*occluded);
|
||||||
|
self.queue.push_back(PlatformEvent::Occluded {
|
||||||
|
occluded: *occluded,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
||||||
self.queue.push_back(PlatformEvent::DpiChanged {
|
self.queue.push_back(PlatformEvent::DpiChanged {
|
||||||
scale: *scale_factor,
|
scale: *scale_factor,
|
||||||
@@ -123,8 +143,11 @@ impl WinitEventSource {
|
|||||||
|
|
||||||
/// Pushes events from an event loop event.
|
/// Pushes events from an event loop event.
|
||||||
pub fn push_event<T>(&mut self, event: &Event<T>) {
|
pub fn push_event<T>(&mut self, event: &Event<T>) {
|
||||||
if let Event::WindowEvent { event, .. } = event {
|
match event {
|
||||||
self.push_window_event(event);
|
Event::Resumed => self.queue.push_back(PlatformEvent::Resumed),
|
||||||
|
Event::Suspended => self.queue.push_back(PlatformEvent::Suspended),
|
||||||
|
Event::WindowEvent { event, .. } => self.push_window_event(event),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,7 +159,7 @@ fn mouse_button_code(button: MouseButton) -> u16 {
|
|||||||
MouseButton::Middle => 2,
|
MouseButton::Middle => 2,
|
||||||
MouseButton::Back => 3,
|
MouseButton::Back => 3,
|
||||||
MouseButton::Forward => 4,
|
MouseButton::Forward => 4,
|
||||||
MouseButton::Other(index) => 100 + index,
|
MouseButton::Other(index) => 100_u16.saturating_add(index),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,113 +210,6 @@ impl WinitWindowPlan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Native smoke window creation result.
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub struct WinitSmokeWindowProbe {
|
|
||||||
/// Validated creation plan.
|
|
||||||
pub plan: WinitWindowPlan,
|
|
||||||
/// Captured window descriptor.
|
|
||||||
pub window: WinitWindow,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WinitSmokeWindowProbe {
|
|
||||||
/// Returns raw native handles captured from the native window.
|
|
||||||
#[must_use]
|
|
||||||
pub fn native_handles(&self) -> Option<NativeWindowHandles> {
|
|
||||||
self.window.native_handles()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a native smoke window, captures raw handles, then exits the event loop.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns [`PlatformError`] when the plan is invalid, the event loop/window
|
|
||||||
/// cannot be created, or raw native handles are unavailable.
|
|
||||||
pub fn probe_smoke_window() -> Result<WinitSmokeWindowProbe, PlatformError> {
|
|
||||||
let plan = WinitWindowPlan::smoke().validate()?;
|
|
||||||
let event_loop = EventLoop::new().map_err(|err| PlatformError::Backend {
|
|
||||||
context: "winit event loop",
|
|
||||||
message: err.to_string(),
|
|
||||||
})?;
|
|
||||||
let mut app = SmokeWindowApp::new(plan);
|
|
||||||
event_loop
|
|
||||||
.run_app(&mut app)
|
|
||||||
.map_err(|err| PlatformError::Backend {
|
|
||||||
context: "winit event loop",
|
|
||||||
message: err.to_string(),
|
|
||||||
})?;
|
|
||||||
app.into_probe()
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SmokeWindowApp {
|
|
||||||
plan: WinitWindowPlan,
|
|
||||||
window: Option<WinitWindow>,
|
|
||||||
error: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SmokeWindowApp {
|
|
||||||
const fn new(plan: WinitWindowPlan) -> Self {
|
|
||||||
Self {
|
|
||||||
plan,
|
|
||||||
window: None,
|
|
||||||
error: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_probe(self) -> Result<WinitSmokeWindowProbe, PlatformError> {
|
|
||||||
if let Some(message) = self.error {
|
|
||||||
return Err(PlatformError::Backend {
|
|
||||||
context: "winit smoke window",
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let window = self.window.ok_or_else(|| PlatformError::Backend {
|
|
||||||
context: "winit smoke window",
|
|
||||||
message: "event loop exited before creating a window".to_string(),
|
|
||||||
})?;
|
|
||||||
if self.plan.requires_native_handles && window.native_handles().is_none() {
|
|
||||||
return Err(PlatformError::Backend {
|
|
||||||
context: "winit smoke window",
|
|
||||||
message: "native window/display handles are unavailable".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(WinitSmokeWindowProbe {
|
|
||||||
plan: self.plan,
|
|
||||||
window,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApplicationHandler for SmokeWindowApp {
|
|
||||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
|
||||||
if self.window.is_some() || self.error.is_some() {
|
|
||||||
event_loop.exit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let attributes = Window::default_attributes()
|
|
||||||
.with_title("FParkan Vulkan smoke")
|
|
||||||
.with_inner_size(WinitPhysicalSize::new(self.plan.width, self.plan.height));
|
|
||||||
match event_loop.create_window(attributes) {
|
|
||||||
Ok(window) => {
|
|
||||||
self.window = Some(WinitWindow::from_window(&window));
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.error = Some(err.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event_loop.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn window_event(
|
|
||||||
&mut self,
|
|
||||||
_event_loop: &ActiveEventLoop,
|
|
||||||
_window_id: WindowId,
|
|
||||||
_event: WindowEvent,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Minimal window view over a `winit` window.
|
/// Minimal window view over a `winit` window.
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct WinitWindow {
|
pub struct WinitWindow {
|
||||||
@@ -323,7 +239,7 @@ impl WinitWindow {
|
|||||||
focused: true,
|
focused: true,
|
||||||
minimized: false,
|
minimized: false,
|
||||||
occluded: false,
|
occluded: false,
|
||||||
native_handles: native_handles(window),
|
native_handles: window_native_handles(window),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,6 +265,62 @@ impl WinitWindow {
|
|||||||
pub const fn default_render_request() -> RenderRequest {
|
pub const fn default_render_request() -> RenderRequest {
|
||||||
RenderRequest::conservative()
|
RenderRequest::conservative()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Applies one platform event to the cached window descriptor state.
|
||||||
|
pub fn apply_event(&mut self, event: &PlatformEvent) {
|
||||||
|
match event {
|
||||||
|
PlatformEvent::Resize { width, height } => {
|
||||||
|
self.width = *width;
|
||||||
|
self.height = *height;
|
||||||
|
}
|
||||||
|
PlatformEvent::DpiChanged { scale } => {
|
||||||
|
self.scale = *scale;
|
||||||
|
}
|
||||||
|
PlatformEvent::FocusChanged { focused } => {
|
||||||
|
self.focused = *focused;
|
||||||
|
}
|
||||||
|
PlatformEvent::Minimized { minimized } => {
|
||||||
|
self.minimized = *minimized;
|
||||||
|
}
|
||||||
|
PlatformEvent::Occluded { occluded } => {
|
||||||
|
self.occluded = *occluded;
|
||||||
|
}
|
||||||
|
PlatformEvent::Suspended
|
||||||
|
| PlatformEvent::Resumed
|
||||||
|
| PlatformEvent::QuitRequested
|
||||||
|
| PlatformEvent::KeyboardInput { .. }
|
||||||
|
| PlatformEvent::MouseInput { .. }
|
||||||
|
| PlatformEvent::CursorMoved { .. } => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies a sequence of platform events to the cached window descriptor state.
|
||||||
|
pub fn apply_events<'a>(&mut self, events: impl IntoIterator<Item = &'a PlatformEvent>) {
|
||||||
|
for event in events {
|
||||||
|
self.apply_event(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies one native `winit` window event to the cached window descriptor state.
|
||||||
|
pub fn apply_window_event(&mut self, event: &WindowEvent) {
|
||||||
|
match event {
|
||||||
|
WindowEvent::Resized(size) => {
|
||||||
|
self.width = size.width;
|
||||||
|
self.height = size.height;
|
||||||
|
self.minimized = size.width == 0 || size.height == 0;
|
||||||
|
}
|
||||||
|
WindowEvent::Focused(focused) => {
|
||||||
|
self.focused = *focused;
|
||||||
|
}
|
||||||
|
WindowEvent::Occluded(occluded) => {
|
||||||
|
self.occluded = *occluded;
|
||||||
|
}
|
||||||
|
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
||||||
|
self.scale = *scale_factor;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WindowPort for WinitWindow {
|
impl WindowPort for WinitWindow {
|
||||||
@@ -384,7 +356,9 @@ impl WindowPort for WinitWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn native_handles(window: &Window) -> Option<NativeWindowHandles> {
|
/// Extracts raw handles from a live `winit::Window`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn window_native_handles(window: &Window) -> Option<NativeWindowHandles> {
|
||||||
let display = window.display_handle().ok()?.as_raw();
|
let display = window.display_handle().ok()?.as_raw();
|
||||||
let window = window.window_handle().ok()?.as_raw();
|
let window = window.window_handle().ok()?.as_raw();
|
||||||
Some(NativeWindowHandles { display, window })
|
Some(NativeWindowHandles { display, window })
|
||||||
@@ -393,6 +367,7 @@ fn native_handles(window: &Window) -> Option<NativeWindowHandles> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use winit::event::{DeviceId, ElementState};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn event_source_buffers_synthetic_events() -> Result<(), PlatformError> {
|
fn event_source_buffers_synthetic_events() -> Result<(), PlatformError> {
|
||||||
@@ -454,33 +429,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn smoke_window_app_requires_created_native_window() {
|
fn monotonic_clock_uses_process_local_epoch() {
|
||||||
let app = SmokeWindowApp::new(WinitWindowPlan::smoke());
|
let clock = WinitClock;
|
||||||
|
let first = clock.now();
|
||||||
|
let second = clock.now();
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(second >= first);
|
||||||
app.into_probe(),
|
|
||||||
Err(PlatformError::Backend {
|
|
||||||
context: "winit smoke window",
|
|
||||||
..
|
|
||||||
})
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn smoke_window_app_rejects_synthetic_window_without_native_handles() {
|
|
||||||
let mut app = SmokeWindowApp::new(WinitWindowPlan::smoke());
|
|
||||||
app.window = Some(WinitWindow::synthetic(
|
|
||||||
DEFAULT_SMOKE_WIDTH,
|
|
||||||
DEFAULT_SMOKE_HEIGHT,
|
|
||||||
));
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
app.into_probe(),
|
|
||||||
Err(PlatformError::Backend {
|
|
||||||
context: "winit smoke window",
|
|
||||||
..
|
|
||||||
})
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -504,6 +458,119 @@ mod tests {
|
|||||||
assert!(events.contains(&PlatformEvent::FocusChanged { focused: false }));
|
assert!(events.contains(&PlatformEvent::FocusChanged { focused: false }));
|
||||||
assert!(events.contains(&PlatformEvent::QuitRequested));
|
assert!(events.contains(&PlatformEvent::QuitRequested));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn push_event_maps_lifecycle_resumed_and_suspended() -> Result<(), PlatformError> {
|
||||||
|
let mut source = WinitEventSource::new();
|
||||||
|
source.push_event(&Event::<()>::Resumed);
|
||||||
|
source.push_event(&Event::<()>::Suspended);
|
||||||
|
|
||||||
|
let mut events = Vec::new();
|
||||||
|
source.poll(&mut events)?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
events,
|
||||||
|
vec![PlatformEvent::Resumed, PlatformEvent::Suspended]
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cursor_position_and_occlusion_are_preserved_for_mouse_input() -> Result<(), PlatformError> {
|
||||||
|
let mut source = WinitEventSource::new();
|
||||||
|
source.push_window_event(&WindowEvent::CursorMoved {
|
||||||
|
device_id: DeviceId::dummy(),
|
||||||
|
position: (320.0, 240.0).into(),
|
||||||
|
});
|
||||||
|
source.push_window_event(&WindowEvent::MouseInput {
|
||||||
|
device_id: DeviceId::dummy(),
|
||||||
|
state: ElementState::Pressed,
|
||||||
|
button: MouseButton::Other(u16::MAX),
|
||||||
|
});
|
||||||
|
source.push_window_event(&WindowEvent::Occluded(true));
|
||||||
|
|
||||||
|
let mut events = Vec::new();
|
||||||
|
source.poll(&mut events)?;
|
||||||
|
|
||||||
|
assert!(events.contains(&PlatformEvent::CursorMoved { x: 320.0, y: 240.0 }));
|
||||||
|
assert!(events.contains(&PlatformEvent::MouseInput {
|
||||||
|
button: u16::MAX,
|
||||||
|
pressed: true,
|
||||||
|
x: 320.0,
|
||||||
|
y: 240.0,
|
||||||
|
}));
|
||||||
|
assert!(events.contains(&PlatformEvent::Occluded { occluded: true }));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_extent_resize_updates_minimized_state() -> Result<(), PlatformError> {
|
||||||
|
let mut source = WinitEventSource::new();
|
||||||
|
source.push_window_event(&WindowEvent::Resized(winit::dpi::PhysicalSize::new(
|
||||||
|
0u32, 720u32,
|
||||||
|
)));
|
||||||
|
source.push_window_event(&WindowEvent::Resized(winit::dpi::PhysicalSize::new(
|
||||||
|
1280u32, 720u32,
|
||||||
|
)));
|
||||||
|
|
||||||
|
let mut events = Vec::new();
|
||||||
|
source.poll(&mut events)?;
|
||||||
|
|
||||||
|
assert!(events.contains(&PlatformEvent::Minimized { minimized: true }));
|
||||||
|
assert!(events.contains(&PlatformEvent::Minimized { minimized: false }));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn window_descriptor_applies_lifecycle_and_resize_events() {
|
||||||
|
let mut window = WinitWindow::synthetic(640, 360);
|
||||||
|
let events = [
|
||||||
|
PlatformEvent::Resize {
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
},
|
||||||
|
PlatformEvent::DpiChanged { scale: 2.0 },
|
||||||
|
PlatformEvent::FocusChanged { focused: false },
|
||||||
|
PlatformEvent::Minimized { minimized: true },
|
||||||
|
PlatformEvent::Occluded { occluded: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
window.apply_events(events.iter());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
window.drawable_size(),
|
||||||
|
PhysicalSize {
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(window.dpi_scale(), 2.0);
|
||||||
|
assert!(!window.has_focus());
|
||||||
|
assert!(window.is_minimized());
|
||||||
|
assert!(window.is_occluded());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn window_descriptor_applies_native_window_events() {
|
||||||
|
let mut window = WinitWindow::synthetic(640, 360);
|
||||||
|
|
||||||
|
window.apply_window_event(&WindowEvent::Resized(winit::dpi::PhysicalSize::new(
|
||||||
|
0u32, 720u32,
|
||||||
|
)));
|
||||||
|
window.apply_window_event(&WindowEvent::Focused(false));
|
||||||
|
window.apply_window_event(&WindowEvent::Occluded(true));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
window.drawable_size(),
|
||||||
|
PhysicalSize {
|
||||||
|
width: 0,
|
||||||
|
height: 720,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert!(!window.has_focus());
|
||||||
|
assert!(window.is_minimized());
|
||||||
|
assert!(window.is_occluded());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SAFETY: no unsafe usage in this crate.
|
// SAFETY: no unsafe usage in this crate.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ version.workspace = true
|
|||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ash = "0.38"
|
ash = "0.38"
|
||||||
@@ -11,6 +12,8 @@ ash-window = "0.13"
|
|||||||
fparkan-binary = { path = "../../crates/fparkan-binary" }
|
fparkan-binary = { path = "../../crates/fparkan-binary" }
|
||||||
fparkan-platform = { path = "../../crates/fparkan-platform" }
|
fparkan-platform = { path = "../../crates/fparkan-platform" }
|
||||||
fparkan-render = { path = "../../crates/fparkan-render" }
|
fparkan-render = { path = "../../crates/fparkan-render" }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
unsafe_code = "allow"
|
unsafe_code = "allow"
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
//! Build-time shader tool metadata for Vulkan reports.
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
const SHADER_COMPILER_NAME: &str = "glslangValidator";
|
||||||
|
const SPIRV_VALIDATOR_NAME: &str = "spirv-val";
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-env-changed=PATH");
|
||||||
|
println!("cargo:rerun-if-env-changed=FPARKAN_GLSLANG_VALIDATOR");
|
||||||
|
println!("cargo:rerun-if-env-changed=FPARKAN_SPIRV_VAL");
|
||||||
|
|
||||||
|
emit_tool_metadata(
|
||||||
|
"FPARKAN_BUILD_SHADER_COMPILER",
|
||||||
|
&tool_path("FPARKAN_GLSLANG_VALIDATOR", SHADER_COMPILER_NAME),
|
||||||
|
);
|
||||||
|
emit_tool_metadata(
|
||||||
|
"FPARKAN_BUILD_SPIRV_VALIDATOR",
|
||||||
|
&tool_path("FPARKAN_SPIRV_VAL", SPIRV_VALIDATOR_NAME),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tool_path(env_var: &str, fallback: &str) -> String {
|
||||||
|
env::var(env_var).unwrap_or_else(|_| fallback.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_tool_metadata(prefix: &str, tool: &str) {
|
||||||
|
let Some(path) = resolve_tool(tool) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
println!("cargo:rerun-if-changed={}", path.display());
|
||||||
|
let Some(version) = tool_version(&path) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(binary_sha256) = tool_sha256(&path) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let name = path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|value| value.to_str())
|
||||||
|
.unwrap_or(tool)
|
||||||
|
.to_string();
|
||||||
|
println!("cargo:rustc-env={prefix}_NAME={name}");
|
||||||
|
println!("cargo:rustc-env={prefix}_VERSION={version}");
|
||||||
|
println!("cargo:rustc-env={prefix}_SHA256={binary_sha256}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_tool(tool: &str) -> Option<std::path::PathBuf> {
|
||||||
|
let candidate = Path::new(tool);
|
||||||
|
if candidate.components().count() > 1 {
|
||||||
|
return candidate.is_file().then(|| candidate.to_path_buf());
|
||||||
|
}
|
||||||
|
let output = Command::new("which").arg(tool).output().ok()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let path = String::from_utf8(output.stdout).ok()?;
|
||||||
|
let path = path.trim();
|
||||||
|
(!path.is_empty()).then(|| path.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tool_version(path: &Path) -> Option<String> {
|
||||||
|
let output = Command::new(path).arg("--version").output().ok()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let stdout = String::from_utf8(output.stdout).ok()?;
|
||||||
|
stdout
|
||||||
|
.lines()
|
||||||
|
.find(|line| !line.trim().is_empty())
|
||||||
|
.map(str::trim)
|
||||||
|
.map(|line| {
|
||||||
|
line.strip_prefix("Glslang Version: ")
|
||||||
|
.unwrap_or(line)
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tool_sha256(path: &Path) -> Option<String> {
|
||||||
|
let output = Command::new("shasum")
|
||||||
|
.args(["-a", "256"])
|
||||||
|
.arg(path)
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let stdout = String::from_utf8(output.stdout).ok()?;
|
||||||
|
stdout.split_whitespace().next().map(ToString::to_string)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"schema":2,"target_env":"vulkan1.1","compiler":{"name":"glslangValidator","version":"11:16.3.0","binary_sha256":"9bcd69d830b350aaa6e2254915ff74e46070e217b67f38daad27c1fc1f22910f"},"validator":{"name":"spirv-val","version":"SPIRV-Tools v2026.2 unknown hash, 2026-04-29T17:02:58+00:00","binary_sha256":"f6d5b96ff19f073f3af0c0bcfa0c18702d288d3ec598efc242d01cd104d8354f"},"modules":[{"name":"triangle.vert","stage":"vertex","entry_point":"main","source_path":"adapters/fparkan-render-vulkan/shaders/triangle.vert","source_sha256":"1e57f14d193fc61457c0749081c452ad25669998913107df12f3ccc3c33e0341","spirv_path":"adapters/fparkan-render-vulkan/shaders/triangle.vert.spv","word_count":253,"sha256":"4d3ceca7b42ebc971d831b0a0d816457397bd9aeda47fb8d44c4b1aeaa5e7ba0","descriptor_sets":0,"push_constant_bytes":0,"compile_command":"glslangValidator -V --target-env vulkan1.1 -S vert -e main adapters/fparkan-render-vulkan/shaders/triangle.vert -o adapters/fparkan-render-vulkan/shaders/triangle.vert.spv","validate_command":"spirv-val --target-env vulkan1.1 adapters/fparkan-render-vulkan/shaders/triangle.vert.spv","interface_hash":"23e1d3d9d32e7f7ec0b9ca87f8b86be8f8363c7eb5d745fc5a157cb8433eb138"},{"name":"triangle.frag","stage":"fragment","entry_point":"main","source_path":"adapters/fparkan-render-vulkan/shaders/triangle.frag","source_sha256":"f19e74d001d07fb537d4b0f9e621f9b8bc40eeb68816130220853abea6bd4445","spirv_path":"adapters/fparkan-render-vulkan/shaders/triangle.frag.spv","word_count":125,"sha256":"5a7441be03cd3c25d557268b2e58d5aa50504c87bffcb4c3fd7cbcf007db0b96","descriptor_sets":0,"push_constant_bytes":0,"compile_command":"glslangValidator -V --target-env vulkan1.1 -S frag -e main adapters/fparkan-render-vulkan/shaders/triangle.frag -o adapters/fparkan-render-vulkan/shaders/triangle.frag.spv","validate_command":"spirv-val --target-env vulkan1.1 adapters/fparkan-render-vulkan/shaders/triangle.frag.spv","interface_hash":"f09342c22d58c8768151ab8579e54e49af586434a4005d16a24e816d881a64f0"}],"manifest_hash":"11e3feb65200ebd2ac87b7e776e9c6433a5da9d71a651bfadea89a51be17ff05"}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(location = 0) in vec3 in_color;
|
||||||
|
layout(location = 0) out vec4 out_color;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
out_color = vec4(in_color, 1.0);
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,11 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 in_position;
|
||||||
|
layout(location = 1) in vec3 in_color;
|
||||||
|
|
||||||
|
layout(location = 0) out vec3 out_color;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
out_color = in_color;
|
||||||
|
gl_Position = vec4(in_position, 0.0, 1.0);
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,472 @@
|
|||||||
|
#![cfg_attr(
|
||||||
|
test,
|
||||||
|
allow(
|
||||||
|
clippy::cast_possible_truncation,
|
||||||
|
clippy::cast_possible_wrap,
|
||||||
|
clippy::cast_precision_loss,
|
||||||
|
clippy::expect_used,
|
||||||
|
clippy::float_cmp,
|
||||||
|
clippy::identity_op,
|
||||||
|
clippy::too_many_lines,
|
||||||
|
clippy::uninlined_format_args,
|
||||||
|
clippy::map_unwrap_or,
|
||||||
|
clippy::needless_raw_string_hashes,
|
||||||
|
clippy::semicolon_if_nothing_returned,
|
||||||
|
clippy::type_complexity,
|
||||||
|
clippy::panic,
|
||||||
|
clippy::unwrap_used
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
#![deny(unsafe_op_in_unsafe_fn)]
|
||||||
|
//! Vulkan adapter facade and migration-ready backend surface contract.
|
||||||
|
//!
|
||||||
|
//! This module intentionally keeps backend-agnostic command validation in the
|
||||||
|
//! shared render crate while exposing deterministic lifecycle telemetry used by
|
||||||
|
//! Stage 0 acceptance evidence.
|
||||||
|
//!
|
||||||
|
//! This crate is the declared low-level Vulkan boundary.
|
||||||
|
|
||||||
|
mod capabilities;
|
||||||
|
mod instance;
|
||||||
|
mod resources;
|
||||||
|
mod runtime;
|
||||||
|
mod smoke;
|
||||||
|
mod smoke_types;
|
||||||
|
mod surface;
|
||||||
|
mod swapchain;
|
||||||
|
mod swapchain_resources;
|
||||||
|
mod validation;
|
||||||
|
|
||||||
|
pub use self::capabilities::{
|
||||||
|
probe_vulkan_runtime_capabilities, probe_vulkan_runtime_capabilities_for_request,
|
||||||
|
VulkanRuntimeCapabilityError, VulkanRuntimeCapabilityProbe,
|
||||||
|
};
|
||||||
|
pub use self::instance::{
|
||||||
|
create_vulkan_instance_probe, plan_vulkan_instance, probe_vulkan_loader,
|
||||||
|
render_instance_plan_json, render_loader_probe_report_json, vulkan_entry_symbol_name,
|
||||||
|
VulkanInstanceConfig, VulkanInstanceError, VulkanInstancePlan, VulkanInstanceProbe,
|
||||||
|
VulkanLoaderError, VulkanLoaderProbeReport,
|
||||||
|
};
|
||||||
|
#[cfg(test)]
|
||||||
|
use self::instance::{cstring_vec, ensure_instance_extensions_available};
|
||||||
|
use self::resources::{
|
||||||
|
color_subresource_range, create_command_pool, create_frame_sync, create_triangle_index_buffer,
|
||||||
|
create_triangle_vertex_buffer, destroy_allocated_buffer, VulkanAllocatedBuffer,
|
||||||
|
VulkanFrameSync,
|
||||||
|
};
|
||||||
|
pub use self::runtime::{
|
||||||
|
create_vulkan_logical_device_probe, create_vulkan_logical_device_probe_for_request,
|
||||||
|
VulkanLogicalDeviceError, VulkanLogicalDeviceProbe, VulkanLogicalDeviceReport,
|
||||||
|
};
|
||||||
|
pub use self::smoke_types::{
|
||||||
|
VulkanSmokeBootstrapProgress, VulkanSmokeBootstrapSnapshot, VulkanSmokeFrameOutcome,
|
||||||
|
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError,
|
||||||
|
VulkanSmokeRendererReport, VulkanSmokeShutdownReport, VulkanValidationReport,
|
||||||
|
};
|
||||||
|
#[cfg(test)]
|
||||||
|
use self::surface::extension_name;
|
||||||
|
pub use self::surface::{
|
||||||
|
create_vulkan_surface_probe, plan_vulkan_surface, render_surface_plan_json, VulkanSurfaceError,
|
||||||
|
VulkanSurfacePlan, VulkanSurfaceProbe,
|
||||||
|
};
|
||||||
|
pub use self::swapchain::{
|
||||||
|
create_vulkan_swapchain_probe, create_vulkan_swapchain_probe_for_extent, VulkanSwapchainProbe,
|
||||||
|
VulkanSwapchainProbeError, VulkanSwapchainReport,
|
||||||
|
};
|
||||||
|
use self::swapchain_resources::{
|
||||||
|
create_swapchain_resources, destroy_swapchain_resources, VulkanSwapchainResources,
|
||||||
|
};
|
||||||
|
use self::validation::{create_validation_messenger, VulkanValidationMessenger};
|
||||||
|
use ash::vk;
|
||||||
|
/// Minimum Vulkan API version accepted by the Stage 0 backend.
|
||||||
|
pub const MIN_VULKAN_API_VERSION: u32 = vk::API_VERSION_1_1;
|
||||||
|
const KHR_PORTABILITY_ENUMERATION_EXTENSION: &str = "VK_KHR_portability_enumeration";
|
||||||
|
const EXT_DEBUG_UTILS_EXTENSION: &str = "VK_EXT_debug_utils";
|
||||||
|
const VALIDATION_LAYER_NAME: &str = "VK_LAYER_KHRONOS_validation";
|
||||||
|
pub(crate) const SPIRV_MAGIC: u32 = 0x0723_0203;
|
||||||
|
pub(crate) const SPIRV_VERSION_1_0: u32 = 0x0001_0000;
|
||||||
|
pub(crate) const TRIANGLE_VERTEX_SHADER_WORDS: &[u32] = &[
|
||||||
|
0x0723_0203,
|
||||||
|
0x0001_0300,
|
||||||
|
0x0008_000b,
|
||||||
|
0x0000_0021,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0002_0011,
|
||||||
|
0x0000_0001,
|
||||||
|
0x0006_000b,
|
||||||
|
0x0000_0001,
|
||||||
|
0x4c53_4c47,
|
||||||
|
0x6474_732e,
|
||||||
|
0x3035_342e,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0003_000e,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0000_0001,
|
||||||
|
0x0009_000f,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0000_0004,
|
||||||
|
0x6e69_616d,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0000_0009,
|
||||||
|
0x0000_000b,
|
||||||
|
0x0000_0013,
|
||||||
|
0x0000_0018,
|
||||||
|
0x0003_0003,
|
||||||
|
0x0000_0002,
|
||||||
|
0x0000_01c2,
|
||||||
|
0x0004_0005,
|
||||||
|
0x0000_0004,
|
||||||
|
0x6e69_616d,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0005_0005,
|
||||||
|
0x0000_0009,
|
||||||
|
0x5f74_756f,
|
||||||
|
0x6f6c_6f63,
|
||||||
|
0x0000_0072,
|
||||||
|
0x0005_0005,
|
||||||
|
0x0000_000b,
|
||||||
|
0x635f_6e69,
|
||||||
|
0x726f_6c6f,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0006_0005,
|
||||||
|
0x0000_0011,
|
||||||
|
0x505f_6c67,
|
||||||
|
0x6556_7265,
|
||||||
|
0x7865_7472,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0006_0006,
|
||||||
|
0x0000_0011,
|
||||||
|
0x0000_0000,
|
||||||
|
0x505f_6c67,
|
||||||
|
0x7469_736f,
|
||||||
|
0x006e_6f69,
|
||||||
|
0x0007_0006,
|
||||||
|
0x0000_0011,
|
||||||
|
0x0000_0001,
|
||||||
|
0x505f_6c67,
|
||||||
|
0x746e_696f,
|
||||||
|
0x657a_6953,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0007_0006,
|
||||||
|
0x0000_0011,
|
||||||
|
0x0000_0002,
|
||||||
|
0x435f_6c67,
|
||||||
|
0x4470_696c,
|
||||||
|
0x6174_7369,
|
||||||
|
0x0065_636e,
|
||||||
|
0x0007_0006,
|
||||||
|
0x0000_0011,
|
||||||
|
0x0000_0003,
|
||||||
|
0x435f_6c67,
|
||||||
|
0x446c_6c75,
|
||||||
|
0x6174_7369,
|
||||||
|
0x0065_636e,
|
||||||
|
0x0003_0005,
|
||||||
|
0x0000_0013,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0005_0005,
|
||||||
|
0x0000_0018,
|
||||||
|
0x705f_6e69,
|
||||||
|
0x7469_736f,
|
||||||
|
0x006e_6f69,
|
||||||
|
0x0004_0047,
|
||||||
|
0x0000_0009,
|
||||||
|
0x0000_001e,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0004_0047,
|
||||||
|
0x0000_000b,
|
||||||
|
0x0000_001e,
|
||||||
|
0x0000_0001,
|
||||||
|
0x0003_0047,
|
||||||
|
0x0000_0011,
|
||||||
|
0x0000_0002,
|
||||||
|
0x0005_0048,
|
||||||
|
0x0000_0011,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0000_000b,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0005_0048,
|
||||||
|
0x0000_0011,
|
||||||
|
0x0000_0001,
|
||||||
|
0x0000_000b,
|
||||||
|
0x0000_0001,
|
||||||
|
0x0005_0048,
|
||||||
|
0x0000_0011,
|
||||||
|
0x0000_0002,
|
||||||
|
0x0000_000b,
|
||||||
|
0x0000_0003,
|
||||||
|
0x0005_0048,
|
||||||
|
0x0000_0011,
|
||||||
|
0x0000_0003,
|
||||||
|
0x0000_000b,
|
||||||
|
0x0000_0004,
|
||||||
|
0x0004_0047,
|
||||||
|
0x0000_0018,
|
||||||
|
0x0000_001e,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0002_0013,
|
||||||
|
0x0000_0002,
|
||||||
|
0x0003_0021,
|
||||||
|
0x0000_0003,
|
||||||
|
0x0000_0002,
|
||||||
|
0x0003_0016,
|
||||||
|
0x0000_0006,
|
||||||
|
0x0000_0020,
|
||||||
|
0x0004_0017,
|
||||||
|
0x0000_0007,
|
||||||
|
0x0000_0006,
|
||||||
|
0x0000_0003,
|
||||||
|
0x0004_0020,
|
||||||
|
0x0000_0008,
|
||||||
|
0x0000_0003,
|
||||||
|
0x0000_0007,
|
||||||
|
0x0004_003b,
|
||||||
|
0x0000_0008,
|
||||||
|
0x0000_0009,
|
||||||
|
0x0000_0003,
|
||||||
|
0x0004_0020,
|
||||||
|
0x0000_000a,
|
||||||
|
0x0000_0001,
|
||||||
|
0x0000_0007,
|
||||||
|
0x0004_003b,
|
||||||
|
0x0000_000a,
|
||||||
|
0x0000_000b,
|
||||||
|
0x0000_0001,
|
||||||
|
0x0004_0017,
|
||||||
|
0x0000_000d,
|
||||||
|
0x0000_0006,
|
||||||
|
0x0000_0004,
|
||||||
|
0x0004_0015,
|
||||||
|
0x0000_000e,
|
||||||
|
0x0000_0020,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0004_002b,
|
||||||
|
0x0000_000e,
|
||||||
|
0x0000_000f,
|
||||||
|
0x0000_0001,
|
||||||
|
0x0004_001c,
|
||||||
|
0x0000_0010,
|
||||||
|
0x0000_0006,
|
||||||
|
0x0000_000f,
|
||||||
|
0x0006_001e,
|
||||||
|
0x0000_0011,
|
||||||
|
0x0000_000d,
|
||||||
|
0x0000_0006,
|
||||||
|
0x0000_0010,
|
||||||
|
0x0000_0010,
|
||||||
|
0x0004_0020,
|
||||||
|
0x0000_0012,
|
||||||
|
0x0000_0003,
|
||||||
|
0x0000_0011,
|
||||||
|
0x0004_003b,
|
||||||
|
0x0000_0012,
|
||||||
|
0x0000_0013,
|
||||||
|
0x0000_0003,
|
||||||
|
0x0004_0015,
|
||||||
|
0x0000_0014,
|
||||||
|
0x0000_0020,
|
||||||
|
0x0000_0001,
|
||||||
|
0x0004_002b,
|
||||||
|
0x0000_0014,
|
||||||
|
0x0000_0015,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0004_0017,
|
||||||
|
0x0000_0016,
|
||||||
|
0x0000_0006,
|
||||||
|
0x0000_0002,
|
||||||
|
0x0004_0020,
|
||||||
|
0x0000_0017,
|
||||||
|
0x0000_0001,
|
||||||
|
0x0000_0016,
|
||||||
|
0x0004_003b,
|
||||||
|
0x0000_0017,
|
||||||
|
0x0000_0018,
|
||||||
|
0x0000_0001,
|
||||||
|
0x0004_002b,
|
||||||
|
0x0000_0006,
|
||||||
|
0x0000_001a,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0004_002b,
|
||||||
|
0x0000_0006,
|
||||||
|
0x0000_001b,
|
||||||
|
0x3f80_0000,
|
||||||
|
0x0004_0020,
|
||||||
|
0x0000_001f,
|
||||||
|
0x0000_0003,
|
||||||
|
0x0000_000d,
|
||||||
|
0x0005_0036,
|
||||||
|
0x0000_0002,
|
||||||
|
0x0000_0004,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0000_0003,
|
||||||
|
0x0002_00f8,
|
||||||
|
0x0000_0005,
|
||||||
|
0x0004_003d,
|
||||||
|
0x0000_0007,
|
||||||
|
0x0000_000c,
|
||||||
|
0x0000_000b,
|
||||||
|
0x0003_003e,
|
||||||
|
0x0000_0009,
|
||||||
|
0x0000_000c,
|
||||||
|
0x0004_003d,
|
||||||
|
0x0000_0016,
|
||||||
|
0x0000_0019,
|
||||||
|
0x0000_0018,
|
||||||
|
0x0005_0051,
|
||||||
|
0x0000_0006,
|
||||||
|
0x0000_001c,
|
||||||
|
0x0000_0019,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0005_0051,
|
||||||
|
0x0000_0006,
|
||||||
|
0x0000_001d,
|
||||||
|
0x0000_0019,
|
||||||
|
0x0000_0001,
|
||||||
|
0x0007_0050,
|
||||||
|
0x0000_000d,
|
||||||
|
0x0000_001e,
|
||||||
|
0x0000_001c,
|
||||||
|
0x0000_001d,
|
||||||
|
0x0000_001a,
|
||||||
|
0x0000_001b,
|
||||||
|
0x0005_0041,
|
||||||
|
0x0000_001f,
|
||||||
|
0x0000_0020,
|
||||||
|
0x0000_0013,
|
||||||
|
0x0000_0015,
|
||||||
|
0x0003_003e,
|
||||||
|
0x0000_0020,
|
||||||
|
0x0000_001e,
|
||||||
|
0x0001_00fd,
|
||||||
|
0x0001_0038,
|
||||||
|
];
|
||||||
|
pub(crate) const TRIANGLE_FRAGMENT_SHADER_WORDS: &[u32] = &[
|
||||||
|
0x0723_0203,
|
||||||
|
0x0001_0300,
|
||||||
|
0x0008_000b,
|
||||||
|
0x0000_0013,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0002_0011,
|
||||||
|
0x0000_0001,
|
||||||
|
0x0006_000b,
|
||||||
|
0x0000_0001,
|
||||||
|
0x4c53_4c47,
|
||||||
|
0x6474_732e,
|
||||||
|
0x3035_342e,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0003_000e,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0000_0001,
|
||||||
|
0x0007_000f,
|
||||||
|
0x0000_0004,
|
||||||
|
0x0000_0004,
|
||||||
|
0x6e69_616d,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0000_0009,
|
||||||
|
0x0000_000c,
|
||||||
|
0x0003_0010,
|
||||||
|
0x0000_0004,
|
||||||
|
0x0000_0007,
|
||||||
|
0x0003_0003,
|
||||||
|
0x0000_0002,
|
||||||
|
0x0000_01c2,
|
||||||
|
0x0004_0005,
|
||||||
|
0x0000_0004,
|
||||||
|
0x6e69_616d,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0005_0005,
|
||||||
|
0x0000_0009,
|
||||||
|
0x5f74_756f,
|
||||||
|
0x6f6c_6f63,
|
||||||
|
0x0000_0072,
|
||||||
|
0x0005_0005,
|
||||||
|
0x0000_000c,
|
||||||
|
0x635f_6e69,
|
||||||
|
0x726f_6c6f,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0004_0047,
|
||||||
|
0x0000_0009,
|
||||||
|
0x0000_001e,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0004_0047,
|
||||||
|
0x0000_000c,
|
||||||
|
0x0000_001e,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0002_0013,
|
||||||
|
0x0000_0002,
|
||||||
|
0x0003_0021,
|
||||||
|
0x0000_0003,
|
||||||
|
0x0000_0002,
|
||||||
|
0x0003_0016,
|
||||||
|
0x0000_0006,
|
||||||
|
0x0000_0020,
|
||||||
|
0x0004_0017,
|
||||||
|
0x0000_0007,
|
||||||
|
0x0000_0006,
|
||||||
|
0x0000_0004,
|
||||||
|
0x0004_0020,
|
||||||
|
0x0000_0008,
|
||||||
|
0x0000_0003,
|
||||||
|
0x0000_0007,
|
||||||
|
0x0004_003b,
|
||||||
|
0x0000_0008,
|
||||||
|
0x0000_0009,
|
||||||
|
0x0000_0003,
|
||||||
|
0x0004_0017,
|
||||||
|
0x0000_000a,
|
||||||
|
0x0000_0006,
|
||||||
|
0x0000_0003,
|
||||||
|
0x0004_0020,
|
||||||
|
0x0000_000b,
|
||||||
|
0x0000_0001,
|
||||||
|
0x0000_000a,
|
||||||
|
0x0004_003b,
|
||||||
|
0x0000_000b,
|
||||||
|
0x0000_000c,
|
||||||
|
0x0000_0001,
|
||||||
|
0x0004_002b,
|
||||||
|
0x0000_0006,
|
||||||
|
0x0000_000e,
|
||||||
|
0x3f80_0000,
|
||||||
|
0x0005_0036,
|
||||||
|
0x0000_0002,
|
||||||
|
0x0000_0004,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0000_0003,
|
||||||
|
0x0002_00f8,
|
||||||
|
0x0000_0005,
|
||||||
|
0x0004_003d,
|
||||||
|
0x0000_000a,
|
||||||
|
0x0000_000d,
|
||||||
|
0x0000_000c,
|
||||||
|
0x0005_0051,
|
||||||
|
0x0000_0006,
|
||||||
|
0x0000_000f,
|
||||||
|
0x0000_000d,
|
||||||
|
0x0000_0000,
|
||||||
|
0x0005_0051,
|
||||||
|
0x0000_0006,
|
||||||
|
0x0000_0010,
|
||||||
|
0x0000_000d,
|
||||||
|
0x0000_0001,
|
||||||
|
0x0005_0051,
|
||||||
|
0x0000_0006,
|
||||||
|
0x0000_0011,
|
||||||
|
0x0000_000d,
|
||||||
|
0x0000_0002,
|
||||||
|
0x0007_0050,
|
||||||
|
0x0000_0007,
|
||||||
|
0x0000_0012,
|
||||||
|
0x0000_000f,
|
||||||
|
0x0000_0010,
|
||||||
|
0x0000_0011,
|
||||||
|
0x0000_000e,
|
||||||
|
0x0003_003e,
|
||||||
|
0x0000_0009,
|
||||||
|
0x0000_0012,
|
||||||
|
0x0001_00fd,
|
||||||
|
0x0001_0038,
|
||||||
|
];
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
@@ -0,0 +1,508 @@
|
|||||||
|
#![allow(unsafe_code)]
|
||||||
|
|
||||||
|
use ash::vk;
|
||||||
|
use fparkan_platform::RenderRequest;
|
||||||
|
use std::ffi::CStr;
|
||||||
|
|
||||||
|
use super::{VulkanInstanceProbe, VulkanSurfaceProbe};
|
||||||
|
use crate::policy::{
|
||||||
|
compare_reports, plan_vulkan_swapchain, validate_device_for_request, VulkanCapabilityError,
|
||||||
|
VulkanCapabilityReport, VulkanDeviceLimits, VulkanDeviceType, VulkanPhysicalDeviceRecord,
|
||||||
|
VulkanQueueFamily, VulkanSurfaceFormat, VulkanSwapchainError, VulkanSwapchainPlan,
|
||||||
|
VulkanSwapchainRequest, VulkanSwapchainSurfaceCapabilities,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Live Vulkan device/surface capability probe.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanRuntimeCapabilityProbe {
|
||||||
|
/// Selected device/queue capability report.
|
||||||
|
pub capability: VulkanCapabilityReport,
|
||||||
|
/// Swapchain plan built from the selected device and live surface capabilities.
|
||||||
|
pub swapchain: VulkanSwapchainPlan,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Live Vulkan device/surface capability probe error.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum VulkanRuntimeCapabilityError {
|
||||||
|
/// Physical device enumeration failed.
|
||||||
|
EnumerateDevicesFailed {
|
||||||
|
/// Vulkan result.
|
||||||
|
result: vk::Result,
|
||||||
|
},
|
||||||
|
/// Device extension enumeration failed.
|
||||||
|
EnumerateDeviceExtensionsFailed {
|
||||||
|
/// Device name or index context.
|
||||||
|
device: String,
|
||||||
|
/// Vulkan result.
|
||||||
|
result: vk::Result,
|
||||||
|
},
|
||||||
|
/// Queue-family present support query failed.
|
||||||
|
PresentSupportFailed {
|
||||||
|
/// Device name.
|
||||||
|
device: String,
|
||||||
|
/// Queue-family index.
|
||||||
|
queue_family: u32,
|
||||||
|
/// Vulkan result.
|
||||||
|
result: vk::Result,
|
||||||
|
},
|
||||||
|
/// Surface format query failed.
|
||||||
|
SurfaceFormatsFailed {
|
||||||
|
/// Device name.
|
||||||
|
device: String,
|
||||||
|
/// Vulkan result.
|
||||||
|
result: vk::Result,
|
||||||
|
},
|
||||||
|
/// Surface capability query failed.
|
||||||
|
SurfaceCapabilitiesFailed {
|
||||||
|
/// Device name.
|
||||||
|
device: String,
|
||||||
|
/// Vulkan result.
|
||||||
|
result: vk::Result,
|
||||||
|
},
|
||||||
|
/// Present mode query failed.
|
||||||
|
PresentModesFailed {
|
||||||
|
/// Device name.
|
||||||
|
device: String,
|
||||||
|
/// Vulkan result.
|
||||||
|
result: vk::Result,
|
||||||
|
},
|
||||||
|
/// No device satisfied Stage 0 capability policy.
|
||||||
|
Capability(VulkanCapabilityError),
|
||||||
|
/// Live surface capabilities could not produce a swapchain plan.
|
||||||
|
Swapchain(VulkanSwapchainError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for VulkanRuntimeCapabilityError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::EnumerateDevicesFailed { result } => {
|
||||||
|
write!(f, "Vulkan physical device enumeration failed: {result:?}")
|
||||||
|
}
|
||||||
|
Self::EnumerateDeviceExtensionsFailed { device, result } => write!(
|
||||||
|
f,
|
||||||
|
"Vulkan device {device} extension enumeration failed: {result:?}"
|
||||||
|
),
|
||||||
|
Self::PresentSupportFailed {
|
||||||
|
device,
|
||||||
|
queue_family,
|
||||||
|
result,
|
||||||
|
} => write!(
|
||||||
|
f,
|
||||||
|
"Vulkan device {device} queue family {queue_family} present support query failed: {result:?}"
|
||||||
|
),
|
||||||
|
Self::SurfaceFormatsFailed { device, result } => write!(
|
||||||
|
f,
|
||||||
|
"Vulkan device {device} surface format query failed: {result:?}"
|
||||||
|
),
|
||||||
|
Self::SurfaceCapabilitiesFailed { device, result } => write!(
|
||||||
|
f,
|
||||||
|
"Vulkan device {device} surface capabilities query failed: {result:?}"
|
||||||
|
),
|
||||||
|
Self::PresentModesFailed { device, result } => write!(
|
||||||
|
f,
|
||||||
|
"Vulkan device {device} present mode query failed: {result:?}"
|
||||||
|
),
|
||||||
|
Self::Capability(error) => write!(f, "{error}"),
|
||||||
|
Self::Swapchain(error) => write!(f, "{error}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for VulkanRuntimeCapabilityError {}
|
||||||
|
|
||||||
|
pub(super) struct SelectedLiveDevice {
|
||||||
|
pub(super) physical_device: vk::PhysicalDevice,
|
||||||
|
pub(super) runtime: VulkanRuntimeCapabilityProbe,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiveDeviceCandidate {
|
||||||
|
physical_device: vk::PhysicalDevice,
|
||||||
|
capability: VulkanCapabilityReport,
|
||||||
|
surface_formats: Vec<VulkanSurfaceFormat>,
|
||||||
|
present_modes: Vec<i32>,
|
||||||
|
surface_capabilities: VulkanSwapchainSurfaceCapabilities,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Probes live Vulkan device, queue, surface and swapchain capabilities.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanRuntimeCapabilityError`] when device enumeration, surface
|
||||||
|
/// capability queries, Stage 0 device selection, or swapchain planning fails.
|
||||||
|
pub fn probe_vulkan_runtime_capabilities(
|
||||||
|
instance: &VulkanInstanceProbe,
|
||||||
|
surface: &VulkanSurfaceProbe,
|
||||||
|
drawable_extent: (u32, u32),
|
||||||
|
) -> Result<VulkanRuntimeCapabilityProbe, VulkanRuntimeCapabilityError> {
|
||||||
|
let selected = select_live_device_candidate_for_request(
|
||||||
|
instance,
|
||||||
|
surface,
|
||||||
|
drawable_extent,
|
||||||
|
RenderRequest::conservative(),
|
||||||
|
)?;
|
||||||
|
Ok(selected.runtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Probes live Vulkan device, queue, surface and swapchain capabilities for a
|
||||||
|
/// specific Stage 0 render request.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanRuntimeCapabilityError`] when device enumeration, surface
|
||||||
|
/// capability queries, Stage 0 device selection, or swapchain planning fails.
|
||||||
|
pub fn probe_vulkan_runtime_capabilities_for_request(
|
||||||
|
instance: &VulkanInstanceProbe,
|
||||||
|
surface: &VulkanSurfaceProbe,
|
||||||
|
drawable_extent: (u32, u32),
|
||||||
|
render_request: RenderRequest,
|
||||||
|
) -> Result<VulkanRuntimeCapabilityProbe, VulkanRuntimeCapabilityError> {
|
||||||
|
let selected = select_live_device_candidate_for_request(
|
||||||
|
instance,
|
||||||
|
surface,
|
||||||
|
drawable_extent,
|
||||||
|
render_request,
|
||||||
|
)?;
|
||||||
|
Ok(selected.runtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn select_live_device_candidate_for_request(
|
||||||
|
instance: &VulkanInstanceProbe,
|
||||||
|
surface: &VulkanSurfaceProbe,
|
||||||
|
drawable_extent: (u32, u32),
|
||||||
|
render_request: RenderRequest,
|
||||||
|
) -> Result<SelectedLiveDevice, VulkanRuntimeCapabilityError> {
|
||||||
|
let devices = {
|
||||||
|
// SAFETY: The Vulkan instance is live for this query and no handles are retained.
|
||||||
|
unsafe { instance.instance.enumerate_physical_devices() }.map_err(|error| {
|
||||||
|
VulkanRuntimeCapabilityError::EnumerateDevicesFailed { result: error }
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
let mut best: Option<LiveDeviceCandidate> = None;
|
||||||
|
let mut last_error = None;
|
||||||
|
for (index, device) in devices.iter().copied().enumerate() {
|
||||||
|
let candidate =
|
||||||
|
match live_device_candidate(instance, surface, device, index, render_request) {
|
||||||
|
Ok(candidate) => candidate,
|
||||||
|
Err(err) => {
|
||||||
|
last_error = Some(err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match &best {
|
||||||
|
Some(existing)
|
||||||
|
if compare_reports(&candidate.capability, &existing.capability)
|
||||||
|
!= std::cmp::Ordering::Greater => {}
|
||||||
|
_ => best = Some(candidate),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let best = best.ok_or_else(|| {
|
||||||
|
last_error.unwrap_or(VulkanRuntimeCapabilityError::Capability(
|
||||||
|
VulkanCapabilityError::NoPhysicalDevice,
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let swapchain = plan_vulkan_swapchain(&VulkanSwapchainRequest {
|
||||||
|
drawable_extent,
|
||||||
|
formats: best.surface_formats,
|
||||||
|
present_modes: best.present_modes,
|
||||||
|
capabilities: best.surface_capabilities,
|
||||||
|
preferred_present_mode: vk::PresentModeKHR::MAILBOX.as_raw(),
|
||||||
|
})
|
||||||
|
.map_err(VulkanRuntimeCapabilityError::Swapchain)?;
|
||||||
|
Ok(SelectedLiveDevice {
|
||||||
|
physical_device: best.physical_device,
|
||||||
|
runtime: VulkanRuntimeCapabilityProbe {
|
||||||
|
capability: best.capability,
|
||||||
|
swapchain,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn live_device_candidate(
|
||||||
|
instance: &VulkanInstanceProbe,
|
||||||
|
surface: &VulkanSurfaceProbe,
|
||||||
|
device: vk::PhysicalDevice,
|
||||||
|
index: usize,
|
||||||
|
render_request: RenderRequest,
|
||||||
|
) -> Result<LiveDeviceCandidate, VulkanRuntimeCapabilityError> {
|
||||||
|
let properties = {
|
||||||
|
// SAFETY: `device` was returned by this live instance and the result is copied by value.
|
||||||
|
unsafe { instance.instance.get_physical_device_properties(device) }
|
||||||
|
};
|
||||||
|
let name = physical_device_name(&properties, index);
|
||||||
|
let queue_properties = {
|
||||||
|
// SAFETY: `device` was returned by this live instance and the result is owned by Rust.
|
||||||
|
unsafe {
|
||||||
|
instance
|
||||||
|
.instance
|
||||||
|
.get_physical_device_queue_family_properties(device)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let extensions = live_device_extensions(instance, device, &name)?;
|
||||||
|
let surface_formats = live_surface_formats(surface, device, &name)?;
|
||||||
|
let present_modes = live_present_modes(surface, device, &name)?;
|
||||||
|
let surface_capabilities = live_surface_capabilities(surface, device, &name)?;
|
||||||
|
let supported_depth_stencil_formats = live_depth_stencil_formats(instance, device);
|
||||||
|
let sampled_image_formats = live_sampled_image_formats(instance, device);
|
||||||
|
let queue_families = queue_properties
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(queue_index, properties)| {
|
||||||
|
let index = u32::try_from(queue_index).unwrap_or(u32::MAX);
|
||||||
|
let present = {
|
||||||
|
// SAFETY: The physical device, surface and queue-family index are live query inputs.
|
||||||
|
unsafe {
|
||||||
|
surface.loader.get_physical_device_surface_support(
|
||||||
|
device,
|
||||||
|
index,
|
||||||
|
surface.surface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map_err(|error| VulkanRuntimeCapabilityError::PresentSupportFailed {
|
||||||
|
device: name.clone(),
|
||||||
|
queue_family: index,
|
||||||
|
result: error,
|
||||||
|
})?;
|
||||||
|
Ok(VulkanQueueFamily {
|
||||||
|
index,
|
||||||
|
graphics: properties.queue_flags.contains(vk::QueueFlags::GRAPHICS),
|
||||||
|
present,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, VulkanRuntimeCapabilityError>>()?;
|
||||||
|
let record = VulkanPhysicalDeviceRecord {
|
||||||
|
name,
|
||||||
|
api_version: properties.api_version,
|
||||||
|
device_type: match properties.device_type {
|
||||||
|
vk::PhysicalDeviceType::DISCRETE_GPU => VulkanDeviceType::DiscreteGpu,
|
||||||
|
vk::PhysicalDeviceType::INTEGRATED_GPU => VulkanDeviceType::IntegratedGpu,
|
||||||
|
vk::PhysicalDeviceType::CPU => VulkanDeviceType::Cpu,
|
||||||
|
_ => VulkanDeviceType::Other,
|
||||||
|
},
|
||||||
|
extensions,
|
||||||
|
queue_families,
|
||||||
|
surface_formats: surface_formats.clone(),
|
||||||
|
present_modes: present_modes.clone(),
|
||||||
|
surface_capabilities,
|
||||||
|
supported_depth_stencil_formats,
|
||||||
|
sampled_image_formats,
|
||||||
|
limits: VulkanDeviceLimits {
|
||||||
|
max_image_dimension_2d: properties.limits.max_image_dimension2_d,
|
||||||
|
max_sampler_allocation_count: properties.limits.max_sampler_allocation_count,
|
||||||
|
max_per_stage_descriptor_samplers: properties.limits.max_per_stage_descriptor_samplers,
|
||||||
|
max_bound_descriptor_sets: properties.limits.max_bound_descriptor_sets,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let capability = validate_device_for_request(&record, render_request)
|
||||||
|
.map_err(VulkanRuntimeCapabilityError::Capability)?;
|
||||||
|
Ok(LiveDeviceCandidate {
|
||||||
|
physical_device: device,
|
||||||
|
capability,
|
||||||
|
surface_formats,
|
||||||
|
present_modes,
|
||||||
|
surface_capabilities,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn unique_queue_families(graphics: u32, present: u32) -> Vec<u32> {
|
||||||
|
if graphics == present {
|
||||||
|
vec![graphics]
|
||||||
|
} else {
|
||||||
|
vec![graphics, present]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn physical_device_name(properties: &vk::PhysicalDeviceProperties, index: usize) -> String {
|
||||||
|
// SAFETY: Vulkan device names are fixed-size NUL-terminated C strings per the spec.
|
||||||
|
let name = unsafe { CStr::from_ptr(properties.device_name.as_ptr()) }
|
||||||
|
.to_string_lossy()
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
if name.is_empty() {
|
||||||
|
format!("physical-device-{index}")
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn live_device_extensions(
|
||||||
|
instance: &VulkanInstanceProbe,
|
||||||
|
device: vk::PhysicalDevice,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<Vec<String>, VulkanRuntimeCapabilityError> {
|
||||||
|
let properties = {
|
||||||
|
// SAFETY: `device` was returned by this live instance and no borrowed data escapes.
|
||||||
|
unsafe {
|
||||||
|
instance
|
||||||
|
.instance
|
||||||
|
.enumerate_device_extension_properties(device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map_err(
|
||||||
|
|error| VulkanRuntimeCapabilityError::EnumerateDeviceExtensionsFailed {
|
||||||
|
device: name.to_string(),
|
||||||
|
result: error,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let mut extensions = properties
|
||||||
|
.iter()
|
||||||
|
.map(|property| {
|
||||||
|
// SAFETY: Vulkan extension names are fixed-size NUL-terminated C strings per the spec.
|
||||||
|
unsafe { CStr::from_ptr(property.extension_name.as_ptr()) }
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
extensions.sort();
|
||||||
|
extensions.dedup();
|
||||||
|
Ok(extensions)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn live_surface_formats(
|
||||||
|
surface: &VulkanSurfaceProbe,
|
||||||
|
device: vk::PhysicalDevice,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<Vec<VulkanSurfaceFormat>, VulkanRuntimeCapabilityError> {
|
||||||
|
let formats = {
|
||||||
|
// SAFETY: The physical device and surface are live query inputs and no handles are retained.
|
||||||
|
unsafe {
|
||||||
|
surface
|
||||||
|
.loader
|
||||||
|
.get_physical_device_surface_formats(device, surface.surface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map_err(|error| VulkanRuntimeCapabilityError::SurfaceFormatsFailed {
|
||||||
|
device: name.to_string(),
|
||||||
|
result: error,
|
||||||
|
})?;
|
||||||
|
Ok(formats
|
||||||
|
.into_iter()
|
||||||
|
.map(|format| VulkanSurfaceFormat {
|
||||||
|
format: format.format.as_raw(),
|
||||||
|
color_space: format.color_space.as_raw(),
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn live_present_modes(
|
||||||
|
surface: &VulkanSurfaceProbe,
|
||||||
|
device: vk::PhysicalDevice,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<Vec<i32>, VulkanRuntimeCapabilityError> {
|
||||||
|
let modes = {
|
||||||
|
// SAFETY: The physical device and surface are live query inputs and no handles are retained.
|
||||||
|
unsafe {
|
||||||
|
surface
|
||||||
|
.loader
|
||||||
|
.get_physical_device_surface_present_modes(device, surface.surface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map_err(|error| VulkanRuntimeCapabilityError::PresentModesFailed {
|
||||||
|
device: name.to_string(),
|
||||||
|
result: error,
|
||||||
|
})?;
|
||||||
|
Ok(modes.into_iter().map(vk::PresentModeKHR::as_raw).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn live_surface_capabilities(
|
||||||
|
surface: &VulkanSurfaceProbe,
|
||||||
|
device: vk::PhysicalDevice,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<VulkanSwapchainSurfaceCapabilities, VulkanRuntimeCapabilityError> {
|
||||||
|
let capabilities = {
|
||||||
|
// SAFETY: The physical device and surface are live query inputs and no handles are retained.
|
||||||
|
unsafe {
|
||||||
|
surface
|
||||||
|
.loader
|
||||||
|
.get_physical_device_surface_capabilities(device, surface.surface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map_err(
|
||||||
|
|error| VulkanRuntimeCapabilityError::SurfaceCapabilitiesFailed {
|
||||||
|
device: name.to_string(),
|
||||||
|
result: error,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
Ok(VulkanSwapchainSurfaceCapabilities {
|
||||||
|
current_extent: if capabilities.current_extent.width == u32::MAX {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some((
|
||||||
|
capabilities.current_extent.width,
|
||||||
|
capabilities.current_extent.height,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
min_extent: (
|
||||||
|
capabilities.min_image_extent.width,
|
||||||
|
capabilities.min_image_extent.height,
|
||||||
|
),
|
||||||
|
max_extent: (
|
||||||
|
capabilities.max_image_extent.width,
|
||||||
|
capabilities.max_image_extent.height,
|
||||||
|
),
|
||||||
|
min_image_count: capabilities.min_image_count,
|
||||||
|
max_image_count: capabilities.max_image_count,
|
||||||
|
supported_usage_flags: capabilities.supported_usage_flags.as_raw(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn live_depth_stencil_formats(
|
||||||
|
instance: &VulkanInstanceProbe,
|
||||||
|
device: vk::PhysicalDevice,
|
||||||
|
) -> Vec<i32> {
|
||||||
|
[
|
||||||
|
vk::Format::D16_UNORM,
|
||||||
|
vk::Format::X8_D24_UNORM_PACK32,
|
||||||
|
vk::Format::D32_SFLOAT,
|
||||||
|
vk::Format::S8_UINT,
|
||||||
|
vk::Format::D16_UNORM_S8_UINT,
|
||||||
|
vk::Format::D24_UNORM_S8_UINT,
|
||||||
|
vk::Format::D32_SFLOAT_S8_UINT,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.filter(|format| {
|
||||||
|
let properties = {
|
||||||
|
// SAFETY: `device` belongs to `instance`; format-property queries copy data by value.
|
||||||
|
unsafe {
|
||||||
|
instance
|
||||||
|
.instance
|
||||||
|
.get_physical_device_format_properties(device, *format)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
properties
|
||||||
|
.optimal_tiling_features
|
||||||
|
.contains(vk::FormatFeatureFlags::DEPTH_STENCIL_ATTACHMENT)
|
||||||
|
})
|
||||||
|
.map(vk::Format::as_raw)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn live_sampled_image_formats(
|
||||||
|
instance: &VulkanInstanceProbe,
|
||||||
|
device: vk::PhysicalDevice,
|
||||||
|
) -> Vec<i32> {
|
||||||
|
[
|
||||||
|
vk::Format::R8G8B8A8_SRGB,
|
||||||
|
vk::Format::B8G8R8A8_SRGB,
|
||||||
|
vk::Format::D16_UNORM,
|
||||||
|
vk::Format::D32_SFLOAT,
|
||||||
|
vk::Format::D24_UNORM_S8_UINT,
|
||||||
|
vk::Format::D32_SFLOAT_S8_UINT,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.filter(|format| {
|
||||||
|
let properties = {
|
||||||
|
// SAFETY: `device` belongs to `instance`; format-property queries copy data by value.
|
||||||
|
unsafe {
|
||||||
|
instance
|
||||||
|
.instance
|
||||||
|
.get_physical_device_format_properties(device, *format)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
properties
|
||||||
|
.optimal_tiling_features
|
||||||
|
.contains(vk::FormatFeatureFlags::SAMPLED_IMAGE)
|
||||||
|
})
|
||||||
|
.map(vk::Format::as_raw)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
#![allow(unsafe_code)]
|
||||||
|
|
||||||
|
use ash::vk;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::ffi::{CStr, CString};
|
||||||
|
use std::os::raw::c_char;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
EXT_DEBUG_UTILS_EXTENSION, KHR_PORTABILITY_ENUMERATION_EXTENSION, MIN_VULKAN_API_VERSION,
|
||||||
|
VALIDATION_LAYER_NAME,
|
||||||
|
};
|
||||||
|
use crate::policy::{format_api_version, serialize_json_or_fallback};
|
||||||
|
|
||||||
|
/// Vulkan instance bootstrap configuration.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanInstanceConfig {
|
||||||
|
/// Application name reported to the loader.
|
||||||
|
pub application_name: String,
|
||||||
|
/// Required instance extensions, usually including surface extensions.
|
||||||
|
pub required_extensions: Vec<String>,
|
||||||
|
/// Whether `VK_KHR_portability_enumeration` and its create flag are enabled.
|
||||||
|
pub enable_portability_enumeration: bool,
|
||||||
|
/// Whether validation layers are requested.
|
||||||
|
pub enable_validation: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VulkanInstanceConfig {
|
||||||
|
/// Returns a conservative instance configuration for smoke probes.
|
||||||
|
#[must_use]
|
||||||
|
pub fn smoke(application_name: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
application_name: application_name.into(),
|
||||||
|
required_extensions: Vec::new(),
|
||||||
|
enable_portability_enumeration: cfg!(target_os = "macos"),
|
||||||
|
enable_validation: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deterministic Vulkan instance creation plan.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanInstancePlan {
|
||||||
|
/// Report schema version.
|
||||||
|
pub schema: u32,
|
||||||
|
/// Instance extensions requested at creation time.
|
||||||
|
pub enabled_extensions: Vec<String>,
|
||||||
|
/// Raw Vulkan instance creation flags.
|
||||||
|
pub create_flags: u32,
|
||||||
|
/// Whether validation was requested.
|
||||||
|
pub validation_requested: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Created Vulkan instance probe.
|
||||||
|
pub struct VulkanInstanceProbe {
|
||||||
|
pub(super) entry: ash::Entry,
|
||||||
|
pub(super) instance: ash::Instance,
|
||||||
|
/// Deterministic instance creation report.
|
||||||
|
pub report: VulkanInstancePlan,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for VulkanInstanceProbe {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// SAFETY: The `Instance` was created by this probe and is destroyed once during drop.
|
||||||
|
unsafe { self.instance.destroy_instance(None) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vulkan instance bootstrap error.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum VulkanInstanceError {
|
||||||
|
/// The Vulkan loader could not be opened.
|
||||||
|
Loader(VulkanLoaderError),
|
||||||
|
/// Application name contained an interior NUL byte.
|
||||||
|
InvalidApplicationName,
|
||||||
|
/// An extension name contained an interior NUL byte.
|
||||||
|
InvalidExtensionName {
|
||||||
|
/// Invalid extension name.
|
||||||
|
extension: String,
|
||||||
|
},
|
||||||
|
/// A required instance extension is unavailable from the loader.
|
||||||
|
MissingInstanceExtension {
|
||||||
|
/// Required extension name.
|
||||||
|
extension: String,
|
||||||
|
},
|
||||||
|
/// Validation layers were requested but unavailable.
|
||||||
|
MissingValidationLayer,
|
||||||
|
/// Instance creation failed.
|
||||||
|
CreateFailed {
|
||||||
|
/// Vulkan result.
|
||||||
|
result: vk::Result,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for VulkanInstanceError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Loader(error) => write!(f, "{error}"),
|
||||||
|
Self::InvalidApplicationName => {
|
||||||
|
write!(f, "Vulkan application name contains an interior NUL byte")
|
||||||
|
}
|
||||||
|
Self::InvalidExtensionName { extension } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Vulkan instance extension name contains an interior NUL byte: {extension:?}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Self::MissingInstanceExtension { extension } => {
|
||||||
|
write!(f, "Vulkan instance extension {extension} is unavailable")
|
||||||
|
}
|
||||||
|
Self::MissingValidationLayer => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Vulkan validation layer VK_LAYER_KHRONOS_validation is unavailable"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Self::CreateFailed { result } => {
|
||||||
|
write!(f, "Vulkan instance creation failed: {result:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for VulkanInstanceError {}
|
||||||
|
|
||||||
|
/// Deterministic Vulkan loader probe report.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanLoaderProbeReport {
|
||||||
|
/// Report schema version.
|
||||||
|
pub schema: u32,
|
||||||
|
/// Whether the Vulkan loader was opened successfully.
|
||||||
|
pub loader_available: bool,
|
||||||
|
/// Reported loader instance API version.
|
||||||
|
pub instance_api_version: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vulkan loader bootstrap error.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum VulkanLoaderError {
|
||||||
|
/// The Vulkan loader library could not be opened.
|
||||||
|
Unavailable {
|
||||||
|
/// Loader error text.
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for VulkanLoaderError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Unavailable { message } => {
|
||||||
|
write!(f, "Vulkan loader is unavailable: {message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for VulkanLoaderError {}
|
||||||
|
|
||||||
|
/// Builds the deterministic instance creation plan without touching the loader.
|
||||||
|
#[must_use]
|
||||||
|
pub fn plan_vulkan_instance(config: &VulkanInstanceConfig) -> VulkanInstancePlan {
|
||||||
|
let mut enabled_extensions = config.required_extensions.clone();
|
||||||
|
if config.enable_validation
|
||||||
|
&& !enabled_extensions
|
||||||
|
.iter()
|
||||||
|
.any(|extension| extension == EXT_DEBUG_UTILS_EXTENSION)
|
||||||
|
{
|
||||||
|
enabled_extensions.push(EXT_DEBUG_UTILS_EXTENSION.to_string());
|
||||||
|
}
|
||||||
|
if config.enable_portability_enumeration
|
||||||
|
&& !enabled_extensions
|
||||||
|
.iter()
|
||||||
|
.any(|extension| extension == KHR_PORTABILITY_ENUMERATION_EXTENSION)
|
||||||
|
{
|
||||||
|
enabled_extensions.push(KHR_PORTABILITY_ENUMERATION_EXTENSION.to_string());
|
||||||
|
}
|
||||||
|
enabled_extensions.sort();
|
||||||
|
enabled_extensions.dedup();
|
||||||
|
VulkanInstancePlan {
|
||||||
|
schema: 1,
|
||||||
|
enabled_extensions,
|
||||||
|
create_flags: if config.enable_portability_enumeration {
|
||||||
|
vk::InstanceCreateFlags::ENUMERATE_PORTABILITY_KHR.as_raw()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
},
|
||||||
|
validation_requested: config.enable_validation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a Vulkan instance probe from the supplied configuration.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanInstanceError`] when the loader is unavailable, names are not
|
||||||
|
/// valid C strings, or `vkCreateInstance` fails.
|
||||||
|
pub fn create_vulkan_instance_probe(
|
||||||
|
config: &VulkanInstanceConfig,
|
||||||
|
) -> Result<VulkanInstanceProbe, VulkanInstanceError> {
|
||||||
|
// SAFETY: Loading the entry only resolves loader symbols; no raw Vulkan handles escape.
|
||||||
|
let entry = unsafe { ash::Entry::load() }.map_err(|error| {
|
||||||
|
VulkanInstanceError::Loader(VulkanLoaderError::Unavailable {
|
||||||
|
message: error.to_string(),
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
let app_name = CString::new(config.application_name.clone())
|
||||||
|
.map_err(|_| VulkanInstanceError::InvalidApplicationName)?;
|
||||||
|
let engine_name = c"fparkan";
|
||||||
|
let plan = plan_vulkan_instance(config);
|
||||||
|
let available_extensions = available_instance_extensions(&entry)?;
|
||||||
|
ensure_instance_extensions_available(&plan.enabled_extensions, &available_extensions)?;
|
||||||
|
let extension_names = cstring_vec(&plan.enabled_extensions)?;
|
||||||
|
let extension_ptrs = cstring_ptrs(&extension_names);
|
||||||
|
let layer_names = validation_layer_cstrings(&entry, config.enable_validation)?;
|
||||||
|
let layer_ptrs = cstring_ptrs(&layer_names);
|
||||||
|
let app_info = vk::ApplicationInfo::default()
|
||||||
|
.application_name(&app_name)
|
||||||
|
.application_version(0)
|
||||||
|
.engine_name(engine_name)
|
||||||
|
.engine_version(0)
|
||||||
|
.api_version(MIN_VULKAN_API_VERSION);
|
||||||
|
let create_info = vk::InstanceCreateInfo::default()
|
||||||
|
.application_info(&app_info)
|
||||||
|
.enabled_extension_names(&extension_ptrs)
|
||||||
|
.enabled_layer_names(&layer_ptrs)
|
||||||
|
.flags(vk::InstanceCreateFlags::from_raw(plan.create_flags));
|
||||||
|
// SAFETY: `create_info` points to stack-owned Vulkan create data that lives for the call.
|
||||||
|
let instance = unsafe { entry.create_instance(&create_info, None) }
|
||||||
|
.map_err(|error| VulkanInstanceError::CreateFailed { result: error })?;
|
||||||
|
Ok(VulkanInstanceProbe {
|
||||||
|
entry,
|
||||||
|
instance,
|
||||||
|
report: plan,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a deterministic JSON Vulkan instance plan.
|
||||||
|
#[must_use]
|
||||||
|
pub fn render_instance_plan_json(plan: &VulkanInstancePlan) -> String {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct InstancePlanJson<'a> {
|
||||||
|
schema: u32,
|
||||||
|
create_flags: u32,
|
||||||
|
validation_requested: bool,
|
||||||
|
enabled_extensions: &'a [String],
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize_json_or_fallback(
|
||||||
|
&InstancePlanJson {
|
||||||
|
schema: plan.schema,
|
||||||
|
create_flags: plan.create_flags,
|
||||||
|
validation_requested: plan.validation_requested,
|
||||||
|
enabled_extensions: &plan.enabled_extensions,
|
||||||
|
},
|
||||||
|
"{\"schema\":0,\"create_flags\":0,\"validation_requested\":false,\"enabled_extensions\":[]}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens the Vulkan loader and reports the supported instance API version.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanLoaderError`] when no Vulkan loader library can be opened on
|
||||||
|
/// the host.
|
||||||
|
pub fn probe_vulkan_loader() -> Result<VulkanLoaderProbeReport, VulkanLoaderError> {
|
||||||
|
// SAFETY: Loading the entry only resolves loader symbols; no raw Vulkan handles escape.
|
||||||
|
let entry = unsafe { ash::Entry::load() }.map_err(|error| VulkanLoaderError::Unavailable {
|
||||||
|
message: error.to_string(),
|
||||||
|
})?;
|
||||||
|
// SAFETY: The resolved entry only queries the loader-supported instance API version.
|
||||||
|
let version = unsafe { entry.try_enumerate_instance_version() }
|
||||||
|
.map_err(|error| VulkanLoaderError::Unavailable {
|
||||||
|
message: error.to_string(),
|
||||||
|
})?
|
||||||
|
.unwrap_or(vk::API_VERSION_1_0);
|
||||||
|
Ok(VulkanLoaderProbeReport {
|
||||||
|
schema: 1,
|
||||||
|
loader_available: true,
|
||||||
|
instance_api_version: version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the static Vulkan entry name used by loader probes.
|
||||||
|
#[must_use]
|
||||||
|
pub fn vulkan_entry_symbol_name() -> &'static CStr {
|
||||||
|
c"vkGetInstanceProcAddr"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a deterministic JSON Vulkan loader report.
|
||||||
|
#[must_use]
|
||||||
|
pub fn render_loader_probe_report_json(report: &VulkanLoaderProbeReport) -> String {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct LoaderProbeReportJson {
|
||||||
|
schema: u32,
|
||||||
|
loader_available: bool,
|
||||||
|
instance_api: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize_json_or_fallback(
|
||||||
|
&LoaderProbeReportJson {
|
||||||
|
schema: report.schema,
|
||||||
|
loader_available: report.loader_available,
|
||||||
|
instance_api: format_api_version(report.instance_api_version),
|
||||||
|
},
|
||||||
|
"{\"schema\":0,\"loader_available\":false,\"instance_api\":\"0.0.0\"}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn available_instance_extensions(entry: &ash::Entry) -> Result<Vec<String>, VulkanInstanceError> {
|
||||||
|
let available_extensions =
|
||||||
|
// SAFETY: Enumerating instance extensions reads loader-owned immutable metadata.
|
||||||
|
unsafe { entry.enumerate_instance_extension_properties(None) }.map_err(|error| {
|
||||||
|
VulkanInstanceError::CreateFailed {
|
||||||
|
result: error,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
available_extensions
|
||||||
|
.into_iter()
|
||||||
|
.map(|extension| {
|
||||||
|
// SAFETY: Vulkan extension names are fixed-size NUL-terminated strings from the loader.
|
||||||
|
Ok(unsafe { CStr::from_ptr(extension.extension_name.as_ptr()) }
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn ensure_instance_extensions_available(
|
||||||
|
required_extensions: &[String],
|
||||||
|
available_extensions: &[String],
|
||||||
|
) -> Result<(), VulkanInstanceError> {
|
||||||
|
let available = available_extensions
|
||||||
|
.iter()
|
||||||
|
.map(String::as_str)
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
for extension in required_extensions {
|
||||||
|
if !available.contains(extension.as_str()) {
|
||||||
|
return Err(VulkanInstanceError::MissingInstanceExtension {
|
||||||
|
extension: extension.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validation_layer_cstrings(
|
||||||
|
entry: &ash::Entry,
|
||||||
|
enable_validation: bool,
|
||||||
|
) -> Result<Vec<CString>, VulkanInstanceError> {
|
||||||
|
if !enable_validation {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
let available_layers =
|
||||||
|
// SAFETY: Enumerating instance layers reads loader-owned immutable metadata.
|
||||||
|
unsafe { entry.enumerate_instance_layer_properties() }.map_err(|error| {
|
||||||
|
VulkanInstanceError::CreateFailed {
|
||||||
|
result: error,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
let validation_available = available_layers.iter().any(|layer| {
|
||||||
|
// SAFETY: Vulkan layer names are fixed-size NUL-terminated strings from the loader.
|
||||||
|
unsafe { CStr::from_ptr(layer.layer_name.as_ptr()) }
|
||||||
|
.to_string_lossy()
|
||||||
|
.as_ref()
|
||||||
|
== VALIDATION_LAYER_NAME
|
||||||
|
});
|
||||||
|
if !validation_available {
|
||||||
|
return Err(VulkanInstanceError::MissingValidationLayer);
|
||||||
|
}
|
||||||
|
Ok(vec![CString::new(VALIDATION_LAYER_NAME).map_err(|_| {
|
||||||
|
VulkanInstanceError::InvalidApplicationName
|
||||||
|
})?])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn cstring_vec(values: &[String]) -> Result<Vec<CString>, VulkanInstanceError> {
|
||||||
|
values
|
||||||
|
.iter()
|
||||||
|
.map(|extension| {
|
||||||
|
CString::new(extension.as_str()).map_err(|_| {
|
||||||
|
VulkanInstanceError::InvalidExtensionName {
|
||||||
|
extension: extension.clone(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cstring_ptrs(values: &[CString]) -> Vec<*const c_char> {
|
||||||
|
values.iter().map(|value| value.as_ptr()).collect()
|
||||||
|
}
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
#![allow(unsafe_code)]
|
||||||
|
|
||||||
|
use ash::vk;
|
||||||
|
|
||||||
|
use super::{VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSmokeRendererError};
|
||||||
|
|
||||||
|
pub(super) struct VulkanAllocatedBuffer {
|
||||||
|
pub(super) buffer: vk::Buffer,
|
||||||
|
pub(super) memory: vk::DeviceMemory,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct VulkanFrameSync {
|
||||||
|
pub(super) image_available: vk::Semaphore,
|
||||||
|
pub(super) render_finished: vk::Semaphore,
|
||||||
|
pub(super) fence: vk::Fence,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn create_command_pool(
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
) -> Result<vk::CommandPool, VulkanSmokeRendererError> {
|
||||||
|
let create_info = vk::CommandPoolCreateInfo::default()
|
||||||
|
.queue_family_index(device.report.graphics_queue_family)
|
||||||
|
.flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER);
|
||||||
|
// SAFETY: The queue-family index belongs to this live logical device.
|
||||||
|
unsafe { device.device().create_command_pool(&create_info, None) }.map_err(|error| {
|
||||||
|
VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkCreateCommandPool",
|
||||||
|
result: error,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn create_triangle_vertex_buffer(
|
||||||
|
instance: &VulkanInstanceProbe,
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
|
||||||
|
let vertices: [[f32; 5]; 3] = [
|
||||||
|
[0.0, -0.55, 1.0, 0.2, 0.2],
|
||||||
|
[0.55, 0.55, 0.2, 1.0, 0.2],
|
||||||
|
[-0.55, 0.55, 0.2, 0.4, 1.0],
|
||||||
|
];
|
||||||
|
let mut bytes = Vec::with_capacity(vertices.len() * 5 * std::mem::size_of::<f32>());
|
||||||
|
for vertex in vertices {
|
||||||
|
for value in vertex {
|
||||||
|
bytes.extend_from_slice(&value.to_ne_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
create_host_visible_buffer(
|
||||||
|
instance,
|
||||||
|
device,
|
||||||
|
&bytes,
|
||||||
|
vk::BufferUsageFlags::VERTEX_BUFFER,
|
||||||
|
"triangle vertex buffer",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn create_triangle_index_buffer(
|
||||||
|
instance: &VulkanInstanceProbe,
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
|
||||||
|
let indices = [0_u16, 1_u16, 2_u16];
|
||||||
|
let mut bytes = Vec::with_capacity(indices.len() * std::mem::size_of::<u16>());
|
||||||
|
for index in indices {
|
||||||
|
bytes.extend_from_slice(&index.to_ne_bytes());
|
||||||
|
}
|
||||||
|
create_host_visible_buffer(
|
||||||
|
instance,
|
||||||
|
device,
|
||||||
|
&bytes,
|
||||||
|
vk::BufferUsageFlags::INDEX_BUFFER,
|
||||||
|
"triangle index buffer",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_host_visible_buffer(
|
||||||
|
instance: &VulkanInstanceProbe,
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
bytes: &[u8],
|
||||||
|
usage: vk::BufferUsageFlags,
|
||||||
|
context: &'static str,
|
||||||
|
) -> Result<VulkanAllocatedBuffer, VulkanSmokeRendererError> {
|
||||||
|
let create_info = vk::BufferCreateInfo::default()
|
||||||
|
.size(bytes.len().try_into().unwrap_or(u64::MAX))
|
||||||
|
.usage(usage)
|
||||||
|
.sharing_mode(vk::SharingMode::EXCLUSIVE);
|
||||||
|
// SAFETY: The create info is stack-owned and references no external memory.
|
||||||
|
let buffer = unsafe { device.device().create_buffer(&create_info, None) }.map_err(|error| {
|
||||||
|
VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context,
|
||||||
|
result: error,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
// SAFETY: The buffer belongs to this device and is queried immediately after creation.
|
||||||
|
let requirements = unsafe { device.device().get_buffer_memory_requirements(buffer) };
|
||||||
|
let Some(memory_type_index) = find_memory_type(
|
||||||
|
instance,
|
||||||
|
device.physical_device(),
|
||||||
|
requirements.memory_type_bits,
|
||||||
|
vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT,
|
||||||
|
) else {
|
||||||
|
// SAFETY: The buffer was created above on this logical device and is destroyed on setup failure.
|
||||||
|
unsafe { device.device().destroy_buffer(buffer, None) };
|
||||||
|
return Err(VulkanSmokeRendererError::MissingMemoryType { context });
|
||||||
|
};
|
||||||
|
let allocate_info = vk::MemoryAllocateInfo::default()
|
||||||
|
.allocation_size(requirements.size)
|
||||||
|
.memory_type_index(memory_type_index);
|
||||||
|
let memory =
|
||||||
|
// SAFETY: The allocation request matches the queried memory requirements for this buffer.
|
||||||
|
unsafe { device.device().allocate_memory(&allocate_info, None) }.map_err(|error| {
|
||||||
|
// SAFETY: The buffer was created above on this logical device and is destroyed on setup failure.
|
||||||
|
unsafe { device.device().destroy_buffer(buffer, None) };
|
||||||
|
VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context,
|
||||||
|
result: error,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
// SAFETY: The allocation satisfies the queried buffer memory requirements for this device.
|
||||||
|
unsafe { device.device().bind_buffer_memory(buffer, memory, 0) }.map_err(|error| {
|
||||||
|
// SAFETY: The buffer and allocation were created above on this logical device and are destroyed on setup failure.
|
||||||
|
unsafe {
|
||||||
|
device.device().destroy_buffer(buffer, None);
|
||||||
|
device.device().free_memory(memory, None);
|
||||||
|
}
|
||||||
|
VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context,
|
||||||
|
result: error,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
// SAFETY: The mapping range is within the host-visible allocation bound to the buffer.
|
||||||
|
let mapped = unsafe {
|
||||||
|
device
|
||||||
|
.device()
|
||||||
|
.map_memory(memory, 0, requirements.size, vk::MemoryMapFlags::empty())
|
||||||
|
}
|
||||||
|
.map_err(|error| {
|
||||||
|
// SAFETY: The buffer and allocation were created above on this logical device and are destroyed on setup failure.
|
||||||
|
unsafe {
|
||||||
|
device.device().destroy_buffer(buffer, None);
|
||||||
|
device.device().free_memory(memory, None);
|
||||||
|
}
|
||||||
|
VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context,
|
||||||
|
result: error,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
// SAFETY: The destination points to the mapped allocation and the source slice lives for the copy.
|
||||||
|
unsafe {
|
||||||
|
std::ptr::copy_nonoverlapping(bytes.as_ptr(), mapped.cast::<u8>(), bytes.len());
|
||||||
|
device.device().unmap_memory(memory);
|
||||||
|
}
|
||||||
|
Ok(VulkanAllocatedBuffer { buffer, memory })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_memory_type(
|
||||||
|
instance: &VulkanInstanceProbe,
|
||||||
|
physical_device: vk::PhysicalDevice,
|
||||||
|
type_bits: u32,
|
||||||
|
required: vk::MemoryPropertyFlags,
|
||||||
|
) -> Option<u32> {
|
||||||
|
let properties =
|
||||||
|
// SAFETY: The physical device was selected from this live instance and queried by value.
|
||||||
|
unsafe {
|
||||||
|
instance
|
||||||
|
.instance
|
||||||
|
.get_physical_device_memory_properties(physical_device)
|
||||||
|
};
|
||||||
|
let count = usize::try_from(properties.memory_type_count).unwrap_or(0);
|
||||||
|
properties.memory_types[..count]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find_map(|(index, memory_type)| {
|
||||||
|
let index_u32 = u32::try_from(index).ok()?;
|
||||||
|
let supported = (type_bits & (1_u32 << index_u32)) != 0;
|
||||||
|
(supported && memory_type.property_flags.contains(required)).then_some(index_u32)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn create_frame_sync(
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
) -> Result<Vec<VulkanFrameSync>, VulkanSmokeRendererError> {
|
||||||
|
let semaphore_info = vk::SemaphoreCreateInfo::default();
|
||||||
|
let fence_info = vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED);
|
||||||
|
let mut sync = Vec::with_capacity(2);
|
||||||
|
for _ in 0..2 {
|
||||||
|
// SAFETY: The sync objects belong to this live logical device and are destroyed at teardown.
|
||||||
|
let image_available = unsafe { device.device().create_semaphore(&semaphore_info, None) }
|
||||||
|
.map_err(|error| VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkCreateSemaphore(image_available)",
|
||||||
|
result: error,
|
||||||
|
})?;
|
||||||
|
let render_finished = {
|
||||||
|
// SAFETY: The sync objects belong to this live logical device and are destroyed at teardown.
|
||||||
|
match unsafe { device.device().create_semaphore(&semaphore_info, None) } {
|
||||||
|
Ok(render_finished) => render_finished,
|
||||||
|
Err(error) => {
|
||||||
|
destroy_frame_sync_objects(device, &sync);
|
||||||
|
// SAFETY: The semaphore was created above on this logical device and is destroyed on setup failure.
|
||||||
|
unsafe { device.device().destroy_semaphore(image_available, None) };
|
||||||
|
return Err(VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkCreateSemaphore(render_finished)",
|
||||||
|
result: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// SAFETY: The fence belongs to this live logical device and is destroyed at teardown.
|
||||||
|
let fence = match unsafe { device.device().create_fence(&fence_info, None) } {
|
||||||
|
Ok(fence) => fence,
|
||||||
|
Err(error) => {
|
||||||
|
destroy_frame_sync_objects(device, &sync);
|
||||||
|
// SAFETY: These semaphores were created above on this logical device and are destroyed on setup failure.
|
||||||
|
unsafe {
|
||||||
|
device.device().destroy_semaphore(image_available, None);
|
||||||
|
device.device().destroy_semaphore(render_finished, None);
|
||||||
|
}
|
||||||
|
return Err(VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkCreateFence",
|
||||||
|
result: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sync.push(VulkanFrameSync {
|
||||||
|
image_available,
|
||||||
|
render_finished,
|
||||||
|
fence,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(sync)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroy_frame_sync_objects(device: &VulkanLogicalDeviceProbe, sync: &[VulkanFrameSync]) {
|
||||||
|
for frame_sync in sync {
|
||||||
|
// SAFETY: These sync objects belong to this live logical device and are destroyed once during teardown.
|
||||||
|
unsafe {
|
||||||
|
device
|
||||||
|
.device()
|
||||||
|
.destroy_semaphore(frame_sync.image_available, None);
|
||||||
|
device
|
||||||
|
.device()
|
||||||
|
.destroy_semaphore(frame_sync.render_finished, None);
|
||||||
|
device.device().destroy_fence(frame_sync.fence, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn destroy_allocated_buffer(
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
buffer: &VulkanAllocatedBuffer,
|
||||||
|
) {
|
||||||
|
// SAFETY: The buffer and allocation belong to this live logical device and are destroyed once during teardown.
|
||||||
|
unsafe {
|
||||||
|
device.device().destroy_buffer(buffer.buffer, None);
|
||||||
|
device.device().free_memory(buffer.memory, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn color_subresource_range() -> vk::ImageSubresourceRange {
|
||||||
|
vk::ImageSubresourceRange::default()
|
||||||
|
.aspect_mask(vk::ImageAspectFlags::COLOR)
|
||||||
|
.base_mip_level(0)
|
||||||
|
.level_count(1)
|
||||||
|
.base_array_layer(0)
|
||||||
|
.layer_count(1)
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
#![allow(unsafe_code)]
|
||||||
|
|
||||||
|
use ash::vk;
|
||||||
|
use fparkan_platform::RenderRequest;
|
||||||
|
use std::ffi::CString;
|
||||||
|
|
||||||
|
use super::capabilities::{
|
||||||
|
select_live_device_candidate_for_request, unique_queue_families, VulkanRuntimeCapabilityError,
|
||||||
|
VulkanRuntimeCapabilityProbe,
|
||||||
|
};
|
||||||
|
use super::{VulkanInstanceProbe, VulkanSurfaceProbe};
|
||||||
|
|
||||||
|
/// Created Vulkan logical device probe.
|
||||||
|
pub struct VulkanLogicalDeviceProbe {
|
||||||
|
device: ash::Device,
|
||||||
|
physical_device: vk::PhysicalDevice,
|
||||||
|
/// Runtime capability report used for device selection.
|
||||||
|
pub runtime: VulkanRuntimeCapabilityProbe,
|
||||||
|
/// Deterministic logical device creation report.
|
||||||
|
pub report: VulkanLogicalDeviceReport,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for VulkanLogicalDeviceProbe {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// SAFETY: The logical device was created by this probe and is destroyed once during drop.
|
||||||
|
unsafe { self.device.destroy_device(None) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VulkanLogicalDeviceProbe {
|
||||||
|
/// Returns the graphics queue selected by the Stage 0 policy.
|
||||||
|
#[must_use]
|
||||||
|
pub fn graphics_queue(&self) -> vk::Queue {
|
||||||
|
// SAFETY: The queue-family index belongs to this live logical device.
|
||||||
|
unsafe {
|
||||||
|
self.device
|
||||||
|
.get_device_queue(self.report.graphics_queue_family, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the presentation queue selected by the Stage 0 policy.
|
||||||
|
#[must_use]
|
||||||
|
pub fn present_queue(&self) -> vk::Queue {
|
||||||
|
// SAFETY: The queue-family index belongs to this live logical device.
|
||||||
|
unsafe {
|
||||||
|
self.device
|
||||||
|
.get_device_queue(self.report.present_queue_family, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a shared reference to the live logical device.
|
||||||
|
#[must_use]
|
||||||
|
pub fn device(&self) -> &ash::Device {
|
||||||
|
&self.device
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the selected physical device handle.
|
||||||
|
#[must_use]
|
||||||
|
pub fn physical_device(&self) -> vk::PhysicalDevice {
|
||||||
|
self.physical_device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Logical device creation report.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanLogicalDeviceReport {
|
||||||
|
/// Report schema version.
|
||||||
|
pub schema: u32,
|
||||||
|
/// Selected physical device name.
|
||||||
|
pub device_name: String,
|
||||||
|
/// Graphics queue-family index used by the logical device.
|
||||||
|
pub graphics_queue_family: u32,
|
||||||
|
/// Present queue-family index used by the logical device.
|
||||||
|
pub present_queue_family: u32,
|
||||||
|
/// Enabled device extensions.
|
||||||
|
pub enabled_extensions: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vulkan logical device creation error.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum VulkanLogicalDeviceError {
|
||||||
|
/// Runtime capability probing failed.
|
||||||
|
Runtime(VulkanRuntimeCapabilityError),
|
||||||
|
/// Device extension name contained an interior NUL byte.
|
||||||
|
InvalidExtensionName {
|
||||||
|
/// Invalid extension name.
|
||||||
|
extension: String,
|
||||||
|
},
|
||||||
|
/// Logical device creation failed.
|
||||||
|
CreateFailed {
|
||||||
|
/// Selected device name.
|
||||||
|
device: String,
|
||||||
|
/// Vulkan result.
|
||||||
|
result: vk::Result,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for VulkanLogicalDeviceError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Runtime(error) => write!(f, "{error}"),
|
||||||
|
Self::InvalidExtensionName { extension } => write!(
|
||||||
|
f,
|
||||||
|
"Vulkan device extension name contains an interior NUL byte: {extension:?}"
|
||||||
|
),
|
||||||
|
Self::CreateFailed { device, result } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Vulkan logical device creation failed for {device}: {result:?}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for VulkanLogicalDeviceError {}
|
||||||
|
|
||||||
|
/// Creates a Vulkan logical device for the selected live surface-capable device.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanLogicalDeviceError`] when runtime capability probing fails,
|
||||||
|
/// device extension names are invalid, or `vkCreateDevice` fails.
|
||||||
|
pub fn create_vulkan_logical_device_probe(
|
||||||
|
instance: &VulkanInstanceProbe,
|
||||||
|
surface: &VulkanSurfaceProbe,
|
||||||
|
drawable_extent: (u32, u32),
|
||||||
|
) -> Result<VulkanLogicalDeviceProbe, VulkanLogicalDeviceError> {
|
||||||
|
create_vulkan_logical_device_probe_for_request(
|
||||||
|
instance,
|
||||||
|
surface,
|
||||||
|
drawable_extent,
|
||||||
|
RenderRequest::conservative(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a Vulkan logical device for a specific Stage 0 render request.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanLogicalDeviceError`] when runtime capability probing fails,
|
||||||
|
/// device extension names are invalid, or `vkCreateDevice` fails.
|
||||||
|
pub fn create_vulkan_logical_device_probe_for_request(
|
||||||
|
instance: &VulkanInstanceProbe,
|
||||||
|
surface: &VulkanSurfaceProbe,
|
||||||
|
drawable_extent: (u32, u32),
|
||||||
|
render_request: RenderRequest,
|
||||||
|
) -> Result<VulkanLogicalDeviceProbe, VulkanLogicalDeviceError> {
|
||||||
|
let selected = select_live_device_candidate_for_request(
|
||||||
|
instance,
|
||||||
|
surface,
|
||||||
|
drawable_extent,
|
||||||
|
render_request,
|
||||||
|
)
|
||||||
|
.map_err(VulkanLogicalDeviceError::Runtime)?;
|
||||||
|
let capability = &selected.runtime.capability;
|
||||||
|
let queue_priorities = [1.0_f32];
|
||||||
|
let queue_families = unique_queue_families(
|
||||||
|
capability.graphics_queue_family,
|
||||||
|
capability.present_queue_family,
|
||||||
|
);
|
||||||
|
let queue_infos = queue_families
|
||||||
|
.iter()
|
||||||
|
.map(|queue_family| {
|
||||||
|
vk::DeviceQueueCreateInfo::default()
|
||||||
|
.queue_family_index(*queue_family)
|
||||||
|
.queue_priorities(&queue_priorities)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let extension_names = device_extension_cstrings(&capability.enabled_extensions)
|
||||||
|
.map_err(|extension| VulkanLogicalDeviceError::InvalidExtensionName { extension })?;
|
||||||
|
let extension_ptrs = extension_names
|
||||||
|
.iter()
|
||||||
|
.map(|extension| extension.as_ptr())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let create_info = vk::DeviceCreateInfo::default()
|
||||||
|
.queue_create_infos(&queue_infos)
|
||||||
|
.enabled_extension_names(&extension_ptrs);
|
||||||
|
// SAFETY: `selected.physical_device` belongs to `instance`; create data lives for the call.
|
||||||
|
let device = unsafe {
|
||||||
|
instance
|
||||||
|
.instance
|
||||||
|
.create_device(selected.physical_device, &create_info, None)
|
||||||
|
}
|
||||||
|
.map_err(|error| VulkanLogicalDeviceError::CreateFailed {
|
||||||
|
device: capability.device_name.clone(),
|
||||||
|
result: error,
|
||||||
|
})?;
|
||||||
|
// SAFETY: Queue family indices came from validated live queue families requested above.
|
||||||
|
let _graphics_queue = unsafe { device.get_device_queue(capability.graphics_queue_family, 0) };
|
||||||
|
// SAFETY: Queue family indices came from validated live queue families requested above.
|
||||||
|
let _present_queue = unsafe { device.get_device_queue(capability.present_queue_family, 0) };
|
||||||
|
Ok(VulkanLogicalDeviceProbe {
|
||||||
|
device,
|
||||||
|
physical_device: selected.physical_device,
|
||||||
|
report: VulkanLogicalDeviceReport {
|
||||||
|
schema: 1,
|
||||||
|
device_name: capability.device_name.clone(),
|
||||||
|
graphics_queue_family: capability.graphics_queue_family,
|
||||||
|
present_queue_family: capability.present_queue_family,
|
||||||
|
enabled_extensions: capability.enabled_extensions.clone(),
|
||||||
|
},
|
||||||
|
runtime: selected.runtime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn device_extension_cstrings(values: &[String]) -> Result<Vec<CString>, String> {
|
||||||
|
values
|
||||||
|
.iter()
|
||||||
|
.map(|extension| CString::new(extension.as_str()).map_err(|_| extension.clone()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
@@ -0,0 +1,914 @@
|
|||||||
|
#![allow(unsafe_code)]
|
||||||
|
|
||||||
|
use ash::vk;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
create_command_pool, create_frame_sync, create_swapchain_resources,
|
||||||
|
create_triangle_index_buffer, create_triangle_vertex_buffer, create_validation_messenger,
|
||||||
|
create_vulkan_instance_probe, create_vulkan_logical_device_probe_for_request,
|
||||||
|
create_vulkan_surface_probe, create_vulkan_swapchain_probe_for_extent,
|
||||||
|
destroy_allocated_buffer, destroy_swapchain_resources, plan_vulkan_surface,
|
||||||
|
VulkanAllocatedBuffer, VulkanInstanceConfig, VulkanInstanceProbe, VulkanLogicalDeviceProbe,
|
||||||
|
VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo,
|
||||||
|
VulkanSmokeRendererError, VulkanSmokeRendererReport, VulkanSmokeShutdownReport,
|
||||||
|
VulkanSurfaceProbe, VulkanSwapchainProbe, VulkanSwapchainResources, VulkanValidationMessenger,
|
||||||
|
VulkanValidationReport,
|
||||||
|
};
|
||||||
|
use crate::policy::KHR_PORTABILITY_SUBSET_EXTENSION;
|
||||||
|
use crate::shader_manifest::{triangle_shader_manifest, validate_shader_manifest};
|
||||||
|
|
||||||
|
fn take_runtime_owners_in_dependency_order<Instance, Validation, Surface, Device, Swapchain>(
|
||||||
|
instance: &mut Option<Instance>,
|
||||||
|
validation: &mut Option<Validation>,
|
||||||
|
surface: &mut Option<Surface>,
|
||||||
|
device: &mut Option<Device>,
|
||||||
|
swapchain: &mut Option<Swapchain>,
|
||||||
|
) {
|
||||||
|
swapchain.take();
|
||||||
|
device.take();
|
||||||
|
surface.take();
|
||||||
|
validation.take();
|
||||||
|
instance.take();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_runtime_owners_with_validation_snapshot<
|
||||||
|
Instance,
|
||||||
|
Validation,
|
||||||
|
Surface,
|
||||||
|
Device,
|
||||||
|
Swapchain,
|
||||||
|
Snapshot,
|
||||||
|
Capture,
|
||||||
|
>(
|
||||||
|
instance: &mut Option<Instance>,
|
||||||
|
validation: &mut Option<Validation>,
|
||||||
|
surface: &mut Option<Surface>,
|
||||||
|
device: &mut Option<Device>,
|
||||||
|
swapchain: &mut Option<Swapchain>,
|
||||||
|
capture: Capture,
|
||||||
|
) -> Option<Snapshot>
|
||||||
|
where
|
||||||
|
Capture: FnOnce(&Validation) -> Snapshot,
|
||||||
|
{
|
||||||
|
let snapshot = validation.as_ref().map(capture);
|
||||||
|
take_runtime_owners_in_dependency_order(instance, validation, surface, device, swapchain);
|
||||||
|
snapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RollbackOnDrop<T, F>
|
||||||
|
where
|
||||||
|
F: FnOnce(T),
|
||||||
|
{
|
||||||
|
value: Option<T>,
|
||||||
|
rollback: Option<F>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, F> RollbackOnDrop<T, F>
|
||||||
|
where
|
||||||
|
F: FnOnce(T),
|
||||||
|
{
|
||||||
|
fn new(value: T, rollback: F) -> Self {
|
||||||
|
Self {
|
||||||
|
value: Some(value),
|
||||||
|
rollback: Some(rollback),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn commit(mut self) -> T {
|
||||||
|
self.rollback.take();
|
||||||
|
match self.value.take() {
|
||||||
|
Some(value) => value,
|
||||||
|
None => unreachable!("rollback guard must hold a value until commit"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, F> Drop for RollbackOnDrop<T, F>
|
||||||
|
where
|
||||||
|
F: FnOnce(T),
|
||||||
|
{
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let (Some(value), Some(rollback)) = (self.value.take(), self.rollback.take()) {
|
||||||
|
rollback(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VulkanSmokeRenderer {
|
||||||
|
/// Creates a live Vulkan smoke renderer bound to a live native window.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanSmokeRendererError`] when Vulkan bootstrap, pipeline creation,
|
||||||
|
/// memory allocation, or synchronization resource creation fails.
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
pub fn new(
|
||||||
|
create_info: &VulkanSmokeRendererCreateInfo,
|
||||||
|
) -> Result<Self, VulkanSmokeRendererError> {
|
||||||
|
let bootstrap_progress = create_info.bootstrap_progress.as_ref();
|
||||||
|
let shader_manifest = validate_shader_manifest(&triangle_shader_manifest())
|
||||||
|
.map_err(VulkanSmokeRendererError::ShaderManifest)?;
|
||||||
|
let surface_plan = plan_vulkan_surface(Some(create_info.native_handles))
|
||||||
|
.map_err(VulkanSmokeRendererError::Surface)?;
|
||||||
|
let mut instance_config = VulkanInstanceConfig::smoke(&create_info.application_name);
|
||||||
|
instance_config
|
||||||
|
.required_extensions
|
||||||
|
.clone_from(&surface_plan.required_instance_extensions);
|
||||||
|
instance_config.enable_validation = create_info.enable_validation;
|
||||||
|
let instance = create_vulkan_instance_probe(&instance_config)
|
||||||
|
.map_err(VulkanSmokeRendererError::Instance)?;
|
||||||
|
if let Some(progress) = bootstrap_progress {
|
||||||
|
progress.mark_loader_available();
|
||||||
|
progress.mark_instance_created();
|
||||||
|
}
|
||||||
|
let validation = if create_info.enable_validation {
|
||||||
|
Some(create_validation_messenger(&instance)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let surface = create_vulkan_surface_probe(&instance, Some(create_info.native_handles))
|
||||||
|
.map_err(VulkanSmokeRendererError::Surface)?;
|
||||||
|
if let Some(progress) = bootstrap_progress {
|
||||||
|
progress.mark_surface_created();
|
||||||
|
}
|
||||||
|
let device = create_vulkan_logical_device_probe_for_request(
|
||||||
|
&instance,
|
||||||
|
&surface,
|
||||||
|
create_info.drawable_extent,
|
||||||
|
create_info.render_request,
|
||||||
|
)
|
||||||
|
.map_err(VulkanSmokeRendererError::LogicalDevice)?;
|
||||||
|
if let Some(progress) = bootstrap_progress {
|
||||||
|
progress.mark_logical_device_created();
|
||||||
|
}
|
||||||
|
let swapchain = create_vulkan_swapchain_probe_for_extent(
|
||||||
|
&instance,
|
||||||
|
&surface,
|
||||||
|
&device,
|
||||||
|
create_info.drawable_extent,
|
||||||
|
vk::SwapchainKHR::null(),
|
||||||
|
)
|
||||||
|
.map_err(VulkanSmokeRendererError::Swapchain)?;
|
||||||
|
if let Some(progress) = bootstrap_progress {
|
||||||
|
progress.mark_swapchain_created();
|
||||||
|
}
|
||||||
|
let command_pool = create_command_pool(&device)?;
|
||||||
|
let vertex_buffer = match create_triangle_vertex_buffer(&instance, &device) {
|
||||||
|
Ok(buffer) => buffer,
|
||||||
|
Err(error) => {
|
||||||
|
// SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
|
||||||
|
unsafe { device.device().destroy_command_pool(command_pool, None) };
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let index_buffer = match create_triangle_index_buffer(&instance, &device) {
|
||||||
|
Ok(buffer) => buffer,
|
||||||
|
Err(error) => {
|
||||||
|
// SAFETY: The command pool belongs to this live logical device and is destroyed on setup failure.
|
||||||
|
unsafe { device.device().destroy_command_pool(command_pool, None) };
|
||||||
|
destroy_allocated_buffer(&device, &vertex_buffer);
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut renderer = Self {
|
||||||
|
instance: Some(instance),
|
||||||
|
validation,
|
||||||
|
surface: Some(surface),
|
||||||
|
device: Some(device),
|
||||||
|
swapchain: Some(swapchain),
|
||||||
|
command_pool,
|
||||||
|
swapchain_resources: None,
|
||||||
|
vertex_buffer: Some(vertex_buffer),
|
||||||
|
index_buffer: Some(index_buffer),
|
||||||
|
frame_sync: Vec::new(),
|
||||||
|
images_in_flight: Vec::new(),
|
||||||
|
current_frame: 0,
|
||||||
|
pending_extent: None,
|
||||||
|
swapchain_recreate_count: 0,
|
||||||
|
report: VulkanSmokeRendererReport {
|
||||||
|
shader_manifest_hash: shader_manifest.manifest_hash.clone(),
|
||||||
|
portability_enumeration: instance_config.enable_portability_enumeration,
|
||||||
|
portability_subset_enabled: false,
|
||||||
|
device_name: String::new(),
|
||||||
|
graphics_queue_family: 0,
|
||||||
|
present_queue_family: 0,
|
||||||
|
enabled_extension_count: 0,
|
||||||
|
swapchain_extent: (0, 0),
|
||||||
|
swapchain_image_count: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
renderer.rebuild_swapchain_resources(false)?;
|
||||||
|
let device_ref = renderer.device_ref()?;
|
||||||
|
let swapchain_ref = renderer.swapchain_ref()?;
|
||||||
|
renderer.report = VulkanSmokeRendererReport {
|
||||||
|
shader_manifest_hash: shader_manifest.manifest_hash,
|
||||||
|
portability_enumeration: renderer
|
||||||
|
.instance
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|instance| instance.report.create_flags != 0),
|
||||||
|
portability_subset_enabled: device_ref
|
||||||
|
.report
|
||||||
|
.enabled_extensions
|
||||||
|
.iter()
|
||||||
|
.any(|extension| extension == KHR_PORTABILITY_SUBSET_EXTENSION),
|
||||||
|
device_name: device_ref.report.device_name.clone(),
|
||||||
|
graphics_queue_family: device_ref.report.graphics_queue_family,
|
||||||
|
present_queue_family: device_ref.report.present_queue_family,
|
||||||
|
enabled_extension_count: device_ref
|
||||||
|
.report
|
||||||
|
.enabled_extensions
|
||||||
|
.len()
|
||||||
|
.try_into()
|
||||||
|
.unwrap_or(u32::MAX),
|
||||||
|
swapchain_extent: swapchain_ref.report.plan.extent,
|
||||||
|
swapchain_image_count: swapchain_ref.report.image_count,
|
||||||
|
};
|
||||||
|
Ok(renderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current bootstrap report.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn report(&self) -> &VulkanSmokeRendererReport {
|
||||||
|
&self.report
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns measured validation counters and VUIDs.
|
||||||
|
#[must_use]
|
||||||
|
pub fn validation_report(&self) -> VulkanValidationReport {
|
||||||
|
self.validation.as_ref().map_or(
|
||||||
|
VulkanValidationReport {
|
||||||
|
warning_count: 0,
|
||||||
|
error_count: 0,
|
||||||
|
vuids: Vec::new(),
|
||||||
|
},
|
||||||
|
VulkanValidationMessenger::report,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the measured swapchain recreation count.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn swapchain_recreate_count(&self) -> u32 {
|
||||||
|
self.swapchain_recreate_count
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explicitly idles and tears down the renderer while the native window is still alive.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanSmokeRendererError`] when the renderer cannot reach a
|
||||||
|
/// stable idle point before teardown begins.
|
||||||
|
pub fn shutdown(mut self) -> Result<VulkanSmokeShutdownReport, VulkanSmokeRendererError> {
|
||||||
|
self.shutdown_inner()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Requests swapchain recreation for a new drawable extent.
|
||||||
|
pub fn request_resize(&mut self, extent: (u32, u32)) {
|
||||||
|
self.pending_extent = Some(extent);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn device_ref(&self) -> Result<&VulkanLogicalDeviceProbe, VulkanSmokeRendererError> {
|
||||||
|
self.device
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(VulkanSmokeRendererError::InvariantViolation {
|
||||||
|
context: "logical device",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn swapchain_ref(&self) -> Result<&VulkanSwapchainProbe, VulkanSmokeRendererError> {
|
||||||
|
self.swapchain
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(VulkanSmokeRendererError::InvariantViolation {
|
||||||
|
context: "swapchain",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instance_ref(&self) -> Result<&VulkanInstanceProbe, VulkanSmokeRendererError> {
|
||||||
|
self.instance
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(VulkanSmokeRendererError::InvariantViolation {
|
||||||
|
context: "instance",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn surface_ref(&self) -> Result<&VulkanSurfaceProbe, VulkanSmokeRendererError> {
|
||||||
|
self.surface
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(VulkanSmokeRendererError::InvariantViolation { context: "surface" })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resources_ref(&self) -> Result<&VulkanSwapchainResources, VulkanSmokeRendererError> {
|
||||||
|
self.swapchain_resources
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(VulkanSmokeRendererError::InvariantViolation {
|
||||||
|
context: "swapchain resources",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vertex_buffer_ref(&self) -> Result<&VulkanAllocatedBuffer, VulkanSmokeRendererError> {
|
||||||
|
self.vertex_buffer
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(VulkanSmokeRendererError::InvariantViolation {
|
||||||
|
context: "vertex buffer",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index_buffer_ref(&self) -> Result<&VulkanAllocatedBuffer, VulkanSmokeRendererError> {
|
||||||
|
self.index_buffer
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(VulkanSmokeRendererError::InvariantViolation {
|
||||||
|
context: "index buffer",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws and presents one indexed-triangle frame.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanSmokeRendererError`] when synchronization, command recording,
|
||||||
|
/// submission, or presentation fails.
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
pub fn draw_frame(&mut self) -> Result<VulkanSmokeFrameOutcome, VulkanSmokeRendererError> {
|
||||||
|
if let Some(extent) = self.pending_extent.take() {
|
||||||
|
if extent.0 == 0 || extent.1 == 0 {
|
||||||
|
self.pending_extent = Some(extent);
|
||||||
|
return Ok(VulkanSmokeFrameOutcome::ZeroExtent);
|
||||||
|
}
|
||||||
|
self.recreate_swapchain(extent)?;
|
||||||
|
return Ok(VulkanSmokeFrameOutcome::Recreated);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sync = &self.frame_sync[self.current_frame];
|
||||||
|
let image_available = sync.image_available;
|
||||||
|
let render_finished = sync.render_finished;
|
||||||
|
let in_flight_fence = sync.fence;
|
||||||
|
// SAFETY: The fence belongs to this live logical device and is waited from one thread.
|
||||||
|
unsafe {
|
||||||
|
self.device_ref()?
|
||||||
|
.device()
|
||||||
|
.wait_for_fences(&[in_flight_fence], true, 1_000_000_000)
|
||||||
|
}
|
||||||
|
.map_err(|error| VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkWaitForFences",
|
||||||
|
result: error,
|
||||||
|
})?;
|
||||||
|
// SAFETY: The swapchain, semaphore and fence inputs are live for the duration of the acquire call.
|
||||||
|
let acquire = unsafe {
|
||||||
|
self.swapchain_ref()?.loader().acquire_next_image(
|
||||||
|
self.swapchain_ref()?.swapchain(),
|
||||||
|
1_000_000_000,
|
||||||
|
image_available,
|
||||||
|
vk::Fence::null(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let (image_index, acquire_suboptimal) = match acquire {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
|
||||||
|
self.recreate_swapchain(self.report.swapchain_extent)?;
|
||||||
|
return Ok(VulkanSmokeFrameOutcome::Recreated);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
return Err(VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkAcquireNextImageKHR",
|
||||||
|
result: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let image_index_usize = usize::try_from(image_index).unwrap_or(0);
|
||||||
|
let image_fence = self.images_in_flight[image_index_usize];
|
||||||
|
if image_fence != vk::Fence::null() {
|
||||||
|
// SAFETY: The fence belongs to this renderer and can be waited independently.
|
||||||
|
unsafe {
|
||||||
|
self.device_ref()?
|
||||||
|
.device()
|
||||||
|
.wait_for_fences(&[image_fence], true, 1_000_000_000)
|
||||||
|
}
|
||||||
|
.map_err(|error| VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkWaitForFences(image)",
|
||||||
|
result: error,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
self.images_in_flight[image_index_usize] = in_flight_fence;
|
||||||
|
// SAFETY: The fence belongs to this frame context and is not in use after the wait above.
|
||||||
|
unsafe { self.device_ref()?.device().reset_fences(&[in_flight_fence]) }.map_err(
|
||||||
|
|error| VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkResetFences",
|
||||||
|
result: error,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
self.record_command_buffer(image_index_usize)?;
|
||||||
|
let wait_semaphores = [image_available];
|
||||||
|
let wait_stages = [vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT];
|
||||||
|
let command_buffers = [self.resources_ref()?.command_buffers[image_index_usize]];
|
||||||
|
let signal_semaphores = [render_finished];
|
||||||
|
let submit_info = [vk::SubmitInfo::default()
|
||||||
|
.wait_semaphores(&wait_semaphores)
|
||||||
|
.wait_dst_stage_mask(&wait_stages)
|
||||||
|
.command_buffers(&command_buffers)
|
||||||
|
.signal_semaphores(&signal_semaphores)];
|
||||||
|
// SAFETY: Submission references live queue, sync objects and recorded command buffer.
|
||||||
|
unsafe {
|
||||||
|
self.device_ref()?.device().queue_submit(
|
||||||
|
self.device_ref()?.graphics_queue(),
|
||||||
|
&submit_info,
|
||||||
|
in_flight_fence,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.map_err(|error| VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkQueueSubmit",
|
||||||
|
result: error,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let present_wait = [render_finished];
|
||||||
|
let swapchains = [self.swapchain_ref()?.swapchain()];
|
||||||
|
let image_indices = [image_index];
|
||||||
|
let present_info = vk::PresentInfoKHR::default()
|
||||||
|
.wait_semaphores(&present_wait)
|
||||||
|
.swapchains(&swapchains)
|
||||||
|
.image_indices(&image_indices);
|
||||||
|
// SAFETY: Presentation uses the rendered image index and a semaphore signaled by queue submission.
|
||||||
|
let present_suboptimal = match unsafe {
|
||||||
|
self.swapchain_ref()?
|
||||||
|
.loader()
|
||||||
|
.queue_present(self.device_ref()?.present_queue(), &present_info)
|
||||||
|
} {
|
||||||
|
Ok(suboptimal) => suboptimal,
|
||||||
|
Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
|
||||||
|
self.recreate_swapchain(self.report.swapchain_extent)?;
|
||||||
|
return Ok(VulkanSmokeFrameOutcome::Recreated);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
return Err(VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkQueuePresentKHR",
|
||||||
|
result: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.current_frame = (self.current_frame + 1) % self.frame_sync.len().max(1);
|
||||||
|
if acquire_suboptimal || present_suboptimal {
|
||||||
|
self.recreate_swapchain(self.report.swapchain_extent)?;
|
||||||
|
Ok(VulkanSmokeFrameOutcome::Recreated)
|
||||||
|
} else {
|
||||||
|
Ok(VulkanSmokeFrameOutcome::Presented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recreate_swapchain(&mut self, extent: (u32, u32)) -> Result<(), VulkanSmokeRendererError> {
|
||||||
|
let device = self.device_ref()?;
|
||||||
|
// SAFETY: The logical device remains live and idling at swapchain recreation boundaries.
|
||||||
|
unsafe { device.device().device_wait_idle() }.map_err(|error| {
|
||||||
|
VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkDeviceWaitIdle",
|
||||||
|
result: error,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
self.pending_extent = None;
|
||||||
|
self.rebuild_swapchain(extent)?;
|
||||||
|
self.swapchain_recreate_count = self.swapchain_recreate_count.saturating_add(1);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild_swapchain(&mut self, extent: (u32, u32)) -> Result<(), VulkanSmokeRendererError> {
|
||||||
|
self.destroy_swapchain_resources();
|
||||||
|
let instance = self.instance_ref()?;
|
||||||
|
let surface = self.surface_ref()?;
|
||||||
|
let device = self.device_ref()?;
|
||||||
|
let old_swapchain = self
|
||||||
|
.swapchain
|
||||||
|
.as_ref()
|
||||||
|
.map_or(vk::SwapchainKHR::null(), VulkanSwapchainProbe::swapchain);
|
||||||
|
let new_swapchain = create_vulkan_swapchain_probe_for_extent(
|
||||||
|
instance,
|
||||||
|
surface,
|
||||||
|
device,
|
||||||
|
extent,
|
||||||
|
old_swapchain,
|
||||||
|
)
|
||||||
|
.map_err(VulkanSmokeRendererError::Swapchain)?;
|
||||||
|
self.swapchain = Some(new_swapchain);
|
||||||
|
self.rebuild_swapchain_resources(true)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild_swapchain_resources(
|
||||||
|
&mut self,
|
||||||
|
reuse_command_pool: bool,
|
||||||
|
) -> Result<(), VulkanSmokeRendererError> {
|
||||||
|
let device = self.device_ref()?;
|
||||||
|
let swapchain = self.swapchain_ref()?;
|
||||||
|
let resources = RollbackOnDrop::new(
|
||||||
|
create_swapchain_resources(
|
||||||
|
device,
|
||||||
|
swapchain,
|
||||||
|
self.command_pool,
|
||||||
|
self.vertex_buffer_ref()?,
|
||||||
|
self.index_buffer_ref()?,
|
||||||
|
reuse_command_pool,
|
||||||
|
)?,
|
||||||
|
|resources| destroy_swapchain_resources(device, self.command_pool, resources),
|
||||||
|
);
|
||||||
|
let frame_sync = create_frame_sync(device)?;
|
||||||
|
let swapchain_extent = self.swapchain_ref()?.report.plan.extent;
|
||||||
|
let swapchain_image_count = self.swapchain_ref()?.report.image_count;
|
||||||
|
let resources = resources.commit();
|
||||||
|
self.images_in_flight = vec![vk::Fence::null(); resources.image_views.len()];
|
||||||
|
self.frame_sync = frame_sync;
|
||||||
|
self.report.swapchain_extent = swapchain_extent;
|
||||||
|
self.report.swapchain_image_count = swapchain_image_count;
|
||||||
|
self.swapchain_resources = Some(resources);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
fn record_command_buffer(
|
||||||
|
&mut self,
|
||||||
|
image_index: usize,
|
||||||
|
) -> Result<(), VulkanSmokeRendererError> {
|
||||||
|
let device = self.device_ref()?;
|
||||||
|
let swapchain = self.swapchain_ref()?;
|
||||||
|
let resources = self.resources_ref()?;
|
||||||
|
let command_buffer = resources.command_buffers[image_index];
|
||||||
|
// SAFETY: The command buffer belongs to the resettable pool owned by this renderer.
|
||||||
|
unsafe {
|
||||||
|
device
|
||||||
|
.device()
|
||||||
|
.reset_command_buffer(command_buffer, vk::CommandBufferResetFlags::empty())
|
||||||
|
}
|
||||||
|
.map_err(|error| VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkResetCommandBuffer",
|
||||||
|
result: error,
|
||||||
|
})?;
|
||||||
|
let begin_info = vk::CommandBufferBeginInfo::default();
|
||||||
|
// SAFETY: The command buffer is in the initial state after reset and recorded on one thread.
|
||||||
|
unsafe {
|
||||||
|
device
|
||||||
|
.device()
|
||||||
|
.begin_command_buffer(command_buffer, &begin_info)
|
||||||
|
}
|
||||||
|
.map_err(|error| VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkBeginCommandBuffer",
|
||||||
|
result: error,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let clear_values = [vk::ClearValue {
|
||||||
|
color: vk::ClearColorValue {
|
||||||
|
float32: [0.05, 0.08, 0.11, 1.0],
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
let render_area = vk::Rect2D {
|
||||||
|
offset: vk::Offset2D { x: 0, y: 0 },
|
||||||
|
extent: vk::Extent2D {
|
||||||
|
width: swapchain.report.plan.extent.0,
|
||||||
|
height: swapchain.report.plan.extent.1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let render_pass_info = vk::RenderPassBeginInfo::default()
|
||||||
|
.render_pass(resources.render_pass)
|
||||||
|
.framebuffer(resources.framebuffers[image_index])
|
||||||
|
.render_area(render_area)
|
||||||
|
.clear_values(&clear_values);
|
||||||
|
// SAFETY: All commands target live frame resources owned by this renderer.
|
||||||
|
unsafe {
|
||||||
|
device.device().cmd_begin_render_pass(
|
||||||
|
command_buffer,
|
||||||
|
&render_pass_info,
|
||||||
|
vk::SubpassContents::INLINE,
|
||||||
|
);
|
||||||
|
device.device().cmd_bind_pipeline(
|
||||||
|
command_buffer,
|
||||||
|
vk::PipelineBindPoint::GRAPHICS,
|
||||||
|
resources.pipeline,
|
||||||
|
);
|
||||||
|
let vertex_buffers = [self.vertex_buffer_ref()?.buffer];
|
||||||
|
let offsets = [0_u64];
|
||||||
|
device
|
||||||
|
.device()
|
||||||
|
.cmd_bind_vertex_buffers(command_buffer, 0, &vertex_buffers, &offsets);
|
||||||
|
device.device().cmd_bind_index_buffer(
|
||||||
|
command_buffer,
|
||||||
|
self.index_buffer_ref()?.buffer,
|
||||||
|
0,
|
||||||
|
vk::IndexType::UINT16,
|
||||||
|
);
|
||||||
|
device
|
||||||
|
.device()
|
||||||
|
.cmd_draw_indexed(command_buffer, 3, 1, 0, 0, 0);
|
||||||
|
device.device().cmd_end_render_pass(command_buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: The render pass owns the attachment layout transitions for this clear-and-present path.
|
||||||
|
unsafe { device.device().end_command_buffer(command_buffer) }.map_err(|error| {
|
||||||
|
VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkEndCommandBuffer",
|
||||||
|
result: error,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroy_swapchain_resources(&mut self) {
|
||||||
|
let Some(device) = self.device.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for sync in self.frame_sync.drain(..) {
|
||||||
|
// SAFETY: These sync objects belong to this device and are destroyed once.
|
||||||
|
unsafe {
|
||||||
|
device
|
||||||
|
.device()
|
||||||
|
.destroy_semaphore(sync.image_available, None);
|
||||||
|
device
|
||||||
|
.device()
|
||||||
|
.destroy_semaphore(sync.render_finished, None);
|
||||||
|
device.device().destroy_fence(sync.fence, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(resources) = self.swapchain_resources.take() {
|
||||||
|
destroy_swapchain_resources(device, self.command_pool, resources);
|
||||||
|
}
|
||||||
|
self.images_in_flight.clear();
|
||||||
|
self.current_frame = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroy_device_owned_resources(&mut self) {
|
||||||
|
self.destroy_swapchain_resources();
|
||||||
|
if let Some(device) = self.device.as_ref() {
|
||||||
|
if let Some(buffer) = self.index_buffer.take() {
|
||||||
|
// SAFETY: Buffer and memory belong to this device and are destroyed once after the device has been idled and frame work has been torn down.
|
||||||
|
unsafe {
|
||||||
|
device.device().destroy_buffer(buffer.buffer, None);
|
||||||
|
device.device().free_memory(buffer.memory, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(buffer) = self.vertex_buffer.take() {
|
||||||
|
// SAFETY: Buffer and memory belong to this device and are destroyed once after the device has been idled and frame work has been torn down.
|
||||||
|
unsafe {
|
||||||
|
device.device().destroy_buffer(buffer.buffer, None);
|
||||||
|
device.device().free_memory(buffer.memory, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SAFETY: The command pool belongs to this device and is destroyed once after the device is idle and all command buffers allocated from it were freed above.
|
||||||
|
unsafe {
|
||||||
|
device
|
||||||
|
.device()
|
||||||
|
.destroy_command_pool(self.command_pool, None);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
self.pending_extent = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shutdown_inner(&mut self) -> Result<VulkanSmokeShutdownReport, VulkanSmokeRendererError> {
|
||||||
|
if let Some(device) = self.device.as_ref() {
|
||||||
|
// SAFETY: The logical device remains live until teardown finishes and idling prevents in-flight work from touching swapchain, buffers, sync objects or the command pool after destruction starts.
|
||||||
|
unsafe { device.device().device_wait_idle() }.map_err(|error| {
|
||||||
|
VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkDeviceWaitIdle",
|
||||||
|
result: error,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
self.destroy_device_owned_resources();
|
||||||
|
let validation = take_runtime_owners_with_validation_snapshot(
|
||||||
|
&mut self.instance,
|
||||||
|
&mut self.validation,
|
||||||
|
&mut self.surface,
|
||||||
|
&mut self.device,
|
||||||
|
&mut self.swapchain,
|
||||||
|
VulkanValidationMessenger::report,
|
||||||
|
)
|
||||||
|
.unwrap_or_default();
|
||||||
|
Ok(VulkanSmokeShutdownReport {
|
||||||
|
renderer_report: self.report.clone(),
|
||||||
|
swapchain_recreate_count: self.swapchain_recreate_count,
|
||||||
|
validation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn teardown(&mut self) {
|
||||||
|
if let Some(device) = self.device.as_ref() {
|
||||||
|
// SAFETY: The logical device remains live until teardown finishes and idling prevents in-flight work from touching swapchain, buffers, sync objects or the command pool after destruction starts.
|
||||||
|
let _ = unsafe { device.device().device_wait_idle() };
|
||||||
|
}
|
||||||
|
self.destroy_device_owned_resources();
|
||||||
|
let _ = take_runtime_owners_with_validation_snapshot(
|
||||||
|
&mut self.instance,
|
||||||
|
&mut self.validation,
|
||||||
|
&mut self.surface,
|
||||||
|
&mut self.device,
|
||||||
|
&mut self.swapchain,
|
||||||
|
VulkanValidationMessenger::report,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for VulkanSmokeRenderer {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{
|
||||||
|
take_runtime_owners_in_dependency_order, take_runtime_owners_with_validation_snapshot,
|
||||||
|
RollbackOnDrop,
|
||||||
|
};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
enum TeardownStep {
|
||||||
|
Snapshot,
|
||||||
|
Instance,
|
||||||
|
Validation,
|
||||||
|
Surface,
|
||||||
|
Device,
|
||||||
|
Swapchain,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DropTracker {
|
||||||
|
step: TeardownStep,
|
||||||
|
log: Rc<RefCell<Vec<TeardownStep>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for DropTracker {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.log.borrow_mut().push(self.step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tracker(step: TeardownStep, log: &Rc<RefCell<Vec<TeardownStep>>>) -> DropTracker {
|
||||||
|
DropTracker {
|
||||||
|
step,
|
||||||
|
log: Rc::clone(log),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_teardown_steps(present_steps: &[TeardownStep]) -> Vec<TeardownStep> {
|
||||||
|
let log = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let mut instance = present_steps
|
||||||
|
.contains(&TeardownStep::Instance)
|
||||||
|
.then(|| tracker(TeardownStep::Instance, &log));
|
||||||
|
let mut validation = present_steps
|
||||||
|
.contains(&TeardownStep::Validation)
|
||||||
|
.then(|| tracker(TeardownStep::Validation, &log));
|
||||||
|
let mut surface = present_steps
|
||||||
|
.contains(&TeardownStep::Surface)
|
||||||
|
.then(|| tracker(TeardownStep::Surface, &log));
|
||||||
|
let mut device = present_steps
|
||||||
|
.contains(&TeardownStep::Device)
|
||||||
|
.then(|| tracker(TeardownStep::Device, &log));
|
||||||
|
let mut swapchain = present_steps
|
||||||
|
.contains(&TeardownStep::Swapchain)
|
||||||
|
.then(|| tracker(TeardownStep::Swapchain, &log));
|
||||||
|
|
||||||
|
take_runtime_owners_in_dependency_order(
|
||||||
|
&mut instance,
|
||||||
|
&mut validation,
|
||||||
|
&mut surface,
|
||||||
|
&mut device,
|
||||||
|
&mut swapchain,
|
||||||
|
);
|
||||||
|
Rc::into_inner(log)
|
||||||
|
.expect("all drop trackers released")
|
||||||
|
.into_inner()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_owners_drop_in_explicit_dependency_order() {
|
||||||
|
assert_eq!(
|
||||||
|
record_teardown_steps(&[
|
||||||
|
TeardownStep::Instance,
|
||||||
|
TeardownStep::Validation,
|
||||||
|
TeardownStep::Surface,
|
||||||
|
TeardownStep::Device,
|
||||||
|
TeardownStep::Swapchain,
|
||||||
|
]),
|
||||||
|
vec![
|
||||||
|
TeardownStep::Swapchain,
|
||||||
|
TeardownStep::Device,
|
||||||
|
TeardownStep::Surface,
|
||||||
|
TeardownStep::Validation,
|
||||||
|
TeardownStep::Instance,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_owners_drop_remaining_children_after_partial_init_failures() {
|
||||||
|
let cases = [
|
||||||
|
(vec![TeardownStep::Instance], vec![TeardownStep::Instance]),
|
||||||
|
(
|
||||||
|
vec![TeardownStep::Instance, TeardownStep::Validation],
|
||||||
|
vec![TeardownStep::Validation, TeardownStep::Instance],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
vec![
|
||||||
|
TeardownStep::Instance,
|
||||||
|
TeardownStep::Validation,
|
||||||
|
TeardownStep::Surface,
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
TeardownStep::Surface,
|
||||||
|
TeardownStep::Validation,
|
||||||
|
TeardownStep::Instance,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
vec![
|
||||||
|
TeardownStep::Instance,
|
||||||
|
TeardownStep::Validation,
|
||||||
|
TeardownStep::Surface,
|
||||||
|
TeardownStep::Device,
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
TeardownStep::Device,
|
||||||
|
TeardownStep::Surface,
|
||||||
|
TeardownStep::Validation,
|
||||||
|
TeardownStep::Instance,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
vec![
|
||||||
|
TeardownStep::Instance,
|
||||||
|
TeardownStep::Surface,
|
||||||
|
TeardownStep::Device,
|
||||||
|
TeardownStep::Swapchain,
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
TeardownStep::Swapchain,
|
||||||
|
TeardownStep::Device,
|
||||||
|
TeardownStep::Surface,
|
||||||
|
TeardownStep::Instance,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (present_steps, expected) in cases {
|
||||||
|
assert_eq!(record_teardown_steps(&present_steps), expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn final_validation_snapshot_is_captured_before_validation_drop() {
|
||||||
|
let log = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let mut instance = Some(tracker(TeardownStep::Instance, &log));
|
||||||
|
let mut validation = Some(tracker(TeardownStep::Validation, &log));
|
||||||
|
let mut surface = Some(tracker(TeardownStep::Surface, &log));
|
||||||
|
let mut device = Some(tracker(TeardownStep::Device, &log));
|
||||||
|
let mut swapchain = Some(tracker(TeardownStep::Swapchain, &log));
|
||||||
|
|
||||||
|
let snapshot = take_runtime_owners_with_validation_snapshot(
|
||||||
|
&mut instance,
|
||||||
|
&mut validation,
|
||||||
|
&mut surface,
|
||||||
|
&mut device,
|
||||||
|
&mut swapchain,
|
||||||
|
|_| {
|
||||||
|
log.borrow_mut().push(TeardownStep::Snapshot);
|
||||||
|
TeardownStep::Validation
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(snapshot, Some(TeardownStep::Validation));
|
||||||
|
assert_eq!(
|
||||||
|
Rc::into_inner(log)
|
||||||
|
.expect("all drop trackers released")
|
||||||
|
.into_inner(),
|
||||||
|
vec![
|
||||||
|
TeardownStep::Snapshot,
|
||||||
|
TeardownStep::Swapchain,
|
||||||
|
TeardownStep::Device,
|
||||||
|
TeardownStep::Surface,
|
||||||
|
TeardownStep::Validation,
|
||||||
|
TeardownStep::Instance,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rollback_guard_runs_cleanup_when_later_step_fails() {
|
||||||
|
let log = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
{
|
||||||
|
let _guard = RollbackOnDrop::new(tracker(TeardownStep::Swapchain, &log), |tracker| {
|
||||||
|
drop(tracker)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(log.borrow().as_slice(), &[TeardownStep::Swapchain]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rollback_guard_skips_cleanup_after_commit() {
|
||||||
|
let log = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let tracker = RollbackOnDrop::new(tracker(TeardownStep::Swapchain, &log), |tracker| {
|
||||||
|
drop(tracker)
|
||||||
|
})
|
||||||
|
.commit();
|
||||||
|
assert!(log.borrow().is_empty());
|
||||||
|
|
||||||
|
drop(tracker);
|
||||||
|
|
||||||
|
assert_eq!(log.borrow().as_slice(), &[TeardownStep::Swapchain]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
use ash::vk;
|
||||||
|
use fparkan_platform::{NativeWindowHandles, RenderRequest};
|
||||||
|
use std::sync::atomic::{AtomicU8, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
VulkanAllocatedBuffer, VulkanFrameSync, VulkanInstanceError, VulkanInstanceProbe,
|
||||||
|
VulkanLogicalDeviceError, VulkanLogicalDeviceProbe, VulkanSurfaceError, VulkanSurfaceProbe,
|
||||||
|
VulkanSwapchainProbe, VulkanSwapchainProbeError, VulkanSwapchainResources,
|
||||||
|
VulkanValidationMessenger,
|
||||||
|
};
|
||||||
|
use crate::shader_manifest::VulkanShaderManifestError;
|
||||||
|
|
||||||
|
/// Creates a live native Vulkan renderer for the Stage 0 smoke loop.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct VulkanSmokeRendererCreateInfo {
|
||||||
|
/// Application name reported to the Vulkan loader.
|
||||||
|
pub application_name: String,
|
||||||
|
/// Native window/display handles borrowed from a live window.
|
||||||
|
pub native_handles: NativeWindowHandles,
|
||||||
|
/// Initial drawable extent.
|
||||||
|
pub drawable_extent: (u32, u32),
|
||||||
|
/// Stage 0 render request used for capability gating.
|
||||||
|
pub render_request: RenderRequest,
|
||||||
|
/// Whether validation layers must be enabled.
|
||||||
|
pub enable_validation: bool,
|
||||||
|
/// Optional shared bootstrap progress tracker for failure evidence.
|
||||||
|
pub bootstrap_progress: Option<Arc<VulkanSmokeBootstrapProgress>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared bootstrap progress used to report partial renderer startup evidence.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct VulkanSmokeBootstrapProgress {
|
||||||
|
flags: AtomicU8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VulkanSmokeBootstrapProgress {
|
||||||
|
/// Marks the Vulkan loader as available.
|
||||||
|
pub fn mark_loader_available(&self) {
|
||||||
|
self.set_flag(BOOTSTRAP_LOADER_AVAILABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks the Vulkan instance as created.
|
||||||
|
pub fn mark_instance_created(&self) {
|
||||||
|
self.set_flag(BOOTSTRAP_INSTANCE_CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks the Vulkan surface as created.
|
||||||
|
pub fn mark_surface_created(&self) {
|
||||||
|
self.set_flag(BOOTSTRAP_SURFACE_CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks a suitable Vulkan device as selected and the logical device as created.
|
||||||
|
pub fn mark_logical_device_created(&self) {
|
||||||
|
self.set_flag(BOOTSTRAP_DEVICE_SELECTED | BOOTSTRAP_LOGICAL_DEVICE_CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks the Vulkan swapchain as created.
|
||||||
|
pub fn mark_swapchain_created(&self) {
|
||||||
|
self.set_flag(BOOTSTRAP_SWAPCHAIN_CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a stable snapshot of the measured bootstrap state.
|
||||||
|
#[must_use]
|
||||||
|
pub fn snapshot(&self) -> VulkanSmokeBootstrapSnapshot {
|
||||||
|
let flags = self.flags.load(Ordering::SeqCst);
|
||||||
|
VulkanSmokeBootstrapSnapshot {
|
||||||
|
loader_available: flags & BOOTSTRAP_LOADER_AVAILABLE != 0,
|
||||||
|
instance_created: flags & BOOTSTRAP_INSTANCE_CREATED != 0,
|
||||||
|
surface_created: flags & BOOTSTRAP_SURFACE_CREATED != 0,
|
||||||
|
device_selected: flags & BOOTSTRAP_DEVICE_SELECTED != 0,
|
||||||
|
logical_device_created: flags & BOOTSTRAP_LOGICAL_DEVICE_CREATED != 0,
|
||||||
|
swapchain_created: flags & BOOTSTRAP_SWAPCHAIN_CREATED != 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_flag(&self, flag: u8) {
|
||||||
|
self.flags.fetch_or(flag, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stable snapshot of measured bootstrap progress.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
|
pub struct VulkanSmokeBootstrapSnapshot {
|
||||||
|
/// Whether the Vulkan loader was resolved.
|
||||||
|
pub loader_available: bool,
|
||||||
|
/// Whether the Vulkan instance was created.
|
||||||
|
pub instance_created: bool,
|
||||||
|
/// Whether the Vulkan surface was created.
|
||||||
|
pub surface_created: bool,
|
||||||
|
/// Whether a suitable Vulkan device was selected.
|
||||||
|
pub device_selected: bool,
|
||||||
|
/// Whether the logical device was created.
|
||||||
|
pub logical_device_created: bool,
|
||||||
|
/// Whether the swapchain was created.
|
||||||
|
pub swapchain_created: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
const BOOTSTRAP_LOADER_AVAILABLE: u8 = 1 << 0;
|
||||||
|
const BOOTSTRAP_INSTANCE_CREATED: u8 = 1 << 1;
|
||||||
|
const BOOTSTRAP_SURFACE_CREATED: u8 = 1 << 2;
|
||||||
|
const BOOTSTRAP_DEVICE_SELECTED: u8 = 1 << 3;
|
||||||
|
const BOOTSTRAP_LOGICAL_DEVICE_CREATED: u8 = 1 << 4;
|
||||||
|
const BOOTSTRAP_SWAPCHAIN_CREATED: u8 = 1 << 5;
|
||||||
|
|
||||||
|
/// Stable smoke renderer bootstrap report.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanSmokeRendererReport {
|
||||||
|
/// Checked-in shader manifest hash used by the renderer.
|
||||||
|
pub shader_manifest_hash: String,
|
||||||
|
/// Whether portability enumeration was enabled at instance creation.
|
||||||
|
pub portability_enumeration: bool,
|
||||||
|
/// Whether the logical device enabled `VK_KHR_portability_subset`.
|
||||||
|
pub portability_subset_enabled: bool,
|
||||||
|
/// Selected device name.
|
||||||
|
pub device_name: String,
|
||||||
|
/// Graphics queue-family index.
|
||||||
|
pub graphics_queue_family: u32,
|
||||||
|
/// Present queue-family index.
|
||||||
|
pub present_queue_family: u32,
|
||||||
|
/// Enabled logical-device extension count.
|
||||||
|
pub enabled_extension_count: u32,
|
||||||
|
/// Current swapchain extent.
|
||||||
|
pub swapchain_extent: (u32, u32),
|
||||||
|
/// Current swapchain image count.
|
||||||
|
pub swapchain_image_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Measured validation counters from the live smoke loop.
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub struct VulkanValidationReport {
|
||||||
|
/// Validation warnings observed by the debug messenger.
|
||||||
|
pub warning_count: u32,
|
||||||
|
/// Validation errors observed by the debug messenger.
|
||||||
|
pub error_count: u32,
|
||||||
|
/// Stable sorted VUID list.
|
||||||
|
pub vuids: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Final smoke renderer shutdown evidence captured after explicit teardown.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanSmokeShutdownReport {
|
||||||
|
/// Stable renderer bootstrap and swapchain report.
|
||||||
|
pub renderer_report: VulkanSmokeRendererReport,
|
||||||
|
/// Measured swapchain recreation count for the completed smoke loop.
|
||||||
|
pub swapchain_recreate_count: u32,
|
||||||
|
/// Final validation snapshot captured before the debug messenger is destroyed.
|
||||||
|
pub validation: VulkanValidationReport,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of one rendered smoke frame.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum VulkanSmokeFrameOutcome {
|
||||||
|
/// A frame was submitted and presented.
|
||||||
|
Presented,
|
||||||
|
/// Rendering was skipped because the swapchain had to be recreated.
|
||||||
|
Recreated,
|
||||||
|
/// Rendering was skipped because the drawable extent is zero.
|
||||||
|
ZeroExtent,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Live smoke renderer error.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum VulkanSmokeRendererError {
|
||||||
|
/// Instance bootstrap failed.
|
||||||
|
Instance(VulkanInstanceError),
|
||||||
|
/// Surface bootstrap failed.
|
||||||
|
Surface(VulkanSurfaceError),
|
||||||
|
/// Logical-device bootstrap failed.
|
||||||
|
LogicalDevice(VulkanLogicalDeviceError),
|
||||||
|
/// Swapchain bootstrap failed.
|
||||||
|
Swapchain(VulkanSwapchainProbeError),
|
||||||
|
/// Shader manifest validation failed.
|
||||||
|
ShaderManifest(VulkanShaderManifestError),
|
||||||
|
/// Vulkan operation failed.
|
||||||
|
VulkanOperation {
|
||||||
|
/// Operation context.
|
||||||
|
context: &'static str,
|
||||||
|
/// Raw Vulkan result code.
|
||||||
|
result: vk::Result,
|
||||||
|
},
|
||||||
|
/// No suitable memory type exists for the required properties.
|
||||||
|
MissingMemoryType {
|
||||||
|
/// Operation context.
|
||||||
|
context: &'static str,
|
||||||
|
},
|
||||||
|
/// Internal smoke renderer state was unexpectedly absent.
|
||||||
|
InvariantViolation {
|
||||||
|
/// Missing state context.
|
||||||
|
context: &'static str,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for VulkanSmokeRendererError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Instance(error) => write!(f, "{error}"),
|
||||||
|
Self::Surface(error) => write!(f, "{error}"),
|
||||||
|
Self::LogicalDevice(error) => write!(f, "{error}"),
|
||||||
|
Self::Swapchain(error) => write!(f, "{error}"),
|
||||||
|
Self::ShaderManifest(error) => write!(f, "{error}"),
|
||||||
|
Self::VulkanOperation { context, result } => {
|
||||||
|
write!(f, "{context}: {result:?}")
|
||||||
|
}
|
||||||
|
Self::MissingMemoryType { context } => {
|
||||||
|
write!(f, "{context}: no compatible Vulkan memory type")
|
||||||
|
}
|
||||||
|
Self::InvariantViolation { context } => {
|
||||||
|
write!(f, "renderer invariant violated: {context}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for VulkanSmokeRendererError {}
|
||||||
|
|
||||||
|
/// Live Stage 0 Vulkan triangle renderer used by the smoke app.
|
||||||
|
pub struct VulkanSmokeRenderer {
|
||||||
|
pub(super) instance: Option<VulkanInstanceProbe>,
|
||||||
|
pub(super) validation: Option<VulkanValidationMessenger>,
|
||||||
|
pub(super) surface: Option<VulkanSurfaceProbe>,
|
||||||
|
pub(super) device: Option<VulkanLogicalDeviceProbe>,
|
||||||
|
pub(super) swapchain: Option<VulkanSwapchainProbe>,
|
||||||
|
pub(super) command_pool: vk::CommandPool,
|
||||||
|
pub(super) swapchain_resources: Option<VulkanSwapchainResources>,
|
||||||
|
pub(super) vertex_buffer: Option<VulkanAllocatedBuffer>,
|
||||||
|
pub(super) index_buffer: Option<VulkanAllocatedBuffer>,
|
||||||
|
pub(super) frame_sync: Vec<VulkanFrameSync>,
|
||||||
|
pub(super) images_in_flight: Vec<vk::Fence>,
|
||||||
|
pub(super) current_frame: usize,
|
||||||
|
pub(super) pending_extent: Option<(u32, u32)>,
|
||||||
|
pub(super) swapchain_recreate_count: u32,
|
||||||
|
pub(super) report: VulkanSmokeRendererReport,
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
#![allow(unsafe_code)]
|
||||||
|
|
||||||
|
use ash::{khr::surface, vk};
|
||||||
|
use fparkan_platform::NativeWindowHandles;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::ffi::CStr;
|
||||||
|
use std::os::raw::c_char;
|
||||||
|
|
||||||
|
use super::VulkanInstanceProbe;
|
||||||
|
use crate::policy::serialize_json_or_fallback;
|
||||||
|
|
||||||
|
/// Deterministic Vulkan surface creation plan.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanSurfacePlan {
|
||||||
|
/// Report schema version.
|
||||||
|
pub schema: u32,
|
||||||
|
/// Instance extensions required by the native display backend.
|
||||||
|
pub required_instance_extensions: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vulkan surface bootstrap error.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum VulkanSurfaceError {
|
||||||
|
/// No native raw window/display handles were available.
|
||||||
|
MissingNativeHandles,
|
||||||
|
/// Required platform surface extensions could not be enumerated.
|
||||||
|
RequiredExtensionsFailed {
|
||||||
|
/// Vulkan result.
|
||||||
|
result: vk::Result,
|
||||||
|
},
|
||||||
|
/// A required extension pointer was not valid UTF-8.
|
||||||
|
InvalidExtensionName,
|
||||||
|
/// Surface creation failed.
|
||||||
|
CreateFailed {
|
||||||
|
/// Vulkan result.
|
||||||
|
result: vk::Result,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for VulkanSurfaceError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::MissingNativeHandles => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"native window/display handles are required for Vulkan surface creation"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Self::RequiredExtensionsFailed { result } => write!(
|
||||||
|
f,
|
||||||
|
"failed to enumerate required Vulkan surface extensions: {result:?}"
|
||||||
|
),
|
||||||
|
Self::InvalidExtensionName => {
|
||||||
|
write!(f, "Vulkan surface extension name is not valid UTF-8")
|
||||||
|
}
|
||||||
|
Self::CreateFailed { result } => {
|
||||||
|
write!(f, "Vulkan surface creation failed: {result:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for VulkanSurfaceError {}
|
||||||
|
|
||||||
|
/// Created Vulkan surface probe.
|
||||||
|
pub struct VulkanSurfaceProbe {
|
||||||
|
pub(super) loader: surface::Instance,
|
||||||
|
pub(super) surface: vk::SurfaceKHR,
|
||||||
|
/// Deterministic surface creation report.
|
||||||
|
pub report: VulkanSurfacePlan,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for VulkanSurfaceProbe {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// SAFETY: The `SurfaceKHR` was created by this probe and is destroyed once during drop.
|
||||||
|
unsafe { self.loader.destroy_surface(self.surface, None) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a deterministic Vulkan surface plan from native window handles.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanSurfaceError`] when no native handles exist or the platform
|
||||||
|
/// display backend has no Vulkan surface extension mapping.
|
||||||
|
pub fn plan_vulkan_surface(
|
||||||
|
handles: Option<NativeWindowHandles>,
|
||||||
|
) -> Result<VulkanSurfacePlan, VulkanSurfaceError> {
|
||||||
|
let handles = handles.ok_or(VulkanSurfaceError::MissingNativeHandles)?;
|
||||||
|
let required = ash_window::enumerate_required_extensions(handles.display)
|
||||||
|
.map_err(|error| VulkanSurfaceError::RequiredExtensionsFailed { result: error })?;
|
||||||
|
let mut required_instance_extensions = Vec::with_capacity(required.len());
|
||||||
|
for extension in required {
|
||||||
|
let name = extension_name(*extension)?;
|
||||||
|
required_instance_extensions.push(name);
|
||||||
|
}
|
||||||
|
required_instance_extensions.sort();
|
||||||
|
required_instance_extensions.dedup();
|
||||||
|
Ok(VulkanSurfacePlan {
|
||||||
|
schema: 1,
|
||||||
|
required_instance_extensions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a Vulkan surface probe from native window handles.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanSurfaceError`] when handles are missing, required extensions
|
||||||
|
/// cannot be planned, or `vkCreate*SurfaceKHR` fails.
|
||||||
|
pub fn create_vulkan_surface_probe(
|
||||||
|
instance: &VulkanInstanceProbe,
|
||||||
|
handles: Option<NativeWindowHandles>,
|
||||||
|
) -> Result<VulkanSurfaceProbe, VulkanSurfaceError> {
|
||||||
|
let handles = handles.ok_or(VulkanSurfaceError::MissingNativeHandles)?;
|
||||||
|
let report = plan_vulkan_surface(Some(handles))?;
|
||||||
|
// SAFETY: The platform handles are only used to create a child surface owned by this probe.
|
||||||
|
let surface = unsafe {
|
||||||
|
ash_window::create_surface(
|
||||||
|
&instance.entry,
|
||||||
|
&instance.instance,
|
||||||
|
handles.display,
|
||||||
|
handles.window,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.map_err(|error| VulkanSurfaceError::CreateFailed { result: error })?;
|
||||||
|
Ok(VulkanSurfaceProbe {
|
||||||
|
loader: surface::Instance::new(&instance.entry, &instance.instance),
|
||||||
|
surface,
|
||||||
|
report,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a deterministic JSON Vulkan surface plan.
|
||||||
|
#[must_use]
|
||||||
|
pub fn render_surface_plan_json(plan: &VulkanSurfacePlan) -> String {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SurfacePlanJson<'a> {
|
||||||
|
schema: u32,
|
||||||
|
required_instance_extensions: &'a [String],
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize_json_or_fallback(
|
||||||
|
&SurfacePlanJson {
|
||||||
|
schema: plan.schema,
|
||||||
|
required_instance_extensions: &plan.required_instance_extensions,
|
||||||
|
},
|
||||||
|
"{\"schema\":0,\"required_instance_extensions\":[]}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn extension_name(extension: *const c_char) -> Result<String, VulkanSurfaceError> {
|
||||||
|
// SAFETY: `ash-window` returns extension pointers to static NUL-terminated Vulkan names.
|
||||||
|
let name = unsafe { CStr::from_ptr(extension) };
|
||||||
|
name.to_str()
|
||||||
|
.map(str::to_string)
|
||||||
|
.map_err(|_| VulkanSurfaceError::InvalidExtensionName)
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
#![allow(unsafe_code)]
|
||||||
|
|
||||||
|
use ash::{khr::swapchain, vk};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
capabilities::{
|
||||||
|
live_present_modes, live_surface_capabilities, live_surface_formats, unique_queue_families,
|
||||||
|
},
|
||||||
|
VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanRuntimeCapabilityError,
|
||||||
|
VulkanSurfaceProbe,
|
||||||
|
};
|
||||||
|
use crate::policy::{
|
||||||
|
plan_vulkan_swapchain, select_composite_alpha, VulkanSwapchainError, VulkanSwapchainPlan,
|
||||||
|
VulkanSwapchainRequest,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Created Vulkan swapchain probe.
|
||||||
|
pub struct VulkanSwapchainProbe {
|
||||||
|
loader: swapchain::Device,
|
||||||
|
swapchain: vk::SwapchainKHR,
|
||||||
|
/// Deterministic swapchain creation report.
|
||||||
|
pub report: VulkanSwapchainReport,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for VulkanSwapchainProbe {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// SAFETY: The swapchain was created by this probe and is destroyed once during drop.
|
||||||
|
unsafe { self.loader.destroy_swapchain(self.swapchain, None) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VulkanSwapchainProbe {
|
||||||
|
/// Returns the live swapchain handle.
|
||||||
|
#[must_use]
|
||||||
|
pub fn swapchain(&self) -> vk::SwapchainKHR {
|
||||||
|
self.swapchain
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the swapchain extension loader for this live swapchain.
|
||||||
|
#[must_use]
|
||||||
|
pub fn loader(&self) -> &swapchain::Device {
|
||||||
|
&self.loader
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runtime swapchain creation report.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanSwapchainReport {
|
||||||
|
/// Report schema version.
|
||||||
|
pub schema: u32,
|
||||||
|
/// Deterministic swapchain policy used for creation.
|
||||||
|
pub plan: VulkanSwapchainPlan,
|
||||||
|
/// Number of images returned by `vkGetSwapchainImagesKHR`.
|
||||||
|
pub image_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vulkan swapchain creation error.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum VulkanSwapchainProbeError {
|
||||||
|
/// Live runtime capability probing failed before swapchain creation.
|
||||||
|
Runtime(VulkanRuntimeCapabilityError),
|
||||||
|
/// Deterministic swapchain planning failed before create.
|
||||||
|
Plan(VulkanSwapchainError),
|
||||||
|
/// Surface capability query failed.
|
||||||
|
SurfaceCapabilitiesFailed {
|
||||||
|
/// Vulkan result.
|
||||||
|
result: vk::Result,
|
||||||
|
},
|
||||||
|
/// Swapchain creation failed.
|
||||||
|
CreateFailed {
|
||||||
|
/// Vulkan result.
|
||||||
|
result: vk::Result,
|
||||||
|
},
|
||||||
|
/// Swapchain image query failed.
|
||||||
|
ImagesFailed {
|
||||||
|
/// Vulkan result.
|
||||||
|
result: vk::Result,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for VulkanSwapchainProbeError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Runtime(error) => write!(f, "{error}"),
|
||||||
|
Self::Plan(error) => write!(f, "{error}"),
|
||||||
|
Self::SurfaceCapabilitiesFailed { result } => {
|
||||||
|
write!(f, "Vulkan surface capabilities query failed: {result:?}")
|
||||||
|
}
|
||||||
|
Self::CreateFailed { result } => {
|
||||||
|
write!(f, "Vulkan swapchain creation failed: {result:?}")
|
||||||
|
}
|
||||||
|
Self::ImagesFailed { result } => {
|
||||||
|
write!(f, "Vulkan swapchain image query failed: {result:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for VulkanSwapchainProbeError {}
|
||||||
|
|
||||||
|
/// Creates a Vulkan swapchain for the live logical device and surface.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanSwapchainProbeError`] when live surface capability queries,
|
||||||
|
/// swapchain creation, or swapchain image enumeration fails.
|
||||||
|
pub fn create_vulkan_swapchain_probe(
|
||||||
|
instance: &VulkanInstanceProbe,
|
||||||
|
surface: &VulkanSurfaceProbe,
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
) -> Result<VulkanSwapchainProbe, VulkanSwapchainProbeError> {
|
||||||
|
create_vulkan_swapchain_probe_for_extent(
|
||||||
|
instance,
|
||||||
|
surface,
|
||||||
|
device,
|
||||||
|
device.runtime.swapchain.extent,
|
||||||
|
vk::SwapchainKHR::null(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a Vulkan swapchain for the live logical device and surface at a specific extent.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanSwapchainProbeError`] when live surface capability queries,
|
||||||
|
/// swapchain creation, or swapchain image enumeration fails.
|
||||||
|
pub fn create_vulkan_swapchain_probe_for_extent(
|
||||||
|
instance: &VulkanInstanceProbe,
|
||||||
|
surface: &VulkanSurfaceProbe,
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
drawable_extent: (u32, u32),
|
||||||
|
old_swapchain: vk::SwapchainKHR,
|
||||||
|
) -> Result<VulkanSwapchainProbe, VulkanSwapchainProbeError> {
|
||||||
|
let raw_capabilities = {
|
||||||
|
// SAFETY: The physical device and surface are live query inputs and no handles are retained.
|
||||||
|
unsafe {
|
||||||
|
surface
|
||||||
|
.loader
|
||||||
|
.get_physical_device_surface_capabilities(device.physical_device(), surface.surface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map_err(|error| VulkanSwapchainProbeError::SurfaceCapabilitiesFailed { result: error })?;
|
||||||
|
let surface_formats = live_surface_formats(
|
||||||
|
surface,
|
||||||
|
device.physical_device(),
|
||||||
|
&device.report.device_name,
|
||||||
|
)
|
||||||
|
.map_err(VulkanSwapchainProbeError::Runtime)?;
|
||||||
|
let present_modes = live_present_modes(
|
||||||
|
surface,
|
||||||
|
device.physical_device(),
|
||||||
|
&device.report.device_name,
|
||||||
|
)
|
||||||
|
.map_err(VulkanSwapchainProbeError::Runtime)?;
|
||||||
|
let capabilities = live_surface_capabilities(
|
||||||
|
surface,
|
||||||
|
device.physical_device(),
|
||||||
|
&device.report.device_name,
|
||||||
|
)
|
||||||
|
.map_err(VulkanSwapchainProbeError::Runtime)?;
|
||||||
|
let plan = plan_vulkan_swapchain(&VulkanSwapchainRequest {
|
||||||
|
drawable_extent,
|
||||||
|
formats: surface_formats,
|
||||||
|
present_modes,
|
||||||
|
capabilities,
|
||||||
|
preferred_present_mode: vk::PresentModeKHR::MAILBOX.as_raw(),
|
||||||
|
})
|
||||||
|
.map_err(VulkanSwapchainProbeError::Plan)?;
|
||||||
|
let queue_family_indices = unique_queue_families(
|
||||||
|
device.runtime.capability.graphics_queue_family,
|
||||||
|
device.runtime.capability.present_queue_family,
|
||||||
|
);
|
||||||
|
let sharing_mode = if queue_family_indices.len() > 1 {
|
||||||
|
vk::SharingMode::CONCURRENT
|
||||||
|
} else {
|
||||||
|
vk::SharingMode::EXCLUSIVE
|
||||||
|
};
|
||||||
|
let create_info = vk::SwapchainCreateInfoKHR::default()
|
||||||
|
.surface(surface.surface)
|
||||||
|
.min_image_count(plan.image_count)
|
||||||
|
.image_format(vk::Format::from_raw(plan.format.format))
|
||||||
|
.image_color_space(vk::ColorSpaceKHR::from_raw(plan.format.color_space))
|
||||||
|
.image_extent(vk::Extent2D {
|
||||||
|
width: plan.extent.0,
|
||||||
|
height: plan.extent.1,
|
||||||
|
})
|
||||||
|
.image_array_layers(1)
|
||||||
|
.image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT)
|
||||||
|
.image_sharing_mode(sharing_mode)
|
||||||
|
.queue_family_indices(&queue_family_indices)
|
||||||
|
.pre_transform(raw_capabilities.current_transform)
|
||||||
|
.composite_alpha(select_composite_alpha(
|
||||||
|
raw_capabilities.supported_composite_alpha,
|
||||||
|
))
|
||||||
|
.present_mode(vk::PresentModeKHR::from_raw(plan.present_mode))
|
||||||
|
.old_swapchain(old_swapchain)
|
||||||
|
.clipped(true);
|
||||||
|
let loader = swapchain::Device::new(&instance.instance, device.device());
|
||||||
|
// SAFETY: The create info references live instance/device/surface handles for this call.
|
||||||
|
let swapchain = unsafe { loader.create_swapchain(&create_info, None) }
|
||||||
|
.map_err(|error| VulkanSwapchainProbeError::CreateFailed { result: error })?;
|
||||||
|
// SAFETY: The swapchain was created above and the returned image handles are owned by it.
|
||||||
|
let images = match unsafe { loader.get_swapchain_images(swapchain) } {
|
||||||
|
Ok(images) => images,
|
||||||
|
Err(error) => {
|
||||||
|
// SAFETY: The swapchain was created above on this loader/device pair and is destroyed on setup failure.
|
||||||
|
unsafe { loader.destroy_swapchain(swapchain, None) };
|
||||||
|
return Err(VulkanSwapchainProbeError::ImagesFailed { result: error });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(VulkanSwapchainProbe {
|
||||||
|
loader,
|
||||||
|
swapchain,
|
||||||
|
report: VulkanSwapchainReport {
|
||||||
|
schema: 1,
|
||||||
|
plan,
|
||||||
|
image_count: images.len().try_into().unwrap_or(u32::MAX),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,501 @@
|
|||||||
|
#![allow(unsafe_code)]
|
||||||
|
|
||||||
|
use ash::vk;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
color_subresource_range, VulkanAllocatedBuffer, VulkanLogicalDeviceProbe,
|
||||||
|
VulkanSmokeRendererError, VulkanSwapchainProbe, TRIANGLE_FRAGMENT_SHADER_WORDS,
|
||||||
|
TRIANGLE_VERTEX_SHADER_WORDS,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(super) struct VulkanSwapchainResources {
|
||||||
|
pub(super) image_views: Vec<vk::ImageView>,
|
||||||
|
pub(super) render_pass: vk::RenderPass,
|
||||||
|
pub(super) pipeline_layout: vk::PipelineLayout,
|
||||||
|
pub(super) pipeline: vk::Pipeline,
|
||||||
|
pub(super) framebuffers: Vec<vk::Framebuffer>,
|
||||||
|
pub(super) command_buffers: Vec<vk::CommandBuffer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PartialSwapchainResources {
|
||||||
|
image_views: Vec<vk::ImageView>,
|
||||||
|
render_pass: Option<vk::RenderPass>,
|
||||||
|
pipeline_layout: Option<vk::PipelineLayout>,
|
||||||
|
pipeline: Option<vk::Pipeline>,
|
||||||
|
framebuffers: Vec<vk::Framebuffer>,
|
||||||
|
command_buffers: Vec<vk::CommandBuffer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn create_swapchain_resources(
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
swapchain: &VulkanSwapchainProbe,
|
||||||
|
command_pool: vk::CommandPool,
|
||||||
|
vertex_buffer: &VulkanAllocatedBuffer,
|
||||||
|
index_buffer: &VulkanAllocatedBuffer,
|
||||||
|
reuse_command_pool: bool,
|
||||||
|
) -> Result<VulkanSwapchainResources, VulkanSmokeRendererError> {
|
||||||
|
// SAFETY: The swapchain is live and owned by this renderer for the duration of the query.
|
||||||
|
let images = unsafe {
|
||||||
|
swapchain
|
||||||
|
.loader()
|
||||||
|
.get_swapchain_images(swapchain.swapchain())
|
||||||
|
}
|
||||||
|
.map_err(|error| VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkGetSwapchainImagesKHR",
|
||||||
|
result: error,
|
||||||
|
})?;
|
||||||
|
let mut partial = PartialSwapchainResources {
|
||||||
|
image_views: create_swapchain_image_views(
|
||||||
|
device,
|
||||||
|
&images,
|
||||||
|
swapchain.report.plan.format.format,
|
||||||
|
)?,
|
||||||
|
render_pass: None,
|
||||||
|
pipeline_layout: None,
|
||||||
|
pipeline: None,
|
||||||
|
framebuffers: Vec::new(),
|
||||||
|
command_buffers: Vec::new(),
|
||||||
|
};
|
||||||
|
let (render_pass, pipeline_layout, pipeline) = match create_swapchain_pipeline_bundle(
|
||||||
|
device,
|
||||||
|
swapchain.report.plan.format.format,
|
||||||
|
swapchain.report.plan.extent,
|
||||||
|
) {
|
||||||
|
Ok(bundle) => bundle,
|
||||||
|
Err(error) => {
|
||||||
|
destroy_partial_swapchain_resources(device, command_pool, partial);
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
partial.render_pass = Some(render_pass);
|
||||||
|
partial.pipeline_layout = Some(pipeline_layout);
|
||||||
|
partial.pipeline = Some(pipeline);
|
||||||
|
let framebuffers = match create_swapchain_framebuffers(
|
||||||
|
device,
|
||||||
|
render_pass,
|
||||||
|
&partial.image_views,
|
||||||
|
swapchain.report.plan.extent,
|
||||||
|
) {
|
||||||
|
Ok(framebuffers) => framebuffers,
|
||||||
|
Err(error) => {
|
||||||
|
destroy_partial_swapchain_resources(device, command_pool, partial);
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
partial.framebuffers = framebuffers;
|
||||||
|
reset_reusable_command_pool(device, command_pool, reuse_command_pool)?;
|
||||||
|
let command_buffers = match allocate_command_buffers(
|
||||||
|
device,
|
||||||
|
command_pool,
|
||||||
|
u32::try_from(images.len()).unwrap_or(u32::MAX),
|
||||||
|
) {
|
||||||
|
Ok(command_buffers) => command_buffers,
|
||||||
|
Err(error) => {
|
||||||
|
destroy_partial_swapchain_resources(device, command_pool, partial);
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
partial.command_buffers = command_buffers;
|
||||||
|
let _ = (vertex_buffer, index_buffer);
|
||||||
|
Ok(VulkanSwapchainResources {
|
||||||
|
image_views: partial.image_views,
|
||||||
|
render_pass,
|
||||||
|
pipeline_layout,
|
||||||
|
pipeline,
|
||||||
|
framebuffers: partial.framebuffers,
|
||||||
|
command_buffers: partial.command_buffers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_swapchain_image_views(
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
images: &[vk::Image],
|
||||||
|
format: i32,
|
||||||
|
) -> Result<Vec<vk::ImageView>, VulkanSmokeRendererError> {
|
||||||
|
let mut image_views = Vec::with_capacity(images.len());
|
||||||
|
for image in images.iter().copied() {
|
||||||
|
image_views.push(create_image_view(device, image, format)?);
|
||||||
|
}
|
||||||
|
Ok(image_views)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_swapchain_pipeline_bundle(
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
format: i32,
|
||||||
|
extent: (u32, u32),
|
||||||
|
) -> Result<(vk::RenderPass, vk::PipelineLayout, vk::Pipeline), VulkanSmokeRendererError> {
|
||||||
|
let render_pass = create_render_pass(device, format)?;
|
||||||
|
let pipeline_layout = create_pipeline_layout(device).inspect_err(|_| {
|
||||||
|
// SAFETY: The render pass was created above on this live logical device and is destroyed on setup failure.
|
||||||
|
unsafe { device.device().destroy_render_pass(render_pass, None) };
|
||||||
|
})?;
|
||||||
|
let pipeline = create_graphics_pipeline(device, render_pass, pipeline_layout, extent)
|
||||||
|
.inspect_err(|_| {
|
||||||
|
// SAFETY: These objects were created above on this live logical device and are destroyed on setup failure.
|
||||||
|
unsafe {
|
||||||
|
device
|
||||||
|
.device()
|
||||||
|
.destroy_pipeline_layout(pipeline_layout, None);
|
||||||
|
device.device().destroy_render_pass(render_pass, None);
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
Ok((render_pass, pipeline_layout, pipeline))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_swapchain_framebuffers(
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
render_pass: vk::RenderPass,
|
||||||
|
image_views: &[vk::ImageView],
|
||||||
|
extent: (u32, u32),
|
||||||
|
) -> Result<Vec<vk::Framebuffer>, VulkanSmokeRendererError> {
|
||||||
|
let mut framebuffers = Vec::with_capacity(image_views.len());
|
||||||
|
for image_view in image_views.iter().copied() {
|
||||||
|
match create_framebuffer(device, render_pass, image_view, extent) {
|
||||||
|
Ok(framebuffer) => framebuffers.push(framebuffer),
|
||||||
|
Err(error) => {
|
||||||
|
// SAFETY: These framebuffers were created above on this live logical device and are destroyed on setup failure.
|
||||||
|
unsafe {
|
||||||
|
for framebuffer in framebuffers.iter().copied() {
|
||||||
|
device.device().destroy_framebuffer(framebuffer, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(framebuffers)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_reusable_command_pool(
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
command_pool: vk::CommandPool,
|
||||||
|
reuse_command_pool: bool,
|
||||||
|
) -> Result<(), VulkanSmokeRendererError> {
|
||||||
|
if !reuse_command_pool {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// SAFETY: All command buffers allocated from the live pool are freed before reallocating them.
|
||||||
|
unsafe {
|
||||||
|
device
|
||||||
|
.device()
|
||||||
|
.reset_command_pool(command_pool, vk::CommandPoolResetFlags::empty())
|
||||||
|
}
|
||||||
|
.map_err(|error| VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkResetCommandPool",
|
||||||
|
result: error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_image_view(
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
image: vk::Image,
|
||||||
|
format: i32,
|
||||||
|
) -> Result<vk::ImageView, VulkanSmokeRendererError> {
|
||||||
|
let create_info = vk::ImageViewCreateInfo::default()
|
||||||
|
.image(image)
|
||||||
|
.view_type(vk::ImageViewType::TYPE_2D)
|
||||||
|
.format(vk::Format::from_raw(format))
|
||||||
|
.subresource_range(color_subresource_range());
|
||||||
|
// SAFETY: The image comes from the live swapchain and the subresource range covers its color aspect.
|
||||||
|
unsafe { device.device().create_image_view(&create_info, None) }.map_err(|error| {
|
||||||
|
VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkCreateImageView",
|
||||||
|
result: error,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_render_pass(
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
format: i32,
|
||||||
|
) -> Result<vk::RenderPass, VulkanSmokeRendererError> {
|
||||||
|
let color_attachment = vk::AttachmentDescription::default()
|
||||||
|
.format(vk::Format::from_raw(format))
|
||||||
|
.samples(vk::SampleCountFlags::TYPE_1)
|
||||||
|
.load_op(vk::AttachmentLoadOp::CLEAR)
|
||||||
|
.store_op(vk::AttachmentStoreOp::STORE)
|
||||||
|
.initial_layout(vk::ImageLayout::UNDEFINED)
|
||||||
|
.final_layout(vk::ImageLayout::PRESENT_SRC_KHR);
|
||||||
|
let color_attachment_ref = vk::AttachmentReference::default()
|
||||||
|
.attachment(0)
|
||||||
|
.layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
|
||||||
|
let color_attachments = [color_attachment_ref];
|
||||||
|
let subpass = vk::SubpassDescription::default()
|
||||||
|
.pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
|
||||||
|
.color_attachments(&color_attachments);
|
||||||
|
let dependency = vk::SubpassDependency::default()
|
||||||
|
.src_subpass(vk::SUBPASS_EXTERNAL)
|
||||||
|
.dst_subpass(0)
|
||||||
|
.src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
|
||||||
|
.dst_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)
|
||||||
|
.src_access_mask(vk::AccessFlags::empty())
|
||||||
|
.dst_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE);
|
||||||
|
let attachments = [color_attachment];
|
||||||
|
let subpasses = [subpass];
|
||||||
|
let dependencies = [dependency];
|
||||||
|
let create_info = vk::RenderPassCreateInfo::default()
|
||||||
|
.attachments(&attachments)
|
||||||
|
.subpasses(&subpasses)
|
||||||
|
.dependencies(&dependencies);
|
||||||
|
// SAFETY: The render-pass create info only references stack-owned descriptors.
|
||||||
|
unsafe { device.device().create_render_pass(&create_info, None) }.map_err(|error| {
|
||||||
|
VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkCreateRenderPass",
|
||||||
|
result: error,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_pipeline_layout(
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
) -> Result<vk::PipelineLayout, VulkanSmokeRendererError> {
|
||||||
|
let create_info = vk::PipelineLayoutCreateInfo::default();
|
||||||
|
// SAFETY: The pipeline layout has no descriptor sets or push constants in Stage 0 smoke.
|
||||||
|
unsafe { device.device().create_pipeline_layout(&create_info, None) }.map_err(|error| {
|
||||||
|
VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkCreatePipelineLayout",
|
||||||
|
result: error,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cast_precision_loss)]
|
||||||
|
fn extent_component_to_f32(value: u32) -> f32 {
|
||||||
|
value as f32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
fn create_graphics_pipeline(
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
render_pass: vk::RenderPass,
|
||||||
|
pipeline_layout: vk::PipelineLayout,
|
||||||
|
extent: (u32, u32),
|
||||||
|
) -> Result<vk::Pipeline, VulkanSmokeRendererError> {
|
||||||
|
let vertex_shader = create_shader_module(device, TRIANGLE_VERTEX_SHADER_WORDS)?;
|
||||||
|
let fragment_shader = match create_shader_module(device, TRIANGLE_FRAGMENT_SHADER_WORDS) {
|
||||||
|
Ok(module) => module,
|
||||||
|
Err(error) => {
|
||||||
|
// SAFETY: The shader module was created above on this live logical device and is destroyed on setup failure.
|
||||||
|
unsafe { device.device().destroy_shader_module(vertex_shader, None) };
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let entry_point = c"main";
|
||||||
|
let shader_stages = [
|
||||||
|
vk::PipelineShaderStageCreateInfo::default()
|
||||||
|
.module(vertex_shader)
|
||||||
|
.name(entry_point)
|
||||||
|
.stage(vk::ShaderStageFlags::VERTEX),
|
||||||
|
vk::PipelineShaderStageCreateInfo::default()
|
||||||
|
.module(fragment_shader)
|
||||||
|
.name(entry_point)
|
||||||
|
.stage(vk::ShaderStageFlags::FRAGMENT),
|
||||||
|
];
|
||||||
|
let vertex_binding = vk::VertexInputBindingDescription::default()
|
||||||
|
.binding(0)
|
||||||
|
.stride(u32::try_from(5 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX))
|
||||||
|
.input_rate(vk::VertexInputRate::VERTEX);
|
||||||
|
let vertex_attributes = [
|
||||||
|
vk::VertexInputAttributeDescription::default()
|
||||||
|
.binding(0)
|
||||||
|
.location(0)
|
||||||
|
.format(vk::Format::R32G32_SFLOAT)
|
||||||
|
.offset(0),
|
||||||
|
vk::VertexInputAttributeDescription::default()
|
||||||
|
.binding(0)
|
||||||
|
.location(1)
|
||||||
|
.format(vk::Format::R32G32B32_SFLOAT)
|
||||||
|
.offset(u32::try_from(2 * std::mem::size_of::<f32>()).unwrap_or(u32::MAX)),
|
||||||
|
];
|
||||||
|
let vertex_bindings = [vertex_binding];
|
||||||
|
let vertex_input_state = vk::PipelineVertexInputStateCreateInfo::default()
|
||||||
|
.vertex_binding_descriptions(&vertex_bindings)
|
||||||
|
.vertex_attribute_descriptions(&vertex_attributes);
|
||||||
|
let input_assembly_state = vk::PipelineInputAssemblyStateCreateInfo::default()
|
||||||
|
.topology(vk::PrimitiveTopology::TRIANGLE_LIST)
|
||||||
|
.primitive_restart_enable(false);
|
||||||
|
let viewports = [vk::Viewport {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
width: extent_component_to_f32(extent.0),
|
||||||
|
height: extent_component_to_f32(extent.1),
|
||||||
|
min_depth: 0.0,
|
||||||
|
max_depth: 1.0,
|
||||||
|
}];
|
||||||
|
let scissors = [vk::Rect2D {
|
||||||
|
offset: vk::Offset2D { x: 0, y: 0 },
|
||||||
|
extent: vk::Extent2D {
|
||||||
|
width: extent.0,
|
||||||
|
height: extent.1,
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
let viewport_state = vk::PipelineViewportStateCreateInfo::default()
|
||||||
|
.viewports(&viewports)
|
||||||
|
.scissors(&scissors);
|
||||||
|
let rasterization_state = vk::PipelineRasterizationStateCreateInfo::default()
|
||||||
|
.depth_clamp_enable(false)
|
||||||
|
.rasterizer_discard_enable(false)
|
||||||
|
.polygon_mode(vk::PolygonMode::FILL)
|
||||||
|
.line_width(1.0)
|
||||||
|
.cull_mode(vk::CullModeFlags::BACK)
|
||||||
|
.front_face(vk::FrontFace::CLOCKWISE)
|
||||||
|
.depth_bias_enable(false);
|
||||||
|
let multisample_state = vk::PipelineMultisampleStateCreateInfo::default()
|
||||||
|
.sample_shading_enable(false)
|
||||||
|
.rasterization_samples(vk::SampleCountFlags::TYPE_1);
|
||||||
|
let color_blend_attachment = vk::PipelineColorBlendAttachmentState::default()
|
||||||
|
.color_write_mask(
|
||||||
|
vk::ColorComponentFlags::R
|
||||||
|
| vk::ColorComponentFlags::G
|
||||||
|
| vk::ColorComponentFlags::B
|
||||||
|
| vk::ColorComponentFlags::A,
|
||||||
|
)
|
||||||
|
.blend_enable(false);
|
||||||
|
let color_blend_attachments = [color_blend_attachment];
|
||||||
|
let color_blend_state = vk::PipelineColorBlendStateCreateInfo::default()
|
||||||
|
.logic_op_enable(false)
|
||||||
|
.attachments(&color_blend_attachments);
|
||||||
|
let create_info = vk::GraphicsPipelineCreateInfo::default()
|
||||||
|
.stages(&shader_stages)
|
||||||
|
.vertex_input_state(&vertex_input_state)
|
||||||
|
.input_assembly_state(&input_assembly_state)
|
||||||
|
.viewport_state(&viewport_state)
|
||||||
|
.rasterization_state(&rasterization_state)
|
||||||
|
.multisample_state(&multisample_state)
|
||||||
|
.color_blend_state(&color_blend_state)
|
||||||
|
.layout(pipeline_layout)
|
||||||
|
.render_pass(render_pass)
|
||||||
|
.subpass(0);
|
||||||
|
let create_infos = [create_info];
|
||||||
|
// SAFETY: Pipeline creation references only stack-owned descriptions and live render-pass/layout handles.
|
||||||
|
let pipeline_result = unsafe {
|
||||||
|
device
|
||||||
|
.device()
|
||||||
|
.create_graphics_pipelines(vk::PipelineCache::null(), &create_infos, None)
|
||||||
|
};
|
||||||
|
// SAFETY: The shader modules were created above on this live logical device and are no longer needed after pipeline creation.
|
||||||
|
unsafe {
|
||||||
|
device.device().destroy_shader_module(vertex_shader, None);
|
||||||
|
device.device().destroy_shader_module(fragment_shader, None);
|
||||||
|
}
|
||||||
|
let pipelines =
|
||||||
|
pipeline_result.map_err(|(_, error)| VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkCreateGraphicsPipelines",
|
||||||
|
result: error,
|
||||||
|
})?;
|
||||||
|
Ok(pipelines[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_shader_module(
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
words: &[u32],
|
||||||
|
) -> Result<vk::ShaderModule, VulkanSmokeRendererError> {
|
||||||
|
let create_info = vk::ShaderModuleCreateInfo::default().code(words);
|
||||||
|
// SAFETY: The SPIR-V slice points to static checked-in words and lives for the duration of the call.
|
||||||
|
unsafe { device.device().create_shader_module(&create_info, None) }.map_err(|error| {
|
||||||
|
VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkCreateShaderModule",
|
||||||
|
result: error,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_framebuffer(
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
render_pass: vk::RenderPass,
|
||||||
|
image_view: vk::ImageView,
|
||||||
|
extent: (u32, u32),
|
||||||
|
) -> Result<vk::Framebuffer, VulkanSmokeRendererError> {
|
||||||
|
let attachments = [image_view];
|
||||||
|
let create_info = vk::FramebufferCreateInfo::default()
|
||||||
|
.render_pass(render_pass)
|
||||||
|
.attachments(&attachments)
|
||||||
|
.width(extent.0)
|
||||||
|
.height(extent.1)
|
||||||
|
.layers(1);
|
||||||
|
// SAFETY: The framebuffer references a live image view and render pass owned by this logical device.
|
||||||
|
unsafe { device.device().create_framebuffer(&create_info, None) }.map_err(|error| {
|
||||||
|
VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkCreateFramebuffer",
|
||||||
|
result: error,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn allocate_command_buffers(
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
command_pool: vk::CommandPool,
|
||||||
|
count: u32,
|
||||||
|
) -> Result<Vec<vk::CommandBuffer>, VulkanSmokeRendererError> {
|
||||||
|
let allocate_info = vk::CommandBufferAllocateInfo::default()
|
||||||
|
.command_pool(command_pool)
|
||||||
|
.level(vk::CommandBufferLevel::PRIMARY)
|
||||||
|
.command_buffer_count(count);
|
||||||
|
// SAFETY: The command pool belongs to this live logical device and the allocation info is stack-owned.
|
||||||
|
unsafe { device.device().allocate_command_buffers(&allocate_info) }.map_err(|error| {
|
||||||
|
VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkAllocateCommandBuffers",
|
||||||
|
result: error,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn destroy_swapchain_resources(
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
command_pool: vk::CommandPool,
|
||||||
|
resources: VulkanSwapchainResources,
|
||||||
|
) {
|
||||||
|
// SAFETY: All handles belong to this live logical device and are destroyed during renderer teardown/recreation.
|
||||||
|
unsafe {
|
||||||
|
if !resources.command_buffers.is_empty() {
|
||||||
|
device
|
||||||
|
.device()
|
||||||
|
.free_command_buffers(command_pool, &resources.command_buffers);
|
||||||
|
}
|
||||||
|
for framebuffer in resources.framebuffers {
|
||||||
|
device.device().destroy_framebuffer(framebuffer, None);
|
||||||
|
}
|
||||||
|
device.device().destroy_pipeline(resources.pipeline, None);
|
||||||
|
device
|
||||||
|
.device()
|
||||||
|
.destroy_pipeline_layout(resources.pipeline_layout, None);
|
||||||
|
device
|
||||||
|
.device()
|
||||||
|
.destroy_render_pass(resources.render_pass, None);
|
||||||
|
for image_view in resources.image_views {
|
||||||
|
device.device().destroy_image_view(image_view, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroy_partial_swapchain_resources(
|
||||||
|
device: &VulkanLogicalDeviceProbe,
|
||||||
|
command_pool: vk::CommandPool,
|
||||||
|
partial: PartialSwapchainResources,
|
||||||
|
) {
|
||||||
|
// SAFETY: All handles in the partial bundle belong to this live logical device and are destroyed on setup failure.
|
||||||
|
unsafe {
|
||||||
|
if !partial.command_buffers.is_empty() {
|
||||||
|
device
|
||||||
|
.device()
|
||||||
|
.free_command_buffers(command_pool, &partial.command_buffers);
|
||||||
|
}
|
||||||
|
for framebuffer in partial.framebuffers {
|
||||||
|
device.device().destroy_framebuffer(framebuffer, None);
|
||||||
|
}
|
||||||
|
if let Some(pipeline) = partial.pipeline {
|
||||||
|
device.device().destroy_pipeline(pipeline, None);
|
||||||
|
}
|
||||||
|
if let Some(pipeline_layout) = partial.pipeline_layout {
|
||||||
|
device
|
||||||
|
.device()
|
||||||
|
.destroy_pipeline_layout(pipeline_layout, None);
|
||||||
|
}
|
||||||
|
if let Some(render_pass) = partial.render_pass {
|
||||||
|
device.device().destroy_render_pass(render_pass, None);
|
||||||
|
}
|
||||||
|
for image_view in partial.image_views {
|
||||||
|
device.device().destroy_image_view(image_view, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,772 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::policy::{KHR_PORTABILITY_SUBSET_EXTENSION, KHR_SWAPCHAIN_EXTENSION};
|
||||||
|
use crate::shader_manifest::{
|
||||||
|
SHADER_COMPILER_BINARY_SHA256, SHADER_COMPILER_NAME, SHADER_COMPILER_VERSION,
|
||||||
|
SHADER_MANIFEST_SCHEMA, SHADER_TARGET_ENV, SPIRV_MAGIC, SPIRV_VALIDATOR_BINARY_SHA256,
|
||||||
|
SPIRV_VALIDATOR_NAME, SPIRV_VALIDATOR_VERSION, SPIRV_VERSION_1_0,
|
||||||
|
TRIANGLE_VERTEX_COMPILE_COMMAND, TRIANGLE_VERTEX_SOURCE_PATH, TRIANGLE_VERTEX_SOURCE_SHA256,
|
||||||
|
TRIANGLE_VERTEX_SPIRV_PATH, TRIANGLE_VERTEX_VALIDATE_COMMAND,
|
||||||
|
};
|
||||||
|
use crate::*;
|
||||||
|
use fparkan_platform::{DepthStencilSupport, RenderRequest};
|
||||||
|
use fparkan_render::{
|
||||||
|
DrawCommand, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderCommand, RenderPhase,
|
||||||
|
};
|
||||||
|
use fparkan_render::{RenderBackend, RenderError};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn planning_backend_tracks_render_request_and_simulated_present() -> Result<(), RenderError> {
|
||||||
|
let mut backend = VulkanPlanningBackend::new();
|
||||||
|
let request = RenderRequest {
|
||||||
|
presentation: fparkan_platform::PresentationMode::Immediate,
|
||||||
|
..RenderRequest::conservative()
|
||||||
|
};
|
||||||
|
backend.set_render_request(request);
|
||||||
|
assert_eq!(backend.render_request(), request);
|
||||||
|
assert_eq!(backend.report().request.current_request, request);
|
||||||
|
assert_eq!(backend.report().request.request_updates, 1);
|
||||||
|
|
||||||
|
let commands = fparkan_render::RenderCommandList {
|
||||||
|
commands: vec![
|
||||||
|
RenderCommand::BeginFrame,
|
||||||
|
RenderCommand::Draw(DrawCommand {
|
||||||
|
id: DrawId(11),
|
||||||
|
phase: RenderPhase::Opaque,
|
||||||
|
object_id: None,
|
||||||
|
mesh: GpuMeshId(1),
|
||||||
|
material: GpuMaterialId(2),
|
||||||
|
transform: [1.0; 16],
|
||||||
|
range: IndexRange { start: 0, count: 3 },
|
||||||
|
stable_order: 7,
|
||||||
|
}),
|
||||||
|
RenderCommand::EndFrame,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
backend.execute(&commands)?;
|
||||||
|
assert_eq!(backend.state(), VulkanPlanningBackendState::Configured);
|
||||||
|
assert_eq!(backend.report().execution.planned_frames, 1);
|
||||||
|
assert_eq!(backend.report().execution.submission_plans, 1);
|
||||||
|
assert_eq!(backend.report().execution.simulated_presents, 1);
|
||||||
|
assert!(backend.report().execution.last_capture_size > 0);
|
||||||
|
assert_eq!(
|
||||||
|
backend.report().last_frame_submission,
|
||||||
|
Some(VulkanFrameSubmissionPlan {
|
||||||
|
schema: 1,
|
||||||
|
frames_in_flight: 2,
|
||||||
|
command_buffers: 2,
|
||||||
|
semaphores_per_frame: 2,
|
||||||
|
fences_per_frame: 1,
|
||||||
|
draw_count: 1,
|
||||||
|
indexed_vertex_count: 3,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_submission_plan_json_is_stable() -> Result<(), RenderError> {
|
||||||
|
let commands = fparkan_render::RenderCommandList {
|
||||||
|
commands: vec![
|
||||||
|
RenderCommand::BeginFrame,
|
||||||
|
RenderCommand::Draw(DrawCommand {
|
||||||
|
id: DrawId(11),
|
||||||
|
phase: RenderPhase::Opaque,
|
||||||
|
object_id: None,
|
||||||
|
mesh: GpuMeshId(1),
|
||||||
|
material: GpuMaterialId(2),
|
||||||
|
transform: [1.0; 16],
|
||||||
|
range: IndexRange { start: 0, count: 3 },
|
||||||
|
stable_order: 7,
|
||||||
|
}),
|
||||||
|
RenderCommand::EndFrame,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let swapchain = VulkanSwapchainPlan {
|
||||||
|
schema: 1,
|
||||||
|
extent: (1, 1),
|
||||||
|
format: VulkanSurfaceFormat {
|
||||||
|
format: vk::Format::B8G8R8A8_SRGB.as_raw(),
|
||||||
|
color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
|
||||||
|
},
|
||||||
|
present_mode: vk::PresentModeKHR::FIFO.as_raw(),
|
||||||
|
image_count: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
let plan = plan_vulkan_frame_submission(&swapchain, &commands)?;
|
||||||
|
|
||||||
|
assert_eq!(plan.frames_in_flight, 2);
|
||||||
|
assert_eq!(plan.command_buffers, 3);
|
||||||
|
assert_eq!(plan.draw_count, 1);
|
||||||
|
assert_eq!(plan.indexed_vertex_count, 3);
|
||||||
|
assert_eq!(
|
||||||
|
render_frame_submission_plan_json(&plan),
|
||||||
|
"{\"schema\":1,\"frames_in_flight\":2,\"command_buffers\":3,\"semaphores_per_frame\":2,\"fences_per_frame\":1,\"draw_count\":1,\"indexed_vertex_count\":3}"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn device_scoring_is_deterministic_and_prefers_discrete_unified_queue() {
|
||||||
|
let devices = vec![
|
||||||
|
device("SwiftShader", VulkanDeviceType::Cpu, 0, true, false),
|
||||||
|
device("Discrete", VulkanDeviceType::DiscreteGpu, 1, true, false),
|
||||||
|
device(
|
||||||
|
"Integrated",
|
||||||
|
VulkanDeviceType::IntegratedGpu,
|
||||||
|
2,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let report = select_physical_device(&devices).expect("selected device");
|
||||||
|
|
||||||
|
assert_eq!(report.device_name, "Discrete");
|
||||||
|
assert_eq!(report.graphics_queue_family, 1);
|
||||||
|
assert_eq!(report.present_queue_family, 1);
|
||||||
|
assert!(!report.portability_subset);
|
||||||
|
assert_eq!(report.enabled_extensions, vec![KHR_SWAPCHAIN_EXTENSION]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn device_selection_skips_rejected_candidates_before_accepting_valid_gpu() {
|
||||||
|
let mut rejected = device("Rejected", VulkanDeviceType::DiscreteGpu, 0, true, false);
|
||||||
|
rejected.queue_families[0].present = false;
|
||||||
|
let accepted = device("Accepted", VulkanDeviceType::IntegratedGpu, 2, true, false);
|
||||||
|
|
||||||
|
let report = select_physical_device(&[rejected, accepted]).expect("selected fallback device");
|
||||||
|
|
||||||
|
assert_eq!(report.device_name, "Accepted");
|
||||||
|
assert_eq!(report.graphics_queue_family, 2);
|
||||||
|
assert_eq!(report.present_queue_family, 2);
|
||||||
|
assert_eq!(
|
||||||
|
report.rejected_devices,
|
||||||
|
vec![VulkanRejectedDeviceReport {
|
||||||
|
device_name: "Rejected".to_string(),
|
||||||
|
reason_code: "no_present_queue",
|
||||||
|
reason: "Vulkan device Rejected has no present queue".to_string(),
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn queue_family_selection_prefers_lowest_index_unified_family() {
|
||||||
|
let mut candidate = device(
|
||||||
|
"Unified later in list",
|
||||||
|
VulkanDeviceType::DiscreteGpu,
|
||||||
|
7,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
candidate.queue_families = vec![
|
||||||
|
VulkanQueueFamily {
|
||||||
|
index: 9,
|
||||||
|
graphics: true,
|
||||||
|
present: true,
|
||||||
|
},
|
||||||
|
VulkanQueueFamily {
|
||||||
|
index: 3,
|
||||||
|
graphics: true,
|
||||||
|
present: true,
|
||||||
|
},
|
||||||
|
VulkanQueueFamily {
|
||||||
|
index: 1,
|
||||||
|
graphics: true,
|
||||||
|
present: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let report = select_physical_device(&[candidate]).expect("selected unified queue");
|
||||||
|
|
||||||
|
assert_eq!(report.graphics_queue_family, 3);
|
||||||
|
assert_eq!(report.present_queue_family, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn portability_subset_is_reported_and_enabled_when_exposed() {
|
||||||
|
let report = select_physical_device(&[device(
|
||||||
|
"MoltenVK",
|
||||||
|
VulkanDeviceType::IntegratedGpu,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
)])
|
||||||
|
.expect("selected device");
|
||||||
|
|
||||||
|
assert!(report.portability_subset);
|
||||||
|
assert_eq!(
|
||||||
|
report.enabled_extensions,
|
||||||
|
vec![
|
||||||
|
KHR_SWAPCHAIN_EXTENSION.to_string(),
|
||||||
|
KHR_PORTABILITY_SUBSET_EXTENSION.to_string()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_loader_candidates_are_reported() {
|
||||||
|
assert_eq!(
|
||||||
|
select_physical_device(&[]),
|
||||||
|
Err(VulkanCapabilityError::NoPhysicalDevice)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_low_api_version() {
|
||||||
|
let mut candidate = device("Old GPU", VulkanDeviceType::DiscreteGpu, 0, true, false);
|
||||||
|
candidate.api_version = vk::API_VERSION_1_0;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
select_physical_device(&[candidate]),
|
||||||
|
Err(VulkanCapabilityError::ApiVersionTooLow { .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_missing_graphics_present_swapchain_and_format() {
|
||||||
|
let mut no_graphics = device("No graphics", VulkanDeviceType::DiscreteGpu, 0, true, false);
|
||||||
|
no_graphics.queue_families[0].graphics = false;
|
||||||
|
assert!(matches!(
|
||||||
|
select_physical_device(&[no_graphics]),
|
||||||
|
Err(VulkanCapabilityError::NoGraphicsQueue { .. })
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut no_present = device("No present", VulkanDeviceType::DiscreteGpu, 0, true, false);
|
||||||
|
no_present.queue_families[0].present = false;
|
||||||
|
assert!(matches!(
|
||||||
|
select_physical_device(&[no_present]),
|
||||||
|
Err(VulkanCapabilityError::NoPresentQueue { .. })
|
||||||
|
));
|
||||||
|
|
||||||
|
let no_swapchain = device(
|
||||||
|
"No swapchain",
|
||||||
|
VulkanDeviceType::DiscreteGpu,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
select_physical_device(&[no_swapchain]),
|
||||||
|
Err(VulkanCapabilityError::MissingSwapchainExtension { .. })
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut no_format = device("No format", VulkanDeviceType::DiscreteGpu, 0, true, false);
|
||||||
|
no_format.surface_formats.clear();
|
||||||
|
assert!(matches!(
|
||||||
|
select_physical_device(&[no_format]),
|
||||||
|
Err(VulkanCapabilityError::MissingSurfaceFormat { .. })
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut no_present_mode = device(
|
||||||
|
"No present mode",
|
||||||
|
VulkanDeviceType::DiscreteGpu,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
no_present_mode.present_modes.clear();
|
||||||
|
assert!(matches!(
|
||||||
|
select_physical_device(&[no_present_mode]),
|
||||||
|
Err(VulkanCapabilityError::MissingPresentMode { .. })
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut no_color_attachment = device(
|
||||||
|
"No color attachment",
|
||||||
|
VulkanDeviceType::DiscreteGpu,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
no_color_attachment
|
||||||
|
.surface_capabilities
|
||||||
|
.supported_usage_flags = vk::ImageUsageFlags::TRANSFER_DST.as_raw();
|
||||||
|
assert!(matches!(
|
||||||
|
select_physical_device(&[no_color_attachment]),
|
||||||
|
Err(VulkanCapabilityError::MissingColorAttachmentUsage { .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn capability_gate_rejects_devices_without_requested_depth_stencil_support() {
|
||||||
|
let mut no_depth = device("No depth", VulkanDeviceType::DiscreteGpu, 0, true, false);
|
||||||
|
no_depth.supported_depth_stencil_formats = vec![vk::Format::D32_SFLOAT.as_raw()];
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
select_physical_device(&[no_depth]),
|
||||||
|
Err(VulkanCapabilityError::MissingDepthStencilFormat { .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn capability_gate_respects_request_specific_depth_profiles() {
|
||||||
|
let mut no_stencil = device("No stencil", VulkanDeviceType::DiscreteGpu, 0, true, false);
|
||||||
|
no_stencil.supported_depth_stencil_formats = vec![vk::Format::D32_SFLOAT.as_raw()];
|
||||||
|
let relaxed_request = RenderRequest {
|
||||||
|
depth: DepthStencilSupport {
|
||||||
|
depth_bits: 32,
|
||||||
|
stencil_bits: 0,
|
||||||
|
},
|
||||||
|
..RenderRequest::conservative()
|
||||||
|
};
|
||||||
|
|
||||||
|
let report = select_physical_device_for_request(&[no_stencil], relaxed_request)
|
||||||
|
.expect("selected device for depth-only request");
|
||||||
|
|
||||||
|
assert_eq!(report.device_name, "No stencil");
|
||||||
|
assert!(report.rejected_devices.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn capability_report_preserves_informational_sampled_formats_and_limits() {
|
||||||
|
let report = select_physical_device(&[device(
|
||||||
|
"Telemetry GPU",
|
||||||
|
VulkanDeviceType::DiscreteGpu,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
)])
|
||||||
|
.expect("selected device");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
report.informational_capabilities.sampled_color_formats,
|
||||||
|
vec![vk::Format::B8G8R8A8_SRGB.as_raw()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
report.informational_capabilities.sampled_depth_formats,
|
||||||
|
vec![vk::Format::D32_SFLOAT.as_raw()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
report.informational_capabilities.limits,
|
||||||
|
VulkanDeviceLimits {
|
||||||
|
max_image_dimension_2d: 4096,
|
||||||
|
max_sampler_allocation_count: 4096,
|
||||||
|
max_per_stage_descriptor_samplers: 16,
|
||||||
|
max_bound_descriptor_sets: 4,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn capability_report_json_is_stable() {
|
||||||
|
let mut rejected = device("Rejected", VulkanDeviceType::IntegratedGpu, 0, true, false);
|
||||||
|
rejected.present_modes.clear();
|
||||||
|
let report = select_physical_device(&[
|
||||||
|
rejected,
|
||||||
|
device("GPU \"A\"", VulkanDeviceType::DiscreteGpu, 3, true, false),
|
||||||
|
])
|
||||||
|
.expect("selected device");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
render_capability_report_json(&report),
|
||||||
|
"{\"schema\":1,\"vulkan_api\":\"1.1.0\",\"device_name\":\"GPU \\\"A\\\"\",\"score\":1101,\"graphics_queue_family\":3,\"present_queue_family\":3,\"portability_subset\":false,\"enabled_extensions\":[\"VK_KHR_swapchain\"],\"informational_capabilities\":{\"sampled_color_formats\":[50],\"sampled_depth_formats\":[126],\"limits\":{\"max_image_dimension_2d\":4096,\"max_sampler_allocation_count\":4096,\"max_per_stage_descriptor_samplers\":16,\"max_bound_descriptor_sets\":4}},\"rejected_devices\":[{\"device_name\":\"Rejected\",\"reason_code\":\"missing_present_mode\",\"reason\":\"Vulkan device Rejected has no supported present mode\"}]}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loader_probe_report_json_is_stable() {
|
||||||
|
assert_eq!(
|
||||||
|
vulkan_entry_symbol_name().to_bytes(),
|
||||||
|
b"vkGetInstanceProcAddr"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
render_loader_probe_report_json(&VulkanLoaderProbeReport {
|
||||||
|
schema: 1,
|
||||||
|
loader_available: true,
|
||||||
|
instance_api_version: vk::API_VERSION_1_2,
|
||||||
|
}),
|
||||||
|
"{\"schema\":1,\"loader_available\":true,\"instance_api\":\"1.2.0\"}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loader_error_display_is_actionable() {
|
||||||
|
assert_eq!(
|
||||||
|
VulkanLoaderError::Unavailable {
|
||||||
|
message: "dlopen failed".to_string(),
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
"Vulkan loader is unavailable: dlopen failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instance_plan_is_sorted_deduplicated_and_portability_aware() {
|
||||||
|
let plan = plan_vulkan_instance(&VulkanInstanceConfig {
|
||||||
|
application_name: "FParkan".to_string(),
|
||||||
|
required_extensions: vec![
|
||||||
|
"VK_KHR_surface".to_string(),
|
||||||
|
KHR_PORTABILITY_ENUMERATION_EXTENSION.to_string(),
|
||||||
|
"VK_KHR_surface".to_string(),
|
||||||
|
],
|
||||||
|
enable_portability_enumeration: true,
|
||||||
|
enable_validation: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
render_instance_plan_json(&plan),
|
||||||
|
"{\"schema\":1,\"create_flags\":1,\"validation_requested\":true,\"enabled_extensions\":[\"VK_EXT_debug_utils\",\"VK_KHR_portability_enumeration\",\"VK_KHR_surface\"]}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instance_plan_adds_portability_extension_when_requested() {
|
||||||
|
let plan = plan_vulkan_instance(&VulkanInstanceConfig {
|
||||||
|
application_name: "FParkan".to_string(),
|
||||||
|
required_extensions: vec!["VK_KHR_surface".to_string()],
|
||||||
|
enable_portability_enumeration: true,
|
||||||
|
enable_validation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
plan.enabled_extensions,
|
||||||
|
vec![
|
||||||
|
KHR_PORTABILITY_ENUMERATION_EXTENSION.to_string(),
|
||||||
|
"VK_KHR_surface".to_string()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(plan.create_flags, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_instance_extension_name_is_reported_before_loader_use() {
|
||||||
|
assert_eq!(
|
||||||
|
cstring_vec(&["bad\0extension".to_string()]),
|
||||||
|
Err(VulkanInstanceError::InvalidExtensionName {
|
||||||
|
extension: "bad\0extension".to_string()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_instance_extension_is_reported_before_create_instance() {
|
||||||
|
assert_eq!(
|
||||||
|
ensure_instance_extensions_available(
|
||||||
|
&[
|
||||||
|
"VK_EXT_debug_utils".to_string(),
|
||||||
|
"VK_KHR_surface".to_string(),
|
||||||
|
],
|
||||||
|
&["VK_KHR_surface".to_string()],
|
||||||
|
),
|
||||||
|
Err(VulkanInstanceError::MissingInstanceExtension {
|
||||||
|
extension: "VK_EXT_debug_utils".to_string(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn surface_plan_requires_native_handles() {
|
||||||
|
assert_eq!(
|
||||||
|
plan_vulkan_surface(None),
|
||||||
|
Err(VulkanSurfaceError::MissingNativeHandles)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
VulkanSurfaceError::MissingNativeHandles.to_string(),
|
||||||
|
"native window/display handles are required for Vulkan surface creation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn surface_plan_json_is_stable() {
|
||||||
|
assert_eq!(
|
||||||
|
render_surface_plan_json(&VulkanSurfacePlan {
|
||||||
|
schema: 1,
|
||||||
|
required_instance_extensions: vec![
|
||||||
|
"VK_KHR_surface".to_string(),
|
||||||
|
"VK_EXT_metal_surface".to_string(),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
"{\"schema\":1,\"required_instance_extensions\":[\"VK_KHR_surface\",\"VK_EXT_metal_surface\"]}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn static_surface_extension_name_is_decoded() {
|
||||||
|
let name = extension_name(ash::khr::surface::NAME.as_ptr()).expect("extension name");
|
||||||
|
|
||||||
|
assert_eq!(name, "VK_KHR_surface");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn swapchain_plan_prefers_srgb_mailbox_and_clamps_extent() {
|
||||||
|
let plan = plan_vulkan_swapchain(&swapchain_request()).expect("swapchain plan");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
plan.format,
|
||||||
|
VulkanSurfaceFormat {
|
||||||
|
format: vk::Format::B8G8R8A8_SRGB.as_raw(),
|
||||||
|
color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(plan.present_mode, vk::PresentModeKHR::MAILBOX.as_raw());
|
||||||
|
assert_eq!(plan.extent, (1024, 720));
|
||||||
|
assert_eq!(plan.image_count, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn swapchain_plan_uses_fifo_and_current_extent_fallbacks() {
|
||||||
|
let mut request = swapchain_request();
|
||||||
|
request.preferred_present_mode = vk::PresentModeKHR::IMMEDIATE.as_raw();
|
||||||
|
request.present_modes = vec![vk::PresentModeKHR::FIFO.as_raw()];
|
||||||
|
request.capabilities.current_extent = Some((800, 600));
|
||||||
|
|
||||||
|
let plan = plan_vulkan_swapchain(&request).expect("swapchain plan");
|
||||||
|
|
||||||
|
assert_eq!(plan.present_mode, vk::PresentModeKHR::FIFO.as_raw());
|
||||||
|
assert_eq!(plan.extent, (800, 600));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn swapchain_plan_accepts_undefined_surface_format_by_picking_stage0_default() {
|
||||||
|
let mut request = swapchain_request();
|
||||||
|
request.formats = vec![VulkanSurfaceFormat {
|
||||||
|
format: vk::Format::UNDEFINED.as_raw(),
|
||||||
|
color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
|
||||||
|
}];
|
||||||
|
|
||||||
|
let plan = plan_vulkan_swapchain(&request).expect("swapchain plan");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
plan.format,
|
||||||
|
VulkanSurfaceFormat {
|
||||||
|
format: vk::Format::B8G8R8A8_SRGB.as_raw(),
|
||||||
|
color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn swapchain_plan_rejects_missing_surface_data_and_empty_extent() {
|
||||||
|
let mut request = swapchain_request();
|
||||||
|
request.formats.clear();
|
||||||
|
assert_eq!(
|
||||||
|
plan_vulkan_swapchain(&request),
|
||||||
|
Err(VulkanSwapchainError::MissingSurfaceFormat)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut request = swapchain_request();
|
||||||
|
request.present_modes.clear();
|
||||||
|
assert_eq!(
|
||||||
|
plan_vulkan_swapchain(&request),
|
||||||
|
Err(VulkanSwapchainError::MissingPresentMode)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut request = swapchain_request();
|
||||||
|
request.capabilities.current_extent = Some((0, 600));
|
||||||
|
assert_eq!(
|
||||||
|
plan_vulkan_swapchain(&request),
|
||||||
|
Err(VulkanSwapchainError::EmptyExtent)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn swapchain_plan_json_and_recreation_reports_are_stable() {
|
||||||
|
let plan = plan_vulkan_swapchain(&swapchain_request()).expect("swapchain plan");
|
||||||
|
assert_eq!(
|
||||||
|
render_swapchain_plan_json(&plan),
|
||||||
|
"{\"schema\":1,\"extent\":[1024,720],\"format\":50,\"color_space\":0,\"present_mode\":1,\"image_count\":3}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let report = swapchain_recreation_report(
|
||||||
|
VulkanSwapchainRecreationReason::OutOfDate,
|
||||||
|
(1024, 720),
|
||||||
|
(1280, 720),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
render_swapchain_recreation_report_json(&report),
|
||||||
|
"{\"schema\":1,\"reason\":\"out_of_date\",\"previous_extent\":[1024,720],\"next_extent\":[1280,720]}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn triangle_shader_manifest_hashes_are_stable() {
|
||||||
|
let report = validate_shader_manifest(&triangle_shader_manifest()).expect("shader manifest");
|
||||||
|
|
||||||
|
assert_eq!(report.schema, SHADER_MANIFEST_SCHEMA);
|
||||||
|
assert_eq!(report.target_env, SHADER_TARGET_ENV);
|
||||||
|
assert_eq!(
|
||||||
|
report.compiler,
|
||||||
|
VulkanShaderToolManifest {
|
||||||
|
name: SHADER_COMPILER_NAME,
|
||||||
|
version: SHADER_COMPILER_VERSION,
|
||||||
|
binary_sha256: SHADER_COMPILER_BINARY_SHA256,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
report.validator,
|
||||||
|
VulkanShaderToolManifest {
|
||||||
|
name: SPIRV_VALIDATOR_NAME,
|
||||||
|
version: SPIRV_VALIDATOR_VERSION,
|
||||||
|
binary_sha256: SPIRV_VALIDATOR_BINARY_SHA256,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(report.modules.len(), 2);
|
||||||
|
assert_eq!(report.modules[0].name, "triangle.vert");
|
||||||
|
assert_eq!(report.modules[0].stage, VulkanShaderStage::Vertex);
|
||||||
|
assert_eq!(report.modules[0].source_path, TRIANGLE_VERTEX_SOURCE_PATH);
|
||||||
|
assert_eq!(
|
||||||
|
report.modules[0].source_sha256,
|
||||||
|
TRIANGLE_VERTEX_SOURCE_SHA256
|
||||||
|
);
|
||||||
|
assert_eq!(report.modules[0].spirv_path, TRIANGLE_VERTEX_SPIRV_PATH);
|
||||||
|
assert_eq!(report.modules[0].word_count, 253);
|
||||||
|
assert_eq!(
|
||||||
|
report.modules[0].sha256,
|
||||||
|
"4d3ceca7b42ebc971d831b0a0d816457397bd9aeda47fb8d44c4b1aeaa5e7ba0"
|
||||||
|
);
|
||||||
|
assert_eq!(report.modules[0].descriptor_sets, 0);
|
||||||
|
assert_eq!(report.modules[0].push_constant_bytes, 0);
|
||||||
|
assert_eq!(
|
||||||
|
report.modules[0].compile_command,
|
||||||
|
TRIANGLE_VERTEX_COMPILE_COMMAND
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
report.modules[0].validate_command,
|
||||||
|
TRIANGLE_VERTEX_VALIDATE_COMMAND
|
||||||
|
);
|
||||||
|
assert!(!report.modules[0].interface_hash.is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
report.modules[1].sha256,
|
||||||
|
"5a7441be03cd3c25d557268b2e58d5aa50504c87bffcb4c3fd7cbcf007db0b96"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
report.manifest_hash,
|
||||||
|
"11e3feb65200ebd2ac87b7e776e9c6433a5da9d71a651bfadea89a51be17ff05"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shader_manifest_report_json_is_stable() {
|
||||||
|
let report = validate_shader_manifest(&triangle_shader_manifest()).expect("shader manifest");
|
||||||
|
let json = render_shader_manifest_report_json(&report);
|
||||||
|
|
||||||
|
assert!(json.contains(SHADER_COMPILER_NAME));
|
||||||
|
assert!(json.contains(SPIRV_VALIDATOR_NAME));
|
||||||
|
assert!(json.contains(TRIANGLE_VERTEX_SOURCE_PATH));
|
||||||
|
assert!(json.contains(TRIANGLE_VERTEX_COMPILE_COMMAND));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn checked_in_shader_manifest_matches_generated_report() {
|
||||||
|
let report = validate_shader_manifest(&triangle_shader_manifest()).expect("shader manifest");
|
||||||
|
assert_eq!(
|
||||||
|
render_shader_manifest_report_json(&report),
|
||||||
|
include_str!("../../shaders/manifest.json").trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shader_manifest_rejects_invalid_spirv_containers() {
|
||||||
|
let mut module = triangle_shader_manifest().remove(0);
|
||||||
|
module.words = &[0xFFFF_FFFF, SPIRV_VERSION_1_0, 0, 1, 0];
|
||||||
|
assert_eq!(
|
||||||
|
validate_shader_manifest(&[module]),
|
||||||
|
Err(VulkanShaderManifestError::InvalidMagic {
|
||||||
|
name: "triangle.vert",
|
||||||
|
found: 0xFFFF_FFFF,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut module = triangle_shader_manifest().remove(0);
|
||||||
|
module.words = &[SPIRV_MAGIC, 0, 0, 1, 0];
|
||||||
|
assert_eq!(
|
||||||
|
validate_shader_manifest(&[module]),
|
||||||
|
Err(VulkanShaderManifestError::UnsupportedVersion {
|
||||||
|
name: "triangle.vert",
|
||||||
|
found: 0,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut module = triangle_shader_manifest().remove(0);
|
||||||
|
module.words = &[SPIRV_MAGIC, SPIRV_VERSION_1_0, 0, 0, 0];
|
||||||
|
assert_eq!(
|
||||||
|
validate_shader_manifest(&[module]),
|
||||||
|
Err(VulkanShaderManifestError::InvalidBound {
|
||||||
|
name: "triangle.vert",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn device(
|
||||||
|
name: &str,
|
||||||
|
device_type: VulkanDeviceType,
|
||||||
|
queue_index: u32,
|
||||||
|
swapchain: bool,
|
||||||
|
portability_subset: bool,
|
||||||
|
) -> VulkanPhysicalDeviceRecord {
|
||||||
|
let mut extensions = Vec::new();
|
||||||
|
if swapchain {
|
||||||
|
extensions.push(KHR_SWAPCHAIN_EXTENSION.to_string());
|
||||||
|
}
|
||||||
|
if portability_subset {
|
||||||
|
extensions.push(KHR_PORTABILITY_SUBSET_EXTENSION.to_string());
|
||||||
|
}
|
||||||
|
VulkanPhysicalDeviceRecord {
|
||||||
|
name: name.to_string(),
|
||||||
|
api_version: MIN_VULKAN_API_VERSION,
|
||||||
|
device_type,
|
||||||
|
extensions,
|
||||||
|
queue_families: vec![VulkanQueueFamily {
|
||||||
|
index: queue_index,
|
||||||
|
graphics: true,
|
||||||
|
present: true,
|
||||||
|
}],
|
||||||
|
surface_formats: vec![VulkanSurfaceFormat {
|
||||||
|
format: vk::Format::B8G8R8A8_SRGB.as_raw(),
|
||||||
|
color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
|
||||||
|
}],
|
||||||
|
present_modes: vec![
|
||||||
|
vk::PresentModeKHR::FIFO.as_raw(),
|
||||||
|
vk::PresentModeKHR::MAILBOX.as_raw(),
|
||||||
|
],
|
||||||
|
surface_capabilities: default_surface_capabilities(),
|
||||||
|
supported_depth_stencil_formats: vec![
|
||||||
|
vk::Format::D24_UNORM_S8_UINT.as_raw(),
|
||||||
|
vk::Format::D32_SFLOAT_S8_UINT.as_raw(),
|
||||||
|
vk::Format::D32_SFLOAT.as_raw(),
|
||||||
|
],
|
||||||
|
sampled_image_formats: vec![
|
||||||
|
vk::Format::B8G8R8A8_SRGB.as_raw(),
|
||||||
|
vk::Format::D32_SFLOAT.as_raw(),
|
||||||
|
],
|
||||||
|
limits: VulkanDeviceLimits {
|
||||||
|
max_image_dimension_2d: 4096,
|
||||||
|
max_sampler_allocation_count: 4096,
|
||||||
|
max_per_stage_descriptor_samplers: 16,
|
||||||
|
max_bound_descriptor_sets: 4,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn swapchain_request() -> VulkanSwapchainRequest {
|
||||||
|
VulkanSwapchainRequest {
|
||||||
|
drawable_extent: (1280, 720),
|
||||||
|
formats: vec![
|
||||||
|
VulkanSurfaceFormat {
|
||||||
|
format: vk::Format::R8G8B8A8_UNORM.as_raw(),
|
||||||
|
color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
|
||||||
|
},
|
||||||
|
VulkanSurfaceFormat {
|
||||||
|
format: vk::Format::B8G8R8A8_SRGB.as_raw(),
|
||||||
|
color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
present_modes: vec![
|
||||||
|
vk::PresentModeKHR::FIFO.as_raw(),
|
||||||
|
vk::PresentModeKHR::MAILBOX.as_raw(),
|
||||||
|
],
|
||||||
|
capabilities: default_surface_capabilities(),
|
||||||
|
preferred_present_mode: vk::PresentModeKHR::MAILBOX.as_raw(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_surface_capabilities() -> VulkanSwapchainSurfaceCapabilities {
|
||||||
|
VulkanSwapchainSurfaceCapabilities {
|
||||||
|
current_extent: None,
|
||||||
|
min_extent: (320, 240),
|
||||||
|
max_extent: (1024, 768),
|
||||||
|
min_image_count: 2,
|
||||||
|
max_image_count: 3,
|
||||||
|
supported_usage_flags: vk::ImageUsageFlags::COLOR_ATTACHMENT.as_raw(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#![allow(unsafe_code)]
|
||||||
|
|
||||||
|
use ash::vk;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::ffi::CStr;
|
||||||
|
use std::sync::{
|
||||||
|
atomic::{AtomicU32, Ordering},
|
||||||
|
Mutex,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{VulkanInstanceProbe, VulkanSmokeRendererError, VulkanValidationReport};
|
||||||
|
|
||||||
|
struct VulkanValidationShared {
|
||||||
|
warning_count: AtomicU32,
|
||||||
|
error_count: AtomicU32,
|
||||||
|
vuids: Mutex<BTreeSet<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for VulkanValidationShared {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
warning_count: AtomicU32::new(0),
|
||||||
|
error_count: AtomicU32::new(0),
|
||||||
|
vuids: Mutex::new(BTreeSet::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct VulkanValidationMessenger {
|
||||||
|
loader: ash::ext::debug_utils::Instance,
|
||||||
|
messenger: vk::DebugUtilsMessengerEXT,
|
||||||
|
shared: Box<VulkanValidationShared>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VulkanValidationMessenger {
|
||||||
|
pub(super) fn report(&self) -> VulkanValidationReport {
|
||||||
|
let vuids = self
|
||||||
|
.shared
|
||||||
|
.vuids
|
||||||
|
.lock()
|
||||||
|
.map(|values| values.iter().cloned().collect::<Vec<_>>())
|
||||||
|
.unwrap_or_default();
|
||||||
|
VulkanValidationReport {
|
||||||
|
warning_count: self.shared.warning_count.load(Ordering::Relaxed),
|
||||||
|
error_count: self.shared.error_count.load(Ordering::Relaxed),
|
||||||
|
vuids,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for VulkanValidationMessenger {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// SAFETY: The messenger belongs to this instance-level loader and is destroyed once.
|
||||||
|
unsafe {
|
||||||
|
self.loader
|
||||||
|
.destroy_debug_utils_messenger(self.messenger, None);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "system" fn vulkan_validation_callback(
|
||||||
|
message_severity: vk::DebugUtilsMessageSeverityFlagsEXT,
|
||||||
|
_message_types: vk::DebugUtilsMessageTypeFlagsEXT,
|
||||||
|
callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>,
|
||||||
|
user_data: *mut std::ffi::c_void,
|
||||||
|
) -> vk::Bool32 {
|
||||||
|
// SAFETY: The debug messenger stores a stable pointer to `VulkanValidationShared` for the messenger lifetime.
|
||||||
|
let Some(shared) = (unsafe { (user_data as *const VulkanValidationShared).as_ref() }) else {
|
||||||
|
return vk::FALSE;
|
||||||
|
};
|
||||||
|
if message_severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::ERROR) {
|
||||||
|
shared.error_count.fetch_add(1, Ordering::Relaxed);
|
||||||
|
} else if message_severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::WARNING) {
|
||||||
|
shared.warning_count.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
// SAFETY: Vulkan invokes the callback with either a null pointer or a valid callback-data payload.
|
||||||
|
let Some(callback_data) = (unsafe { callback_data.as_ref() }) else {
|
||||||
|
return vk::FALSE;
|
||||||
|
};
|
||||||
|
if let Some(vuid) = (!callback_data.p_message_id_name.is_null()).then(|| {
|
||||||
|
// SAFETY: `p_message_id_name` is a Vulkan-owned NUL-terminated string for the callback duration.
|
||||||
|
unsafe { CStr::from_ptr(callback_data.p_message_id_name) }
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
}) {
|
||||||
|
if vuid.starts_with("VUID-") {
|
||||||
|
if let Ok(mut vuids) = shared.vuids.lock() {
|
||||||
|
vuids.insert(vuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vk::FALSE
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn create_validation_messenger(
|
||||||
|
instance: &VulkanInstanceProbe,
|
||||||
|
) -> Result<VulkanValidationMessenger, VulkanSmokeRendererError> {
|
||||||
|
let shared = Box::new(VulkanValidationShared::default());
|
||||||
|
let loader = ash::ext::debug_utils::Instance::new(&instance.entry, &instance.instance);
|
||||||
|
let create_info = vk::DebugUtilsMessengerCreateInfoEXT::default()
|
||||||
|
.message_severity(
|
||||||
|
vk::DebugUtilsMessageSeverityFlagsEXT::WARNING
|
||||||
|
| vk::DebugUtilsMessageSeverityFlagsEXT::ERROR,
|
||||||
|
)
|
||||||
|
.message_type(
|
||||||
|
vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
|
||||||
|
| vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION
|
||||||
|
| vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE,
|
||||||
|
)
|
||||||
|
.pfn_user_callback(Some(vulkan_validation_callback))
|
||||||
|
.user_data((&raw const *shared).cast_mut().cast());
|
||||||
|
let messenger =
|
||||||
|
// SAFETY: The create info points at a stable boxed user-data allocation for the messenger lifetime.
|
||||||
|
unsafe { loader.create_debug_utils_messenger(&create_info, None) }.map_err(|error| {
|
||||||
|
VulkanSmokeRendererError::VulkanOperation {
|
||||||
|
context: "vkCreateDebugUtilsMessengerEXT",
|
||||||
|
result: error,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
Ok(VulkanValidationMessenger {
|
||||||
|
loader,
|
||||||
|
messenger,
|
||||||
|
shared,
|
||||||
|
})
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,172 @@
|
|||||||
|
use ash::vk;
|
||||||
|
use fparkan_platform::RenderRequest;
|
||||||
|
use fparkan_render::{
|
||||||
|
canonical_capture, FrameOutput, RenderBackend, RenderCommandList, RenderError,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
plan_vulkan_frame_submission, VulkanFrameSubmissionPlan, VulkanSurfaceFormat,
|
||||||
|
VulkanSwapchainPlan,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Vulkan backend migration readiness.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum VulkanPlanningBackendState {
|
||||||
|
/// Planning facade is configured and able to accept command lists.
|
||||||
|
Configured,
|
||||||
|
/// Adapter is tracking a recoverable runtime surface/depth pipeline fault.
|
||||||
|
Degraded,
|
||||||
|
/// Adapter has encountered a non-recoverable error.
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for VulkanPlanningBackendState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Configured
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diagnostics for planning-facade request tracking.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct VulkanPlanningRequestReport {
|
||||||
|
/// Last render request observed by the planning facade.
|
||||||
|
pub current_request: RenderRequest,
|
||||||
|
/// Number of meaningful request updates applied to the facade.
|
||||||
|
pub request_updates: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for VulkanPlanningRequestReport {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
current_request: RenderRequest::conservative(),
|
||||||
|
request_updates: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diagnostics for planning-facade execution telemetry.
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
|
pub struct VulkanPlanningExecutionReport {
|
||||||
|
/// Total frames planned by the facade.
|
||||||
|
pub planned_frames: u64,
|
||||||
|
/// Total frame-submission plans emitted by the facade.
|
||||||
|
pub submission_plans: u64,
|
||||||
|
/// Last command-capture byte size.
|
||||||
|
pub last_capture_size: usize,
|
||||||
|
/// Number of simulated present calls issued by the planning facade.
|
||||||
|
pub simulated_presents: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diagnostics for Vulkan planning backend setup and frame progression.
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
|
pub struct VulkanPlanningBackendReport {
|
||||||
|
/// Request-tracking telemetry.
|
||||||
|
pub request: VulkanPlanningRequestReport,
|
||||||
|
/// Execution-planning telemetry.
|
||||||
|
pub execution: VulkanPlanningExecutionReport,
|
||||||
|
/// Last deterministic frame submission plan.
|
||||||
|
pub last_frame_submission: Option<VulkanFrameSubmissionPlan>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vulkan planning backend facade used by the game entrypoint.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct VulkanPlanningBackend {
|
||||||
|
state: VulkanPlanningBackendState,
|
||||||
|
report: VulkanPlanningBackendReport,
|
||||||
|
swapchain_plan: VulkanSwapchainPlan,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for VulkanPlanningBackend {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VulkanPlanningBackend {
|
||||||
|
/// Creates a new Vulkan planning backend facade.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
state: VulkanPlanningBackendState::Configured,
|
||||||
|
report: VulkanPlanningBackendReport::default(),
|
||||||
|
swapchain_plan: default_stage0_swapchain_plan(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces active surface/profile request.
|
||||||
|
pub fn set_render_request(&mut self, request: RenderRequest) {
|
||||||
|
if self.report.request.current_request != request {
|
||||||
|
self.report.request.current_request = request;
|
||||||
|
self.report.request.request_updates =
|
||||||
|
self.report.request.request_updates.saturating_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns active render request policy.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn render_request(&self) -> RenderRequest {
|
||||||
|
self.report.request.current_request
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces active swapchain plan used for frame submission planning.
|
||||||
|
pub fn set_swapchain_plan(&mut self, plan: VulkanSwapchainPlan) {
|
||||||
|
self.swapchain_plan = plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns active swapchain plan.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn swapchain_plan(&self) -> &VulkanSwapchainPlan {
|
||||||
|
&self.swapchain_plan
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns adapter state.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn state(&self) -> VulkanPlanningBackendState {
|
||||||
|
self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns backend report.
|
||||||
|
#[must_use]
|
||||||
|
pub fn report(&self) -> &VulkanPlanningBackendReport {
|
||||||
|
&self.report
|
||||||
|
}
|
||||||
|
|
||||||
|
fn simulate_present(&mut self) {
|
||||||
|
self.report.execution.simulated_presents =
|
||||||
|
self.report.execution.simulated_presents.saturating_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderBackend for VulkanPlanningBackend {
|
||||||
|
fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError> {
|
||||||
|
if !matches!(
|
||||||
|
self.state,
|
||||||
|
VulkanPlanningBackendState::Configured | VulkanPlanningBackendState::Degraded
|
||||||
|
) {
|
||||||
|
return Err(RenderError::InvalidRange);
|
||||||
|
}
|
||||||
|
let capture = canonical_capture(commands)?;
|
||||||
|
let frame_plan = plan_vulkan_frame_submission(&self.swapchain_plan, commands)?;
|
||||||
|
self.report.execution.planned_frames =
|
||||||
|
self.report.execution.planned_frames.saturating_add(1);
|
||||||
|
self.report.execution.submission_plans =
|
||||||
|
self.report.execution.submission_plans.saturating_add(1);
|
||||||
|
self.report.execution.last_capture_size = capture.len();
|
||||||
|
self.report.last_frame_submission = Some(frame_plan);
|
||||||
|
self.simulate_present();
|
||||||
|
Ok(FrameOutput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_stage0_swapchain_plan() -> VulkanSwapchainPlan {
|
||||||
|
VulkanSwapchainPlan {
|
||||||
|
schema: 1,
|
||||||
|
extent: (1, 1),
|
||||||
|
format: VulkanSurfaceFormat {
|
||||||
|
format: vk::Format::B8G8R8A8_SRGB.as_raw(),
|
||||||
|
color_space: vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw(),
|
||||||
|
},
|
||||||
|
present_mode: vk::PresentModeKHR::FIFO.as_raw(),
|
||||||
|
image_count: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,887 @@
|
|||||||
|
use ash::vk;
|
||||||
|
use fparkan_platform::{DepthStencilSupport, RenderRequest};
|
||||||
|
use fparkan_render::{validate_command_list, RenderCommand, RenderCommandList, RenderError};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
const MIN_VULKAN_API_VERSION: u32 = vk::API_VERSION_1_1;
|
||||||
|
pub(crate) const KHR_SWAPCHAIN_EXTENSION: &str = "VK_KHR_swapchain";
|
||||||
|
pub(crate) const KHR_PORTABILITY_SUBSET_EXTENSION: &str = "VK_KHR_portability_subset";
|
||||||
|
|
||||||
|
/// Synthetic physical-device type used by deterministic capability scoring.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum VulkanDeviceType {
|
||||||
|
/// Discrete GPU.
|
||||||
|
DiscreteGpu,
|
||||||
|
/// Integrated GPU.
|
||||||
|
IntegratedGpu,
|
||||||
|
/// CPU or software Vulkan implementation.
|
||||||
|
Cpu,
|
||||||
|
/// Other or unknown implementation.
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VulkanDeviceType {
|
||||||
|
const fn score_bonus(self) -> i32 {
|
||||||
|
match self {
|
||||||
|
Self::DiscreteGpu => 1_000,
|
||||||
|
Self::IntegratedGpu => 700,
|
||||||
|
Self::Cpu => 100,
|
||||||
|
Self::Other => 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queue-family capabilities needed by the Stage 0 renderer.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanQueueFamily {
|
||||||
|
/// Stable queue-family index.
|
||||||
|
pub index: u32,
|
||||||
|
/// Whether the family supports graphics commands.
|
||||||
|
pub graphics: bool,
|
||||||
|
/// Whether the family supports presentation for the target surface.
|
||||||
|
pub present: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Surface format capability needed by the Stage 0 swapchain policy.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanSurfaceFormat {
|
||||||
|
/// Vulkan format numeric value.
|
||||||
|
pub format: i32,
|
||||||
|
/// Vulkan color-space numeric value.
|
||||||
|
pub color_space: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Surface capabilities needed by the Stage 0 swapchain policy.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanSwapchainSurfaceCapabilities {
|
||||||
|
/// Current surface extent, when dictated by the platform.
|
||||||
|
pub current_extent: Option<(u32, u32)>,
|
||||||
|
/// Minimum supported swapchain extent.
|
||||||
|
pub min_extent: (u32, u32),
|
||||||
|
/// Maximum supported swapchain extent.
|
||||||
|
pub max_extent: (u32, u32),
|
||||||
|
/// Minimum supported image count.
|
||||||
|
pub min_image_count: u32,
|
||||||
|
/// Maximum supported image count, or 0 when unbounded.
|
||||||
|
pub max_image_count: u32,
|
||||||
|
/// Supported swapchain image-usage flags as raw Vulkan bits.
|
||||||
|
pub supported_usage_flags: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deterministic swapchain planning input.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanSwapchainRequest {
|
||||||
|
/// Requested drawable extent.
|
||||||
|
pub drawable_extent: (u32, u32),
|
||||||
|
/// Available surface formats.
|
||||||
|
pub formats: Vec<VulkanSurfaceFormat>,
|
||||||
|
/// Available present modes as raw Vulkan values.
|
||||||
|
pub present_modes: Vec<i32>,
|
||||||
|
/// Surface capabilities.
|
||||||
|
pub capabilities: VulkanSwapchainSurfaceCapabilities,
|
||||||
|
/// Preferred present mode.
|
||||||
|
pub preferred_present_mode: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deterministic swapchain plan.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanSwapchainPlan {
|
||||||
|
/// Report schema version.
|
||||||
|
pub schema: u32,
|
||||||
|
/// Selected swapchain extent.
|
||||||
|
pub extent: (u32, u32),
|
||||||
|
/// Selected surface format.
|
||||||
|
pub format: VulkanSurfaceFormat,
|
||||||
|
/// Selected present mode raw Vulkan value.
|
||||||
|
pub present_mode: i32,
|
||||||
|
/// Selected image count.
|
||||||
|
pub image_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Swapchain planning error.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum VulkanSwapchainError {
|
||||||
|
/// No surface format was available.
|
||||||
|
MissingSurfaceFormat,
|
||||||
|
/// No present mode was available.
|
||||||
|
MissingPresentMode,
|
||||||
|
/// Requested or current extent is empty.
|
||||||
|
EmptyExtent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for VulkanSwapchainError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::MissingSurfaceFormat => write!(f, "Vulkan swapchain has no surface format"),
|
||||||
|
Self::MissingPresentMode => write!(f, "Vulkan swapchain has no present mode"),
|
||||||
|
Self::EmptyExtent => write!(f, "Vulkan swapchain extent must be non-zero"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for VulkanSwapchainError {}
|
||||||
|
|
||||||
|
/// Swapchain recreation reason.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum VulkanSwapchainRecreationReason {
|
||||||
|
/// Drawable extent changed.
|
||||||
|
Resize,
|
||||||
|
/// Vulkan reported `VK_ERROR_OUT_OF_DATE_KHR`.
|
||||||
|
OutOfDate,
|
||||||
|
/// Vulkan reported `VK_SUBOPTIMAL_KHR`.
|
||||||
|
Suboptimal,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deterministic swapchain recreation report.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanSwapchainRecreationReport {
|
||||||
|
/// Report schema version.
|
||||||
|
pub schema: u32,
|
||||||
|
/// Recreation reason.
|
||||||
|
pub reason: VulkanSwapchainRecreationReason,
|
||||||
|
/// Previous extent.
|
||||||
|
pub previous_extent: (u32, u32),
|
||||||
|
/// Next extent.
|
||||||
|
pub next_extent: (u32, u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deterministic frame submission plan for command buffers and sync objects.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct VulkanFrameSubmissionPlan {
|
||||||
|
/// Report schema version.
|
||||||
|
pub schema: u32,
|
||||||
|
/// Frames allowed in flight.
|
||||||
|
pub frames_in_flight: u32,
|
||||||
|
/// Swapchain-backed primary command buffers.
|
||||||
|
pub command_buffers: u32,
|
||||||
|
/// Binary semaphores allocated per frame.
|
||||||
|
pub semaphores_per_frame: u32,
|
||||||
|
/// Fences allocated per frame.
|
||||||
|
pub fences_per_frame: u32,
|
||||||
|
/// Draw commands encoded into the frame.
|
||||||
|
pub draw_count: u32,
|
||||||
|
/// Total indexed vertices submitted by draw commands.
|
||||||
|
pub indexed_vertex_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synthetic physical-device capabilities used by negative tests and reports.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanPhysicalDeviceRecord {
|
||||||
|
/// Human-readable device name.
|
||||||
|
pub name: String,
|
||||||
|
/// Reported Vulkan API version.
|
||||||
|
pub api_version: u32,
|
||||||
|
/// Device class.
|
||||||
|
pub device_type: VulkanDeviceType,
|
||||||
|
/// Supported device-extension names.
|
||||||
|
pub extensions: Vec<String>,
|
||||||
|
/// Queue-family capabilities.
|
||||||
|
pub queue_families: Vec<VulkanQueueFamily>,
|
||||||
|
/// Surface formats accepted by the target surface.
|
||||||
|
pub surface_formats: Vec<VulkanSurfaceFormat>,
|
||||||
|
/// Present modes accepted by the target surface.
|
||||||
|
pub present_modes: Vec<i32>,
|
||||||
|
/// Surface capabilities accepted by the target surface.
|
||||||
|
pub surface_capabilities: VulkanSwapchainSurfaceCapabilities,
|
||||||
|
/// Depth/stencil attachment formats supported by the device.
|
||||||
|
pub supported_depth_stencil_formats: Vec<i32>,
|
||||||
|
/// Formats that can be used as sampled images.
|
||||||
|
pub sampled_image_formats: Vec<i32>,
|
||||||
|
/// Informational device limits relevant to the future Stage 0 baseline.
|
||||||
|
pub limits: VulkanDeviceLimits,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VulkanPhysicalDeviceRecord {
|
||||||
|
/// Returns whether the device supports an extension name.
|
||||||
|
#[must_use]
|
||||||
|
pub fn supports_extension(&self, extension: &str) -> bool {
|
||||||
|
self.extensions
|
||||||
|
.iter()
|
||||||
|
.any(|candidate| candidate == extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Informational device limits relevant to future Stage 0 capability growth.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct VulkanDeviceLimits {
|
||||||
|
/// Maximum 2D image dimension supported by the device.
|
||||||
|
pub max_image_dimension_2d: u32,
|
||||||
|
/// Maximum number of live samplers supported by the device.
|
||||||
|
pub max_sampler_allocation_count: u32,
|
||||||
|
/// Maximum number of sampler descriptors per stage.
|
||||||
|
pub max_per_stage_descriptor_samplers: u32,
|
||||||
|
/// Maximum number of bound descriptor sets.
|
||||||
|
pub max_bound_descriptor_sets: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Informational capabilities preserved in deterministic Stage 0 reports.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct VulkanInformationalCapabilities {
|
||||||
|
/// Color formats that support sampled-image usage.
|
||||||
|
pub sampled_color_formats: Vec<i32>,
|
||||||
|
/// Depth/stencil formats that support sampled-image usage.
|
||||||
|
pub sampled_depth_formats: Vec<i32>,
|
||||||
|
/// Future-baseline device limits.
|
||||||
|
pub limits: VulkanDeviceLimits,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selected device and queue capability report.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanCapabilityReport {
|
||||||
|
/// Report schema version.
|
||||||
|
pub schema: u32,
|
||||||
|
/// Selected device name.
|
||||||
|
pub device_name: String,
|
||||||
|
/// Selected Vulkan API version.
|
||||||
|
pub vulkan_api_version: u32,
|
||||||
|
/// Deterministic score used for device selection.
|
||||||
|
pub score: i32,
|
||||||
|
/// Graphics queue family index.
|
||||||
|
pub graphics_queue_family: u32,
|
||||||
|
/// Present queue family index.
|
||||||
|
pub present_queue_family: u32,
|
||||||
|
/// Whether portability subset is enabled for the selected device.
|
||||||
|
pub portability_subset: bool,
|
||||||
|
/// Enabled device extensions.
|
||||||
|
pub enabled_extensions: Vec<String>,
|
||||||
|
/// Informational capabilities retained for future baseline planning.
|
||||||
|
pub informational_capabilities: VulkanInformationalCapabilities,
|
||||||
|
/// Devices rejected by deterministic Stage 0 capability validation.
|
||||||
|
pub rejected_devices: Vec<VulkanRejectedDeviceReport>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deterministic rejection reason for an unsuitable physical device.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct VulkanRejectedDeviceReport {
|
||||||
|
/// Human-readable device name.
|
||||||
|
pub device_name: String,
|
||||||
|
/// Stable machine-readable rejection code.
|
||||||
|
pub reason_code: &'static str,
|
||||||
|
/// Actionable rejection summary.
|
||||||
|
pub reason: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vulkan capability selection error.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum VulkanCapabilityError {
|
||||||
|
/// No physical devices were available.
|
||||||
|
NoPhysicalDevice,
|
||||||
|
/// Device API version is lower than the Stage 0 minimum.
|
||||||
|
ApiVersionTooLow {
|
||||||
|
/// Required Vulkan API version.
|
||||||
|
required: u32,
|
||||||
|
/// Reported Vulkan API version.
|
||||||
|
found: u32,
|
||||||
|
},
|
||||||
|
/// Required graphics queue is unavailable.
|
||||||
|
NoGraphicsQueue {
|
||||||
|
/// Device name that failed validation.
|
||||||
|
device: String,
|
||||||
|
},
|
||||||
|
/// Required present queue is unavailable.
|
||||||
|
NoPresentQueue {
|
||||||
|
/// Device name that failed validation.
|
||||||
|
device: String,
|
||||||
|
},
|
||||||
|
/// Swapchain device extension is unavailable.
|
||||||
|
MissingSwapchainExtension {
|
||||||
|
/// Device name that failed validation.
|
||||||
|
device: String,
|
||||||
|
},
|
||||||
|
/// No compatible surface format exists.
|
||||||
|
MissingSurfaceFormat {
|
||||||
|
/// Device name that failed validation.
|
||||||
|
device: String,
|
||||||
|
},
|
||||||
|
/// No present mode is available for the target surface.
|
||||||
|
MissingPresentMode {
|
||||||
|
/// Device name that failed validation.
|
||||||
|
device: String,
|
||||||
|
},
|
||||||
|
/// Swapchain images cannot be used as color attachments.
|
||||||
|
MissingColorAttachmentUsage {
|
||||||
|
/// Device name that failed validation.
|
||||||
|
device: String,
|
||||||
|
},
|
||||||
|
/// No compatible depth/stencil attachment format exists for the render request.
|
||||||
|
MissingDepthStencilFormat {
|
||||||
|
/// Device name that failed validation.
|
||||||
|
device: String,
|
||||||
|
/// Requested depth/stencil profile.
|
||||||
|
requested: DepthStencilSupport,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for VulkanCapabilityError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::NoPhysicalDevice => write!(f, "no Vulkan physical device available"),
|
||||||
|
Self::ApiVersionTooLow { required, found } => write!(
|
||||||
|
f,
|
||||||
|
"Vulkan API version too low: required {}, found {}",
|
||||||
|
format_api_version(*required),
|
||||||
|
format_api_version(*found)
|
||||||
|
),
|
||||||
|
Self::NoGraphicsQueue { device } => {
|
||||||
|
write!(f, "Vulkan device {device} has no graphics queue")
|
||||||
|
}
|
||||||
|
Self::NoPresentQueue { device } => {
|
||||||
|
write!(f, "Vulkan device {device} has no present queue")
|
||||||
|
}
|
||||||
|
Self::MissingSwapchainExtension { device } => {
|
||||||
|
write!(f, "Vulkan device {device} lacks {KHR_SWAPCHAIN_EXTENSION}")
|
||||||
|
}
|
||||||
|
Self::MissingSurfaceFormat { device } => {
|
||||||
|
write!(f, "Vulkan device {device} has no compatible surface format")
|
||||||
|
}
|
||||||
|
Self::MissingPresentMode { device } => {
|
||||||
|
write!(f, "Vulkan device {device} has no supported present mode")
|
||||||
|
}
|
||||||
|
Self::MissingColorAttachmentUsage { device } => write!(
|
||||||
|
f,
|
||||||
|
"Vulkan device {device} surface does not support COLOR_ATTACHMENT usage"
|
||||||
|
),
|
||||||
|
Self::MissingDepthStencilFormat { device, requested } => write!(
|
||||||
|
f,
|
||||||
|
"Vulkan device {device} lacks a depth/stencil attachment format for {}-bit depth and {}-bit stencil",
|
||||||
|
requested.depth_bits,
|
||||||
|
requested.stencil_bits
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for VulkanCapabilityError {}
|
||||||
|
|
||||||
|
/// Selects a Vulkan physical device using deterministic Stage 0 policy.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanCapabilityError`] when no candidate satisfies the minimum
|
||||||
|
/// API version, queue, swapchain-extension and surface-format requirements.
|
||||||
|
pub fn select_physical_device(
|
||||||
|
devices: &[VulkanPhysicalDeviceRecord],
|
||||||
|
) -> Result<VulkanCapabilityReport, VulkanCapabilityError> {
|
||||||
|
select_physical_device_for_request(devices, RenderRequest::conservative())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selects a Vulkan physical device for a specific Stage 0 render request.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanCapabilityError`] when no candidate satisfies the minimum
|
||||||
|
/// API version, queue, swapchain-extension, surface-format or depth/stencil
|
||||||
|
/// requirements for the requested profile.
|
||||||
|
pub fn select_physical_device_for_request(
|
||||||
|
devices: &[VulkanPhysicalDeviceRecord],
|
||||||
|
render_request: RenderRequest,
|
||||||
|
) -> Result<VulkanCapabilityReport, VulkanCapabilityError> {
|
||||||
|
if devices.is_empty() {
|
||||||
|
return Err(VulkanCapabilityError::NoPhysicalDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut best = None;
|
||||||
|
let mut rejected_devices = Vec::new();
|
||||||
|
let mut last_error = None;
|
||||||
|
for device in devices {
|
||||||
|
let report = match validate_device_for_request(device, render_request) {
|
||||||
|
Ok(report) => report,
|
||||||
|
Err(err) => {
|
||||||
|
rejected_devices.push(rejected_device_report(device, &err));
|
||||||
|
last_error = Some(err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match &best {
|
||||||
|
Some(existing) if compare_reports(&report, existing) != std::cmp::Ordering::Greater => {
|
||||||
|
}
|
||||||
|
_ => best = Some(report),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut best =
|
||||||
|
best.ok_or_else(|| last_error.unwrap_or(VulkanCapabilityError::NoPhysicalDevice))?;
|
||||||
|
best.rejected_devices = rejected_devices;
|
||||||
|
Ok(best)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a deterministic swapchain plan from surface capabilities.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanSwapchainError`] when formats, present modes or extent are
|
||||||
|
/// unusable.
|
||||||
|
pub fn plan_vulkan_swapchain(
|
||||||
|
request: &VulkanSwapchainRequest,
|
||||||
|
) -> Result<VulkanSwapchainPlan, VulkanSwapchainError> {
|
||||||
|
let format = select_surface_format(&request.formats)?;
|
||||||
|
let present_mode = select_present_mode(&request.present_modes, request.preferred_present_mode)?;
|
||||||
|
let extent = select_swapchain_extent(request)?;
|
||||||
|
if extent.0 == 0 || extent.1 == 0 {
|
||||||
|
return Err(VulkanSwapchainError::EmptyExtent);
|
||||||
|
}
|
||||||
|
Ok(VulkanSwapchainPlan {
|
||||||
|
schema: 1,
|
||||||
|
extent,
|
||||||
|
format,
|
||||||
|
present_mode,
|
||||||
|
image_count: select_image_count(request.capabilities),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a deterministic swapchain recreation report.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn swapchain_recreation_report(
|
||||||
|
reason: VulkanSwapchainRecreationReason,
|
||||||
|
previous_extent: (u32, u32),
|
||||||
|
next_extent: (u32, u32),
|
||||||
|
) -> VulkanSwapchainRecreationReport {
|
||||||
|
VulkanSwapchainRecreationReport {
|
||||||
|
schema: 1,
|
||||||
|
reason,
|
||||||
|
previous_extent,
|
||||||
|
next_extent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a deterministic frame submission plan for a validated command list.
|
||||||
|
///
|
||||||
|
/// Stage 0 keeps this as a pure planning boundary so command-pool, command-buffer
|
||||||
|
/// and synchronization policy can be tested without requiring a native surface.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RenderError`] when the command list has invalid frame framing,
|
||||||
|
/// ordering, draw ranges, mesh bounds, or non-finite transforms.
|
||||||
|
pub fn plan_vulkan_frame_submission(
|
||||||
|
swapchain: &VulkanSwapchainPlan,
|
||||||
|
commands: &RenderCommandList,
|
||||||
|
) -> Result<VulkanFrameSubmissionPlan, RenderError> {
|
||||||
|
validate_command_list(commands)?;
|
||||||
|
let mut draw_count = 0_u32;
|
||||||
|
let mut indexed_vertex_count = 0_u32;
|
||||||
|
for command in &commands.commands {
|
||||||
|
if let RenderCommand::Draw(draw) = command {
|
||||||
|
draw_count = draw_count.saturating_add(1);
|
||||||
|
indexed_vertex_count = indexed_vertex_count.saturating_add(draw.range.count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(VulkanFrameSubmissionPlan {
|
||||||
|
schema: 1,
|
||||||
|
frames_in_flight: swapchain.image_count.clamp(1, 2),
|
||||||
|
command_buffers: swapchain.image_count,
|
||||||
|
semaphores_per_frame: 2,
|
||||||
|
fences_per_frame: 1,
|
||||||
|
draw_count,
|
||||||
|
indexed_vertex_count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a deterministic JSON capability report.
|
||||||
|
#[must_use]
|
||||||
|
pub fn render_capability_report_json(report: &VulkanCapabilityReport) -> String {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct CapabilityReportJson<'a> {
|
||||||
|
schema: u32,
|
||||||
|
vulkan_api: String,
|
||||||
|
device_name: &'a str,
|
||||||
|
score: i32,
|
||||||
|
graphics_queue_family: u32,
|
||||||
|
present_queue_family: u32,
|
||||||
|
portability_subset: bool,
|
||||||
|
enabled_extensions: &'a [String],
|
||||||
|
informational_capabilities: &'a VulkanInformationalCapabilities,
|
||||||
|
rejected_devices: &'a [VulkanRejectedDeviceReport],
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize_json_or_fallback(
|
||||||
|
&CapabilityReportJson {
|
||||||
|
schema: report.schema,
|
||||||
|
vulkan_api: format_api_version(report.vulkan_api_version),
|
||||||
|
device_name: &report.device_name,
|
||||||
|
score: report.score,
|
||||||
|
graphics_queue_family: report.graphics_queue_family,
|
||||||
|
present_queue_family: report.present_queue_family,
|
||||||
|
portability_subset: report.portability_subset,
|
||||||
|
enabled_extensions: &report.enabled_extensions,
|
||||||
|
informational_capabilities: &report.informational_capabilities,
|
||||||
|
rejected_devices: &report.rejected_devices,
|
||||||
|
},
|
||||||
|
"{\"schema\":0,\"vulkan_api\":\"0.0.0\",\"device_name\":\"unknown\",\"score\":0,\"graphics_queue_family\":0,\"present_queue_family\":0,\"portability_subset\":false,\"enabled_extensions\":[],\"informational_capabilities\":{\"sampled_color_formats\":[],\"sampled_depth_formats\":[],\"limits\":{\"max_image_dimension_2d\":0,\"max_sampler_allocation_count\":0,\"max_per_stage_descriptor_samplers\":0,\"max_bound_descriptor_sets\":0}},\"rejected_devices\":[]}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a deterministic JSON swapchain plan.
|
||||||
|
#[must_use]
|
||||||
|
pub fn render_swapchain_plan_json(plan: &VulkanSwapchainPlan) -> String {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SwapchainPlanJson {
|
||||||
|
schema: u32,
|
||||||
|
extent: [u32; 2],
|
||||||
|
format: i32,
|
||||||
|
color_space: i32,
|
||||||
|
present_mode: i32,
|
||||||
|
image_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize_json_or_fallback(
|
||||||
|
&SwapchainPlanJson {
|
||||||
|
schema: plan.schema,
|
||||||
|
extent: [plan.extent.0, plan.extent.1],
|
||||||
|
format: plan.format.format,
|
||||||
|
color_space: plan.format.color_space,
|
||||||
|
present_mode: plan.present_mode,
|
||||||
|
image_count: plan.image_count,
|
||||||
|
},
|
||||||
|
"{\"schema\":0,\"extent\":[0,0],\"format\":0,\"color_space\":0,\"present_mode\":0,\"image_count\":0}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a deterministic JSON swapchain recreation report.
|
||||||
|
#[must_use]
|
||||||
|
pub fn render_swapchain_recreation_report_json(report: &VulkanSwapchainRecreationReport) -> String {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SwapchainRecreationReportJson<'a> {
|
||||||
|
schema: u32,
|
||||||
|
reason: &'a str,
|
||||||
|
previous_extent: [u32; 2],
|
||||||
|
next_extent: [u32; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize_json_or_fallback(
|
||||||
|
&SwapchainRecreationReportJson {
|
||||||
|
schema: report.schema,
|
||||||
|
reason: match report.reason {
|
||||||
|
VulkanSwapchainRecreationReason::Resize => "resize",
|
||||||
|
VulkanSwapchainRecreationReason::OutOfDate => "out_of_date",
|
||||||
|
VulkanSwapchainRecreationReason::Suboptimal => "suboptimal",
|
||||||
|
},
|
||||||
|
previous_extent: [report.previous_extent.0, report.previous_extent.1],
|
||||||
|
next_extent: [report.next_extent.0, report.next_extent.1],
|
||||||
|
},
|
||||||
|
"{\"schema\":0,\"reason\":\"unknown\",\"previous_extent\":[0,0],\"next_extent\":[0,0]}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a deterministic JSON frame submission plan.
|
||||||
|
#[must_use]
|
||||||
|
pub fn render_frame_submission_plan_json(plan: &VulkanFrameSubmissionPlan) -> String {
|
||||||
|
serialize_json_or_fallback(
|
||||||
|
plan,
|
||||||
|
"{\"schema\":0,\"frames_in_flight\":0,\"command_buffers\":0,\"semaphores_per_frame\":0,\"fences_per_frame\":0,\"draw_count\":0,\"indexed_vertex_count\":0}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn select_composite_alpha(
|
||||||
|
supported: vk::CompositeAlphaFlagsKHR,
|
||||||
|
) -> vk::CompositeAlphaFlagsKHR {
|
||||||
|
if supported.contains(vk::CompositeAlphaFlagsKHR::OPAQUE) {
|
||||||
|
vk::CompositeAlphaFlagsKHR::OPAQUE
|
||||||
|
} else if supported.contains(vk::CompositeAlphaFlagsKHR::PRE_MULTIPLIED) {
|
||||||
|
vk::CompositeAlphaFlagsKHR::PRE_MULTIPLIED
|
||||||
|
} else if supported.contains(vk::CompositeAlphaFlagsKHR::POST_MULTIPLIED) {
|
||||||
|
vk::CompositeAlphaFlagsKHR::POST_MULTIPLIED
|
||||||
|
} else {
|
||||||
|
vk::CompositeAlphaFlagsKHR::INHERIT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn serialize_json_or_fallback<T: Serialize>(value: &T, fallback: &str) -> String {
|
||||||
|
match serde_json::to_string(value) {
|
||||||
|
Ok(json) => json,
|
||||||
|
Err(_) => fallback.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format_api_version(version: u32) -> String {
|
||||||
|
format!(
|
||||||
|
"{}.{}.{}",
|
||||||
|
vk::api_version_major(version),
|
||||||
|
vk::api_version_minor(version),
|
||||||
|
vk::api_version_patch(version)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_surface_format(
|
||||||
|
formats: &[VulkanSurfaceFormat],
|
||||||
|
) -> Result<VulkanSurfaceFormat, VulkanSwapchainError> {
|
||||||
|
if let Some(format) = undefined_surface_format_override(formats) {
|
||||||
|
return Ok(format);
|
||||||
|
}
|
||||||
|
formats
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|format| {
|
||||||
|
format.format == vk::Format::B8G8R8A8_SRGB.as_raw()
|
||||||
|
&& format.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR.as_raw()
|
||||||
|
})
|
||||||
|
.or_else(|| formats.first().copied())
|
||||||
|
.ok_or(VulkanSwapchainError::MissingSurfaceFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn undefined_surface_format_override(
|
||||||
|
formats: &[VulkanSurfaceFormat],
|
||||||
|
) -> Option<VulkanSurfaceFormat> {
|
||||||
|
match formats {
|
||||||
|
[format] if format.format == vk::Format::UNDEFINED.as_raw() => Some(VulkanSurfaceFormat {
|
||||||
|
format: vk::Format::B8G8R8A8_SRGB.as_raw(),
|
||||||
|
color_space: format.color_space,
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_present_mode(present_modes: &[i32], preferred: i32) -> Result<i32, VulkanSwapchainError> {
|
||||||
|
if present_modes.contains(&preferred) {
|
||||||
|
Ok(preferred)
|
||||||
|
} else if present_modes.contains(&vk::PresentModeKHR::FIFO.as_raw()) {
|
||||||
|
Ok(vk::PresentModeKHR::FIFO.as_raw())
|
||||||
|
} else {
|
||||||
|
present_modes
|
||||||
|
.first()
|
||||||
|
.copied()
|
||||||
|
.ok_or(VulkanSwapchainError::MissingPresentMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_swapchain_extent(
|
||||||
|
request: &VulkanSwapchainRequest,
|
||||||
|
) -> Result<(u32, u32), VulkanSwapchainError> {
|
||||||
|
if let Some(extent) = request.capabilities.current_extent {
|
||||||
|
return if extent.0 == 0 || extent.1 == 0 {
|
||||||
|
Err(VulkanSwapchainError::EmptyExtent)
|
||||||
|
} else {
|
||||||
|
Ok(extent)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let width = request.drawable_extent.0.clamp(
|
||||||
|
request.capabilities.min_extent.0,
|
||||||
|
request.capabilities.max_extent.0,
|
||||||
|
);
|
||||||
|
let height = request.drawable_extent.1.clamp(
|
||||||
|
request.capabilities.min_extent.1,
|
||||||
|
request.capabilities.max_extent.1,
|
||||||
|
);
|
||||||
|
Ok((width, height))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_image_count(capabilities: VulkanSwapchainSurfaceCapabilities) -> u32 {
|
||||||
|
let requested = capabilities.min_image_count.saturating_add(1).max(2);
|
||||||
|
if capabilities.max_image_count == 0 {
|
||||||
|
requested
|
||||||
|
} else {
|
||||||
|
requested.min(capabilities.max_image_count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn validate_device_for_request(
|
||||||
|
device: &VulkanPhysicalDeviceRecord,
|
||||||
|
render_request: RenderRequest,
|
||||||
|
) -> Result<VulkanCapabilityReport, VulkanCapabilityError> {
|
||||||
|
if device.api_version < MIN_VULKAN_API_VERSION {
|
||||||
|
return Err(VulkanCapabilityError::ApiVersionTooLow {
|
||||||
|
required: MIN_VULKAN_API_VERSION,
|
||||||
|
found: device.api_version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if !device.supports_extension(KHR_SWAPCHAIN_EXTENSION) {
|
||||||
|
return Err(VulkanCapabilityError::MissingSwapchainExtension {
|
||||||
|
device: device.name.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if !supports_surface_formats(device) {
|
||||||
|
return Err(VulkanCapabilityError::MissingSurfaceFormat {
|
||||||
|
device: device.name.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if device.present_modes.is_empty() {
|
||||||
|
return Err(VulkanCapabilityError::MissingPresentMode {
|
||||||
|
device: device.name.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if !supports_color_attachment_usage(device.surface_capabilities) {
|
||||||
|
return Err(VulkanCapabilityError::MissingColorAttachmentUsage {
|
||||||
|
device: device.name.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if !supports_depth_stencil_request(device, render_request.depth) {
|
||||||
|
return Err(VulkanCapabilityError::MissingDepthStencilFormat {
|
||||||
|
device: device.name.clone(),
|
||||||
|
requested: render_request.depth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let (graphics_queue_family, present_queue_family) = select_queue_families(device)?;
|
||||||
|
|
||||||
|
let portability_subset = device.supports_extension(KHR_PORTABILITY_SUBSET_EXTENSION);
|
||||||
|
let mut enabled_extensions = vec![KHR_SWAPCHAIN_EXTENSION.to_string()];
|
||||||
|
if portability_subset {
|
||||||
|
enabled_extensions.push(KHR_PORTABILITY_SUBSET_EXTENSION.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(VulkanCapabilityReport {
|
||||||
|
schema: 1,
|
||||||
|
device_name: device.name.clone(),
|
||||||
|
vulkan_api_version: device.api_version,
|
||||||
|
score: score_device(device, graphics_queue_family, present_queue_family),
|
||||||
|
graphics_queue_family,
|
||||||
|
present_queue_family,
|
||||||
|
portability_subset,
|
||||||
|
enabled_extensions,
|
||||||
|
informational_capabilities: informational_capabilities(device),
|
||||||
|
rejected_devices: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rejected_device_report(
|
||||||
|
device: &VulkanPhysicalDeviceRecord,
|
||||||
|
error: &VulkanCapabilityError,
|
||||||
|
) -> VulkanRejectedDeviceReport {
|
||||||
|
VulkanRejectedDeviceReport {
|
||||||
|
device_name: device.name.clone(),
|
||||||
|
reason_code: capability_error_code(error),
|
||||||
|
reason: error.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn capability_error_code(error: &VulkanCapabilityError) -> &'static str {
|
||||||
|
match error {
|
||||||
|
VulkanCapabilityError::NoPhysicalDevice => "no_physical_device",
|
||||||
|
VulkanCapabilityError::ApiVersionTooLow { .. } => "api_version_too_low",
|
||||||
|
VulkanCapabilityError::NoGraphicsQueue { .. } => "no_graphics_queue",
|
||||||
|
VulkanCapabilityError::NoPresentQueue { .. } => "no_present_queue",
|
||||||
|
VulkanCapabilityError::MissingSwapchainExtension { .. } => "missing_swapchain_extension",
|
||||||
|
VulkanCapabilityError::MissingSurfaceFormat { .. } => "missing_surface_format",
|
||||||
|
VulkanCapabilityError::MissingPresentMode { .. } => "missing_present_mode",
|
||||||
|
VulkanCapabilityError::MissingColorAttachmentUsage { .. } => {
|
||||||
|
"missing_color_attachment_usage"
|
||||||
|
}
|
||||||
|
VulkanCapabilityError::MissingDepthStencilFormat { .. } => "missing_depth_stencil_format",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_queue_families(
|
||||||
|
device: &VulkanPhysicalDeviceRecord,
|
||||||
|
) -> Result<(u32, u32), VulkanCapabilityError> {
|
||||||
|
if let Some(unified) = device
|
||||||
|
.queue_families
|
||||||
|
.iter()
|
||||||
|
.filter(|family| family.graphics && family.present)
|
||||||
|
.min_by_key(|family| family.index)
|
||||||
|
{
|
||||||
|
return Ok((unified.index, unified.index));
|
||||||
|
}
|
||||||
|
|
||||||
|
let graphics_queue_family = device
|
||||||
|
.queue_families
|
||||||
|
.iter()
|
||||||
|
.filter(|family| family.graphics)
|
||||||
|
.min_by_key(|family| family.index)
|
||||||
|
.ok_or_else(|| VulkanCapabilityError::NoGraphicsQueue {
|
||||||
|
device: device.name.clone(),
|
||||||
|
})?
|
||||||
|
.index;
|
||||||
|
let present_queue_family = device
|
||||||
|
.queue_families
|
||||||
|
.iter()
|
||||||
|
.filter(|family| family.present)
|
||||||
|
.min_by_key(|family| family.index)
|
||||||
|
.ok_or_else(|| VulkanCapabilityError::NoPresentQueue {
|
||||||
|
device: device.name.clone(),
|
||||||
|
})?
|
||||||
|
.index;
|
||||||
|
Ok((graphics_queue_family, present_queue_family))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_surface_formats(device: &VulkanPhysicalDeviceRecord) -> bool {
|
||||||
|
!device.surface_formats.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_color_attachment_usage(capabilities: VulkanSwapchainSurfaceCapabilities) -> bool {
|
||||||
|
capabilities.supported_usage_flags & vk::ImageUsageFlags::COLOR_ATTACHMENT.as_raw() != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_depth_stencil_request(
|
||||||
|
device: &VulkanPhysicalDeviceRecord,
|
||||||
|
depth: DepthStencilSupport,
|
||||||
|
) -> bool {
|
||||||
|
if depth.depth_bits == 0 && depth.stencil_bits == 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
required_depth_stencil_formats(depth).iter().any(|format| {
|
||||||
|
device
|
||||||
|
.supported_depth_stencil_formats
|
||||||
|
.contains(&format.as_raw())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn informational_capabilities(
|
||||||
|
device: &VulkanPhysicalDeviceRecord,
|
||||||
|
) -> VulkanInformationalCapabilities {
|
||||||
|
let (sampled_depth_formats, sampled_color_formats): (Vec<_>, Vec<_>) = device
|
||||||
|
.sampled_image_formats
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.partition(|format| is_depth_stencil_format(*format));
|
||||||
|
VulkanInformationalCapabilities {
|
||||||
|
sampled_color_formats,
|
||||||
|
sampled_depth_formats,
|
||||||
|
limits: device.limits,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required_depth_stencil_formats(depth: DepthStencilSupport) -> &'static [vk::Format] {
|
||||||
|
match (depth.depth_bits, depth.stencil_bits) {
|
||||||
|
(16, 0) => &[vk::Format::D16_UNORM, vk::Format::D32_SFLOAT],
|
||||||
|
(24, 0) => &[vk::Format::X8_D24_UNORM_PACK32, vk::Format::D32_SFLOAT],
|
||||||
|
(32, 0) => &[vk::Format::D32_SFLOAT],
|
||||||
|
(16, 8) => &[vk::Format::D16_UNORM_S8_UINT, vk::Format::D24_UNORM_S8_UINT],
|
||||||
|
(24, 8) => &[
|
||||||
|
vk::Format::D24_UNORM_S8_UINT,
|
||||||
|
vk::Format::D32_SFLOAT_S8_UINT,
|
||||||
|
],
|
||||||
|
(32, 8) => &[vk::Format::D32_SFLOAT_S8_UINT],
|
||||||
|
_ => &[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_depth_stencil_format(format: i32) -> bool {
|
||||||
|
matches!(
|
||||||
|
vk::Format::from_raw(format),
|
||||||
|
vk::Format::D16_UNORM
|
||||||
|
| vk::Format::X8_D24_UNORM_PACK32
|
||||||
|
| vk::Format::D32_SFLOAT
|
||||||
|
| vk::Format::S8_UINT
|
||||||
|
| vk::Format::D16_UNORM_S8_UINT
|
||||||
|
| vk::Format::D24_UNORM_S8_UINT
|
||||||
|
| vk::Format::D32_SFLOAT_S8_UINT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn score_device(
|
||||||
|
device: &VulkanPhysicalDeviceRecord,
|
||||||
|
graphics_queue_family: u32,
|
||||||
|
present_queue_family: u32,
|
||||||
|
) -> i32 {
|
||||||
|
let unified_queue_bonus = if graphics_queue_family == present_queue_family {
|
||||||
|
100
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let portability_penalty = if device.supports_extension(KHR_PORTABILITY_SUBSET_EXTENSION) {
|
||||||
|
-50
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
device.device_type.score_bonus()
|
||||||
|
+ unified_queue_bonus
|
||||||
|
+ portability_penalty
|
||||||
|
+ i32::try_from(device.surface_formats.len()).unwrap_or(i32::MAX)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn compare_reports(
|
||||||
|
left: &VulkanCapabilityReport,
|
||||||
|
right: &VulkanCapabilityReport,
|
||||||
|
) -> std::cmp::Ordering {
|
||||||
|
left.score
|
||||||
|
.cmp(&right.score)
|
||||||
|
.then_with(|| right.device_name.cmp(&left.device_name))
|
||||||
|
}
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
use fparkan_binary::{sha256, sha256_hex};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
pub(crate) use crate::ffi::{
|
||||||
|
SPIRV_MAGIC, SPIRV_VERSION_1_0, TRIANGLE_FRAGMENT_SHADER_WORDS, TRIANGLE_VERTEX_SHADER_WORDS,
|
||||||
|
};
|
||||||
|
use crate::policy::serialize_json_or_fallback;
|
||||||
|
|
||||||
|
pub(crate) const SHADER_MANIFEST_SCHEMA: u32 = 2;
|
||||||
|
pub(crate) const SHADER_TARGET_ENV: &str = "vulkan1.1";
|
||||||
|
pub(crate) const SHADER_COMPILER_NAME: &str = "glslangValidator";
|
||||||
|
pub(crate) const SHADER_COMPILER_VERSION: &str = "11:16.3.0";
|
||||||
|
pub(crate) const SHADER_COMPILER_BINARY_SHA256: &str =
|
||||||
|
"9bcd69d830b350aaa6e2254915ff74e46070e217b67f38daad27c1fc1f22910f";
|
||||||
|
pub(crate) const SPIRV_VALIDATOR_NAME: &str = "spirv-val";
|
||||||
|
pub(crate) const SPIRV_VALIDATOR_VERSION: &str =
|
||||||
|
"SPIRV-Tools v2026.2 unknown hash, 2026-04-29T17:02:58+00:00";
|
||||||
|
pub(crate) const SPIRV_VALIDATOR_BINARY_SHA256: &str =
|
||||||
|
"f6d5b96ff19f073f3af0c0bcfa0c18702d288d3ec598efc242d01cd104d8354f";
|
||||||
|
pub(crate) const TRIANGLE_VERTEX_SOURCE_PATH: &str =
|
||||||
|
"adapters/fparkan-render-vulkan/shaders/triangle.vert";
|
||||||
|
pub(crate) const TRIANGLE_VERTEX_SOURCE_SHA256: &str =
|
||||||
|
"1e57f14d193fc61457c0749081c452ad25669998913107df12f3ccc3c33e0341";
|
||||||
|
pub(crate) const TRIANGLE_VERTEX_SPIRV_PATH: &str =
|
||||||
|
"adapters/fparkan-render-vulkan/shaders/triangle.vert.spv";
|
||||||
|
pub(crate) const TRIANGLE_VERTEX_COMPILE_COMMAND: &str =
|
||||||
|
"glslangValidator -V --target-env vulkan1.1 -S vert -e main adapters/fparkan-render-vulkan/shaders/triangle.vert -o adapters/fparkan-render-vulkan/shaders/triangle.vert.spv";
|
||||||
|
pub(crate) const TRIANGLE_VERTEX_VALIDATE_COMMAND: &str =
|
||||||
|
"spirv-val --target-env vulkan1.1 adapters/fparkan-render-vulkan/shaders/triangle.vert.spv";
|
||||||
|
const TRIANGLE_FRAGMENT_SOURCE_PATH: &str = "adapters/fparkan-render-vulkan/shaders/triangle.frag";
|
||||||
|
const TRIANGLE_FRAGMENT_SOURCE_SHA256: &str =
|
||||||
|
"f19e74d001d07fb537d4b0f9e621f9b8bc40eeb68816130220853abea6bd4445";
|
||||||
|
const TRIANGLE_FRAGMENT_SPIRV_PATH: &str =
|
||||||
|
"adapters/fparkan-render-vulkan/shaders/triangle.frag.spv";
|
||||||
|
const TRIANGLE_FRAGMENT_COMPILE_COMMAND: &str =
|
||||||
|
"glslangValidator -V --target-env vulkan1.1 -S frag -e main adapters/fparkan-render-vulkan/shaders/triangle.frag -o adapters/fparkan-render-vulkan/shaders/triangle.frag.spv";
|
||||||
|
const TRIANGLE_FRAGMENT_VALIDATE_COMMAND: &str =
|
||||||
|
"spirv-val --target-env vulkan1.1 adapters/fparkan-render-vulkan/shaders/triangle.frag.spv";
|
||||||
|
|
||||||
|
fn shader_compiler_name() -> &'static str {
|
||||||
|
option_env!("FPARKAN_BUILD_SHADER_COMPILER_NAME").unwrap_or(SHADER_COMPILER_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shader_compiler_version() -> &'static str {
|
||||||
|
option_env!("FPARKAN_BUILD_SHADER_COMPILER_VERSION").unwrap_or(SHADER_COMPILER_VERSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shader_compiler_binary_sha256() -> &'static str {
|
||||||
|
option_env!("FPARKAN_BUILD_SHADER_COMPILER_SHA256").unwrap_or(SHADER_COMPILER_BINARY_SHA256)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spirv_validator_name() -> &'static str {
|
||||||
|
option_env!("FPARKAN_BUILD_SPIRV_VALIDATOR_NAME").unwrap_or(SPIRV_VALIDATOR_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spirv_validator_version() -> &'static str {
|
||||||
|
option_env!("FPARKAN_BUILD_SPIRV_VALIDATOR_VERSION").unwrap_or(SPIRV_VALIDATOR_VERSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spirv_validator_binary_sha256() -> &'static str {
|
||||||
|
option_env!("FPARKAN_BUILD_SPIRV_VALIDATOR_SHA256").unwrap_or(SPIRV_VALIDATOR_BINARY_SHA256)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shader tool metadata pinned in the Stage 0 manifest.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct VulkanShaderToolManifest {
|
||||||
|
/// Tool executable name.
|
||||||
|
pub name: &'static str,
|
||||||
|
/// Tool version string.
|
||||||
|
pub version: &'static str,
|
||||||
|
/// Tool binary SHA-256.
|
||||||
|
pub binary_sha256: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vulkan shader stage.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum VulkanShaderStage {
|
||||||
|
/// Vertex stage.
|
||||||
|
Vertex,
|
||||||
|
/// Fragment stage.
|
||||||
|
Fragment,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Offline SPIR-V shader manifest entry.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanShaderModuleManifest {
|
||||||
|
/// Logical shader name.
|
||||||
|
pub name: &'static str,
|
||||||
|
/// Shader stage.
|
||||||
|
pub stage: VulkanShaderStage,
|
||||||
|
/// SPIR-V entry point.
|
||||||
|
pub entry_point: &'static str,
|
||||||
|
/// Descriptor set count.
|
||||||
|
pub descriptor_sets: u32,
|
||||||
|
/// Push constant byte count.
|
||||||
|
pub push_constant_bytes: u32,
|
||||||
|
/// Checked-in GLSL source path.
|
||||||
|
pub source_path: &'static str,
|
||||||
|
/// Checked-in GLSL source SHA-256.
|
||||||
|
pub source_sha256: &'static str,
|
||||||
|
/// Checked-in SPIR-V module path.
|
||||||
|
pub spirv_path: &'static str,
|
||||||
|
/// Exact offline compile command used for the checked-in SPIR-V artifact.
|
||||||
|
pub compile_command: &'static str,
|
||||||
|
/// Exact offline validation command used for the checked-in SPIR-V artifact.
|
||||||
|
pub validate_command: &'static str,
|
||||||
|
/// SPIR-V words.
|
||||||
|
pub words: &'static [u32],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shader manifest validation report.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct VulkanShaderManifestReport {
|
||||||
|
/// Report schema version.
|
||||||
|
pub schema: u32,
|
||||||
|
/// Explicit Vulkan target environment for the checked-in SPIR-V.
|
||||||
|
pub target_env: &'static str,
|
||||||
|
/// Pinned compiler metadata.
|
||||||
|
pub compiler: VulkanShaderToolManifest,
|
||||||
|
/// Pinned validator metadata.
|
||||||
|
pub validator: VulkanShaderToolManifest,
|
||||||
|
/// Shader module reports.
|
||||||
|
pub modules: Vec<VulkanShaderModuleReport>,
|
||||||
|
/// Hash of the normalized shader manifest.
|
||||||
|
pub manifest_hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shader module validation report.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct VulkanShaderModuleReport {
|
||||||
|
/// Logical shader name.
|
||||||
|
pub name: &'static str,
|
||||||
|
/// Shader stage.
|
||||||
|
pub stage: VulkanShaderStage,
|
||||||
|
/// SPIR-V entry point.
|
||||||
|
pub entry_point: &'static str,
|
||||||
|
/// Checked-in GLSL source path.
|
||||||
|
pub source_path: &'static str,
|
||||||
|
/// Checked-in GLSL source SHA-256.
|
||||||
|
pub source_sha256: &'static str,
|
||||||
|
/// Checked-in SPIR-V module path.
|
||||||
|
pub spirv_path: &'static str,
|
||||||
|
/// SPIR-V word count.
|
||||||
|
pub word_count: usize,
|
||||||
|
/// SPIR-V byte hash.
|
||||||
|
pub sha256: String,
|
||||||
|
/// Descriptor set count.
|
||||||
|
pub descriptor_sets: u32,
|
||||||
|
/// Push constant byte count.
|
||||||
|
pub push_constant_bytes: u32,
|
||||||
|
/// Exact offline compile command used for the checked-in SPIR-V artifact.
|
||||||
|
pub compile_command: &'static str,
|
||||||
|
/// Exact offline validation command used for the checked-in SPIR-V artifact.
|
||||||
|
pub validate_command: &'static str,
|
||||||
|
/// Stable hash of the reflected interface contract for this module.
|
||||||
|
pub interface_hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shader manifest validation error.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum VulkanShaderManifestError {
|
||||||
|
/// SPIR-V module is too short to contain a header.
|
||||||
|
TooShort {
|
||||||
|
/// Shader name.
|
||||||
|
name: &'static str,
|
||||||
|
},
|
||||||
|
/// SPIR-V module has an invalid magic word.
|
||||||
|
InvalidMagic {
|
||||||
|
/// Shader name.
|
||||||
|
name: &'static str,
|
||||||
|
/// Found magic word.
|
||||||
|
found: u32,
|
||||||
|
},
|
||||||
|
/// SPIR-V module version is below 1.0.
|
||||||
|
UnsupportedVersion {
|
||||||
|
/// Shader name.
|
||||||
|
name: &'static str,
|
||||||
|
/// Found version word.
|
||||||
|
found: u32,
|
||||||
|
},
|
||||||
|
/// SPIR-V module declares an invalid bound.
|
||||||
|
InvalidBound {
|
||||||
|
/// Shader name.
|
||||||
|
name: &'static str,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for VulkanShaderManifestError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::TooShort { name } => write!(f, "shader {name} SPIR-V module is too short"),
|
||||||
|
Self::InvalidMagic { name, found } => {
|
||||||
|
write!(f, "shader {name} has invalid SPIR-V magic 0x{found:08x}")
|
||||||
|
}
|
||||||
|
Self::UnsupportedVersion { name, found } => write!(
|
||||||
|
f,
|
||||||
|
"shader {name} has unsupported SPIR-V version 0x{found:08x}"
|
||||||
|
),
|
||||||
|
Self::InvalidBound { name } => write!(f, "shader {name} has invalid SPIR-V bound"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for VulkanShaderManifestError {}
|
||||||
|
|
||||||
|
/// Returns the built-in Stage 0 indexed-triangle shader manifest.
|
||||||
|
#[must_use]
|
||||||
|
pub fn triangle_shader_manifest() -> Vec<VulkanShaderModuleManifest> {
|
||||||
|
vec![
|
||||||
|
VulkanShaderModuleManifest {
|
||||||
|
name: "triangle.vert",
|
||||||
|
stage: VulkanShaderStage::Vertex,
|
||||||
|
entry_point: "main",
|
||||||
|
descriptor_sets: 0,
|
||||||
|
push_constant_bytes: 0,
|
||||||
|
source_path: TRIANGLE_VERTEX_SOURCE_PATH,
|
||||||
|
source_sha256: TRIANGLE_VERTEX_SOURCE_SHA256,
|
||||||
|
spirv_path: TRIANGLE_VERTEX_SPIRV_PATH,
|
||||||
|
compile_command: TRIANGLE_VERTEX_COMPILE_COMMAND,
|
||||||
|
validate_command: TRIANGLE_VERTEX_VALIDATE_COMMAND,
|
||||||
|
words: TRIANGLE_VERTEX_SHADER_WORDS,
|
||||||
|
},
|
||||||
|
VulkanShaderModuleManifest {
|
||||||
|
name: "triangle.frag",
|
||||||
|
stage: VulkanShaderStage::Fragment,
|
||||||
|
entry_point: "main",
|
||||||
|
descriptor_sets: 0,
|
||||||
|
push_constant_bytes: 0,
|
||||||
|
source_path: TRIANGLE_FRAGMENT_SOURCE_PATH,
|
||||||
|
source_sha256: TRIANGLE_FRAGMENT_SOURCE_SHA256,
|
||||||
|
spirv_path: TRIANGLE_FRAGMENT_SPIRV_PATH,
|
||||||
|
compile_command: TRIANGLE_FRAGMENT_COMPILE_COMMAND,
|
||||||
|
validate_command: TRIANGLE_FRAGMENT_VALIDATE_COMMAND,
|
||||||
|
words: TRIANGLE_FRAGMENT_SHADER_WORDS,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates shader SPIR-V containers and renders a deterministic report.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanShaderManifestError`] when a module fails Stage 0 SPIR-V
|
||||||
|
/// container validation.
|
||||||
|
pub fn validate_shader_manifest(
|
||||||
|
modules: &[VulkanShaderModuleManifest],
|
||||||
|
) -> Result<VulkanShaderManifestReport, VulkanShaderManifestError> {
|
||||||
|
let mut reports = Vec::with_capacity(modules.len());
|
||||||
|
for module in modules {
|
||||||
|
validate_spirv_container(module)?;
|
||||||
|
let bytes = spirv_words_to_bytes(module.words);
|
||||||
|
reports.push(VulkanShaderModuleReport {
|
||||||
|
name: module.name,
|
||||||
|
stage: module.stage,
|
||||||
|
entry_point: module.entry_point,
|
||||||
|
source_path: module.source_path,
|
||||||
|
source_sha256: module.source_sha256,
|
||||||
|
spirv_path: module.spirv_path,
|
||||||
|
word_count: module.words.len(),
|
||||||
|
sha256: sha256_hex(&sha256(&bytes)),
|
||||||
|
descriptor_sets: module.descriptor_sets,
|
||||||
|
push_constant_bytes: module.push_constant_bytes,
|
||||||
|
compile_command: module.compile_command,
|
||||||
|
validate_command: module.validate_command,
|
||||||
|
interface_hash: shader_interface_hash(module),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let normalized = render_shader_manifest_without_hash_json(&reports);
|
||||||
|
Ok(VulkanShaderManifestReport {
|
||||||
|
schema: SHADER_MANIFEST_SCHEMA,
|
||||||
|
target_env: SHADER_TARGET_ENV,
|
||||||
|
compiler: VulkanShaderToolManifest {
|
||||||
|
name: shader_compiler_name(),
|
||||||
|
version: shader_compiler_version(),
|
||||||
|
binary_sha256: shader_compiler_binary_sha256(),
|
||||||
|
},
|
||||||
|
validator: VulkanShaderToolManifest {
|
||||||
|
name: spirv_validator_name(),
|
||||||
|
version: spirv_validator_version(),
|
||||||
|
binary_sha256: spirv_validator_binary_sha256(),
|
||||||
|
},
|
||||||
|
modules: reports,
|
||||||
|
manifest_hash: sha256_hex(&sha256(normalized.as_bytes())),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a deterministic JSON shader manifest report.
|
||||||
|
#[must_use]
|
||||||
|
pub fn render_shader_manifest_report_json(report: &VulkanShaderManifestReport) -> String {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ShaderManifestReportJson<'a> {
|
||||||
|
schema: u32,
|
||||||
|
target_env: &'a str,
|
||||||
|
compiler: &'a VulkanShaderToolManifest,
|
||||||
|
validator: &'a VulkanShaderToolManifest,
|
||||||
|
modules: &'a [VulkanShaderModuleReport],
|
||||||
|
manifest_hash: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize_json_or_fallback(
|
||||||
|
&ShaderManifestReportJson {
|
||||||
|
schema: report.schema,
|
||||||
|
target_env: report.target_env,
|
||||||
|
compiler: &report.compiler,
|
||||||
|
validator: &report.validator,
|
||||||
|
modules: &report.modules,
|
||||||
|
manifest_hash: &report.manifest_hash,
|
||||||
|
},
|
||||||
|
"{\"schema\":0,\"target_env\":\"unknown\",\"compiler\":{\"name\":\"unknown\",\"version\":\"unknown\",\"binary_sha256\":\"unknown\"},\"validator\":{\"name\":\"unknown\",\"version\":\"unknown\",\"binary_sha256\":\"unknown\"},\"modules\":[],\"manifest_hash\":\"unknown\"}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shader_interface_hash(module: &VulkanShaderModuleManifest) -> String {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ShaderInterfaceHashJson<'a> {
|
||||||
|
stage: VulkanShaderStage,
|
||||||
|
entry_point: &'a str,
|
||||||
|
descriptor_sets: u32,
|
||||||
|
push_constant_bytes: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalized = serialize_json_or_fallback(
|
||||||
|
&ShaderInterfaceHashJson {
|
||||||
|
stage: module.stage,
|
||||||
|
entry_point: module.entry_point,
|
||||||
|
descriptor_sets: module.descriptor_sets,
|
||||||
|
push_constant_bytes: module.push_constant_bytes,
|
||||||
|
},
|
||||||
|
"{\"stage\":\"vertex\",\"entry_point\":\"main\",\"descriptor_sets\":0,\"push_constant_bytes\":0}",
|
||||||
|
);
|
||||||
|
sha256_hex(&sha256(normalized.as_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_spirv_container(
|
||||||
|
module: &VulkanShaderModuleManifest,
|
||||||
|
) -> Result<(), VulkanShaderManifestError> {
|
||||||
|
if module.words.len() < 5 {
|
||||||
|
return Err(VulkanShaderManifestError::TooShort { name: module.name });
|
||||||
|
}
|
||||||
|
if module.words[0] != SPIRV_MAGIC {
|
||||||
|
return Err(VulkanShaderManifestError::InvalidMagic {
|
||||||
|
name: module.name,
|
||||||
|
found: module.words[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if module.words[1] < SPIRV_VERSION_1_0 {
|
||||||
|
return Err(VulkanShaderManifestError::UnsupportedVersion {
|
||||||
|
name: module.name,
|
||||||
|
found: module.words[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if module.words[3] == 0 {
|
||||||
|
return Err(VulkanShaderManifestError::InvalidBound { name: module.name });
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spirv_words_to_bytes(words: &[u32]) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(words.len() * 4);
|
||||||
|
for word in words {
|
||||||
|
out.extend_from_slice(&word.to_le_bytes());
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_shader_manifest_without_hash_json(modules: &[VulkanShaderModuleReport]) -> String {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ShaderManifestWithoutHashJson<'a> {
|
||||||
|
schema: u32,
|
||||||
|
target_env: &'a str,
|
||||||
|
compiler: VulkanShaderToolManifest,
|
||||||
|
validator: VulkanShaderToolManifest,
|
||||||
|
modules: &'a [VulkanShaderModuleReport],
|
||||||
|
}
|
||||||
|
|
||||||
|
let json = serialize_json_or_fallback(
|
||||||
|
&ShaderManifestWithoutHashJson {
|
||||||
|
schema: SHADER_MANIFEST_SCHEMA,
|
||||||
|
target_env: SHADER_TARGET_ENV,
|
||||||
|
compiler: VulkanShaderToolManifest {
|
||||||
|
name: SHADER_COMPILER_NAME,
|
||||||
|
version: SHADER_COMPILER_VERSION,
|
||||||
|
binary_sha256: SHADER_COMPILER_BINARY_SHA256,
|
||||||
|
},
|
||||||
|
validator: VulkanShaderToolManifest {
|
||||||
|
name: SPIRV_VALIDATOR_NAME,
|
||||||
|
version: SPIRV_VALIDATOR_VERSION,
|
||||||
|
binary_sha256: SPIRV_VALIDATOR_BINARY_SHA256,
|
||||||
|
},
|
||||||
|
modules,
|
||||||
|
},
|
||||||
|
"{\"schema\":0,\"target_env\":\"unknown\",\"compiler\":{\"name\":\"unknown\",\"version\":\"unknown\",\"binary_sha256\":\"unknown\"},\"validator\":{\"name\":\"unknown\",\"version\":\"unknown\",\"binary_sha256\":\"unknown\"},\"modules\":[]}",
|
||||||
|
);
|
||||||
|
match json.strip_suffix('}') {
|
||||||
|
Some(stripped) => stripped.to_string(),
|
||||||
|
None => json,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ use fparkan_render::{
|
|||||||
DrawCommand, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderBackend, RenderCommand,
|
DrawCommand, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderBackend, RenderCommand,
|
||||||
RenderCommandList, RenderPhase,
|
RenderCommandList, RenderPhase,
|
||||||
};
|
};
|
||||||
use fparkan_render_vulkan::VulkanBackend;
|
use fparkan_render_vulkan::VulkanPlanningBackend;
|
||||||
use fparkan_runtime::{
|
use fparkan_runtime::{
|
||||||
create, frame, load_mission, loaded_mission_assets, EngineConfig, EngineMode, EngineServices,
|
create, frame, load_mission, loaded_mission_assets, EngineConfig, EngineMode, EngineServices,
|
||||||
MissionAssets, MissionRequest,
|
MissionAssets, MissionRequest,
|
||||||
@@ -71,7 +71,7 @@ fn run(args: &[String]) -> Result<String, String> {
|
|||||||
)
|
)
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
let mut backend = VulkanBackend::new();
|
let mut backend = VulkanPlanningBackend::new();
|
||||||
let _request = WinitWindow::default_render_request();
|
let _request = WinitWindow::default_render_request();
|
||||||
let window = WinitWindow::synthetic(1280, 720);
|
let window = WinitWindow::synthetic(1280, 720);
|
||||||
let _ = window.drawable_size();
|
let _ = window.drawable_size();
|
||||||
@@ -104,8 +104,8 @@ fn run(args: &[String]) -> Result<String, String> {
|
|||||||
args.frames,
|
args.frames,
|
||||||
last_tick,
|
last_tick,
|
||||||
last_draw_count,
|
last_draw_count,
|
||||||
capture_report.submissions,
|
capture_report.execution.submission_plans,
|
||||||
capture_report.last_capture_size,
|
capture_report.execution.last_capture_size,
|
||||||
json_hash(&last_hash)
|
json_hash(&last_hash)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ version.workspace = true
|
|||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fparkan-platform = { path = "../../crates/fparkan-platform" }
|
fparkan-platform = { path = "../../crates/fparkan-platform" }
|
||||||
fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit" }
|
fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit" }
|
||||||
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" }
|
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
winit = "0.30"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
//! Build-time provenance for native smoke artifacts.
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-env-changed=TARGET");
|
||||||
|
println!("cargo:rerun-if-env-changed=RUSTC");
|
||||||
|
println!("cargo:rerun-if-env-changed=RUSTUP_TOOLCHAIN");
|
||||||
|
println!("cargo:rerun-if-env-changed=GITHUB_SHA");
|
||||||
|
println!("cargo:rerun-if-env-changed=SOURCE_VERSION");
|
||||||
|
println!("cargo:rerun-if-env-changed=BUILD_VCS_NUMBER");
|
||||||
|
|
||||||
|
if let Ok(target) = env::var("TARGET") {
|
||||||
|
println!("cargo:rustc-env=FPARKAN_BUILD_TARGET_TRIPLE={target}");
|
||||||
|
}
|
||||||
|
if let Some(toolchain) = rustc_release() {
|
||||||
|
println!("cargo:rustc-env=FPARKAN_BUILD_RUST_TOOLCHAIN={toolchain}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(workspace_root) = workspace_root() {
|
||||||
|
if let Some(git_dir) = git_dir(&workspace_root) {
|
||||||
|
emit_git_rerun_hints(&git_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(commit_sha) = env_commit_sha().or_else(|| git_head_commit_sha(&workspace_root))
|
||||||
|
{
|
||||||
|
println!("cargo:rustc-env=FPARKAN_BUILD_COMMIT_SHA={commit_sha}");
|
||||||
|
}
|
||||||
|
if let Some(git_dirty) = git_dirty(&workspace_root) {
|
||||||
|
println!("cargo:rustc-env=FPARKAN_BUILD_GIT_DIRTY={git_dirty}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_root() -> Option<PathBuf> {
|
||||||
|
env::var_os("CARGO_MANIFEST_DIR")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.map(|manifest_dir| manifest_dir.join("../.."))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_commit_sha() -> Option<String> {
|
||||||
|
["GITHUB_SHA", "SOURCE_VERSION", "BUILD_VCS_NUMBER"]
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|name| env::var(name).ok())
|
||||||
|
.find(|value| is_commit_sha(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn git_head_commit_sha(workspace_root: &Path) -> Option<String> {
|
||||||
|
let output = Command::new("git")
|
||||||
|
.args(["-C"])
|
||||||
|
.arg(workspace_root)
|
||||||
|
.args(["rev-parse", "HEAD"])
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let value = String::from_utf8(output.stdout).ok()?;
|
||||||
|
let value = value.trim().to_string();
|
||||||
|
is_commit_sha(&value).then_some(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn git_dirty(workspace_root: &Path) -> Option<bool> {
|
||||||
|
let output = Command::new("git")
|
||||||
|
.args(["-C"])
|
||||||
|
.arg(workspace_root)
|
||||||
|
.args(["status", "--short"])
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
output
|
||||||
|
.status
|
||||||
|
.success()
|
||||||
|
.then(|| !String::from_utf8_lossy(&output.stdout).trim().is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn git_dir(workspace_root: &Path) -> Option<PathBuf> {
|
||||||
|
let output = Command::new("git")
|
||||||
|
.args(["-C"])
|
||||||
|
.arg(workspace_root)
|
||||||
|
.args(["rev-parse", "--git-dir"])
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let value = String::from_utf8(output.stdout).ok()?;
|
||||||
|
let value = value.trim();
|
||||||
|
(!value.is_empty()).then(|| workspace_root.join(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_git_rerun_hints(git_dir: &Path) {
|
||||||
|
let head = git_dir.join("HEAD");
|
||||||
|
println!("cargo:rerun-if-changed={}", head.display());
|
||||||
|
println!(
|
||||||
|
"cargo:rerun-if-changed={}",
|
||||||
|
git_dir.join("packed-refs").display()
|
||||||
|
);
|
||||||
|
println!("cargo:rerun-if-changed={}", git_dir.join("index").display());
|
||||||
|
let Some(reference) = std::fs::read_to_string(&head).ok().and_then(|value| {
|
||||||
|
value
|
||||||
|
.strip_prefix("ref: ")
|
||||||
|
.map(str::trim)
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
"cargo:rerun-if-changed={}",
|
||||||
|
git_dir.join(reference).display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_commit_sha(value: &str) -> bool {
|
||||||
|
value.len() == 40 && value.chars().all(|ch| ch.is_ascii_hexdigit())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rustc_release() -> Option<String> {
|
||||||
|
let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string());
|
||||||
|
let output = Command::new(rustc).arg("-Vv").output().ok()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
String::from_utf8(output.stdout)
|
||||||
|
.ok()?
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| {
|
||||||
|
line.strip_prefix("release: ")
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(ToString::to_string)
|
||||||
|
})
|
||||||
|
}
|
||||||
+1006
-1445
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
|||||||
|
[graph]
|
||||||
|
all-features = true
|
||||||
|
|
||||||
|
[advisories]
|
||||||
|
yanked = "deny"
|
||||||
|
|
||||||
|
[bans]
|
||||||
|
multiple-versions = "allow"
|
||||||
|
wildcards = "deny"
|
||||||
|
deny = [
|
||||||
|
{ name = "native-tls" },
|
||||||
|
{ name = "openssl" },
|
||||||
|
{ name = "openssl-sys" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[licenses]
|
||||||
|
unlicensed = "deny"
|
||||||
|
copyleft = "allow"
|
||||||
|
allow = [
|
||||||
|
"Apache-2.0",
|
||||||
|
"BSD-2-Clause",
|
||||||
|
"BSD-3-Clause",
|
||||||
|
"CC0-1.0",
|
||||||
|
"GPL-2.0-only",
|
||||||
|
"ISC",
|
||||||
|
"MIT",
|
||||||
|
"MPL-2.0",
|
||||||
|
"Unicode-3.0",
|
||||||
|
"Zlib",
|
||||||
|
]
|
||||||
|
|
||||||
|
[sources]
|
||||||
|
unknown-registry = "deny"
|
||||||
|
unknown-git = "deny"
|
||||||
|
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||||
@@ -11,11 +11,11 @@ S0-ARCH-003 covered cargo xtask policy rejects platform/render adapter dependenc
|
|||||||
S0-ARCH-004 covered cargo xtask policy scans workspace-owned Rust/TOML for unsafe constructs and workspace lints forbid unsafe_code
|
S0-ARCH-004 covered cargo xtask policy scans workspace-owned Rust/TOML for unsafe constructs and workspace lints forbid unsafe_code
|
||||||
S0-ARCH-005 covered cargo xtask policy rejects Python source files, Python shebangs, and Python CI workflow steps while allowing docs requirements.txt
|
S0-ARCH-005 covered cargo xtask policy rejects Python source files, Python shebangs, and Python CI workflow steps while allowing docs requirements.txt
|
||||||
S0-ARCH-006 covered cargo xtask policy rejects non-fparkan package directories under crates/
|
S0-ARCH-006 covered cargo xtask policy rejects non-fparkan package directories under crates/
|
||||||
S0-ARCH-007 covered cargo xtask ci runs fmt, policy, workspace test, clippy, rustdoc warnings, cargo-deny or built-in supply-chain fallback, and strict acceptance audit
|
S0-ARCH-007 covered cargo xtask ci runs fmt, policy, workspace test, clippy, rustdoc warnings, cargo-deny with reviewed deny.toml, and strict acceptance audit; built-in supply-chain fallback is opt-in local-only and forbidden when CI is set
|
||||||
S0-ARCH-008 covered cargo xtask policy rejects moving Rust toolchains and workspace rust-version drift
|
S0-ARCH-008 covered cargo xtask policy rejects moving Rust toolchains and workspace rust-version drift
|
||||||
S0-ARCH-009 covered .github/workflows/ci.yml runs a pinned MSRV backend-neutral crate job
|
S0-ARCH-009 covered .github/workflows/ci.yml runs a pinned MSRV backend-neutral crate job
|
||||||
S0-ARCH-010 covered cargo xtask acceptance audit emits commit_sha, rust_toolchain, and msrv metadata into the JSON artifact
|
S0-ARCH-010 covered cargo xtask acceptance audit emits measured commit_sha, git_dirty, runner_identity, rust_toolchain, and msrv metadata into the JSON artifact
|
||||||
S0-ARCH-011 blocked cargo run -p fparkan-vulkan-smoke emits explicit per-platform blocked artifacts until real Vulkan 300-frame validation=0 runner is available
|
S0-ARCH-011 covered .github/workflows/ci.yml runs cargo run -p fparkan-vulkan-smoke --locked -- --out target/fparkan/native-smoke/macos.json and cargo xtask native-smoke audit enforces a passed macOS 300-frame report with measured resize/recreate, validation=0, clean git provenance, exact commit SHA shape, and a platform-consistent target triple
|
||||||
S0-DIAG-001 covered cargo test -p fparkan-diagnostics --offline diagnostic_chain_preserves_context
|
S0-DIAG-001 covered cargo test -p fparkan-diagnostics --offline diagnostic_chain_preserves_context
|
||||||
S0-DIAG-002 covered cargo test -p fparkan-diagnostics --offline json_is_stable
|
S0-DIAG-002 covered cargo test -p fparkan-diagnostics --offline json_is_stable
|
||||||
S0-CORPUS-001 covered cargo test -p fparkan-corpus --offline deterministic_traversal_is_creation_order_independent
|
S0-CORPUS-001 covered cargo test -p fparkan-corpus --offline deterministic_traversal_is_creation_order_independent
|
||||||
@@ -30,7 +30,7 @@ S0-PLAT-001 covered cargo test -p fparkan-platform-winit --offline window_port_r
|
|||||||
S0-PLAT-002 covered cargo clippy -p fparkan-platform -p fparkan-platform-winit --all-targets --all-features --locked -- -D warnings
|
S0-PLAT-002 covered cargo clippy -p fparkan-platform -p fparkan-platform-winit --all-targets --all-features --locked -- -D warnings
|
||||||
S0-PLAT-003 covered cargo test -p fparkan-platform-winit --offline smoke_window_plan_requires_native_handles_and_nonzero_extent smoke_window_plan_rejects_zero_extent
|
S0-PLAT-003 covered cargo test -p fparkan-platform-winit --offline smoke_window_plan_requires_native_handles_and_nonzero_extent smoke_window_plan_rejects_zero_extent
|
||||||
S0-PLAT-004 covered cargo test -p fparkan-platform-winit --offline smoke_window_app_requires_created_native_window smoke_window_app_rejects_synthetic_window_without_native_handles
|
S0-PLAT-004 covered cargo test -p fparkan-platform-winit --offline smoke_window_app_requires_created_native_window smoke_window_app_rejects_synthetic_window_without_native_handles
|
||||||
S0-VK-001 covered cargo test -p fparkan-render-vulkan --offline backend_tracks_render_request_and_presents
|
S0-VK-001 covered cargo test -p fparkan-render-vulkan --offline planning_backend_tracks_render_request_and_simulated_present
|
||||||
S0-VK-002 covered cargo test -p fparkan-render-vulkan --offline device_scoring_is_deterministic_and_prefers_discrete_unified_queue
|
S0-VK-002 covered cargo test -p fparkan-render-vulkan --offline device_scoring_is_deterministic_and_prefers_discrete_unified_queue
|
||||||
S0-VK-003 covered cargo test -p fparkan-render-vulkan --offline portability_subset_is_reported_and_enabled_when_exposed
|
S0-VK-003 covered cargo test -p fparkan-render-vulkan --offline portability_subset_is_reported_and_enabled_when_exposed
|
||||||
S0-VK-004 covered cargo test -p fparkan-render-vulkan --offline rejects_missing_graphics_present_swapchain_and_format
|
S0-VK-004 covered cargo test -p fparkan-render-vulkan --offline rejects_missing_graphics_present_swapchain_and_format
|
||||||
@@ -47,23 +47,23 @@ S0-VK-014 covered cargo test -p fparkan-render-vulkan --offline swapchain_plan_p
|
|||||||
S0-VK-015 covered cargo test -p fparkan-render-vulkan --offline swapchain_plan_uses_fifo_and_current_extent_fallbacks
|
S0-VK-015 covered cargo test -p fparkan-render-vulkan --offline swapchain_plan_uses_fifo_and_current_extent_fallbacks
|
||||||
S0-VK-016 covered cargo test -p fparkan-render-vulkan --offline swapchain_plan_rejects_missing_surface_data_and_empty_extent
|
S0-VK-016 covered cargo test -p fparkan-render-vulkan --offline swapchain_plan_rejects_missing_surface_data_and_empty_extent
|
||||||
S0-VK-017 covered cargo test -p fparkan-render-vulkan --offline swapchain_plan_json_and_recreation_reports_are_stable
|
S0-VK-017 covered cargo test -p fparkan-render-vulkan --offline swapchain_plan_json_and_recreation_reports_are_stable
|
||||||
S0-VK-018 covered cargo test -p fparkan-render-vulkan --offline triangle_shader_manifest_hashes_are_stable
|
S0-VK-018 covered cargo test -p fparkan-render-vulkan --offline triangle_shader_manifest_hashes_are_stable checked_in_shader_manifest_matches_generated_report
|
||||||
S0-VK-019 covered cargo test -p fparkan-render-vulkan --offline shader_manifest_report_json_is_stable
|
S0-VK-019 covered cargo test -p fparkan-render-vulkan --offline shader_manifest_report_json_is_stable
|
||||||
S0-VK-020 covered cargo test -p fparkan-render-vulkan --offline shader_manifest_rejects_invalid_spirv_containers
|
S0-VK-020 covered cargo test -p fparkan-render-vulkan --offline shader_manifest_rejects_invalid_spirv_containers
|
||||||
S0-VK-021 covered cargo test -p fparkan-render-vulkan --offline frame_submission_plan_json_is_stable
|
S0-VK-021 covered cargo test -p fparkan-render-vulkan --offline frame_submission_plan_json_is_stable
|
||||||
S0-VK-022 covered cargo test -p fparkan-render-vulkan --offline backend_tracks_render_request_and_presents
|
S0-VK-022 covered cargo test -p fparkan-render-vulkan --offline planning_backend_tracks_render_request_and_simulated_present
|
||||||
S0-VK-023 covered cargo test -p fparkan-vulkan-smoke --offline rejects_false_pass_without_full_evidence blocked_report_includes_shader_manifest_and_bootstrap_status
|
S0-VK-023 covered cargo test -p fparkan-vulkan-smoke --offline rejects_false_pass_without_full_evidence blocked_report_includes_shader_manifest_and_bootstrap_status
|
||||||
S0-VK-024 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_loader_probe formats_vulkan_api_version
|
S0-VK-024 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_loader_probe formats_vulkan_api_version
|
||||||
S0-VK-025 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_instance_probe parses_instance_probe_as_loader_probe
|
S0-VK-025 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_instance_probe parses_instance_probe_as_loader_probe
|
||||||
S0-VK-026 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_window_probe rejects_passed_without_surface_probe parses_surface_probe_as_instance_probe
|
S0-VK-026 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_window_probe rejects_passed_without_surface_probe parses_surface_probe_as_instance_probe
|
||||||
S0-VK-027 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_swapchain_recreation blocked_report_includes_shader_manifest_and_bootstrap_status
|
S0-VK-027 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_swapchain_recreation blocked_report_includes_shader_manifest_and_bootstrap_status
|
||||||
S0-VK-028 covered cargo test -p fparkan-vulkan-smoke --offline reports_rustc_host_triple blocked_report_includes_shader_manifest_and_bootstrap_status
|
S0-VK-028 covered cargo test -p fparkan-vulkan-smoke --offline reports_rustc_host_triple blocked_report_includes_shader_manifest_and_bootstrap_status
|
||||||
S0-VK-029 covered cargo test -p xtask --offline native_smoke_audit_accepts_complete_three_platform_pass native_smoke_audit_rejects_blocked_or_incomplete_reports
|
S0-VK-029 covered cargo test -p xtask --offline native_smoke_audit_accepts_complete_required_platform_pass native_smoke_audit_rejects_blocked_or_incomplete_reports
|
||||||
S0-VK-030 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_with_failed_surface
|
S0-VK-030 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_with_failed_surface
|
||||||
S0-VK-031 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_selected_device
|
S0-VK-031 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_selected_device
|
||||||
S0-VK-032 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_created_swapchain
|
S0-VK-032 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_created_swapchain
|
||||||
S0-VK-033 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_created_logical_device
|
S0-VK-033 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_created_logical_device
|
||||||
S0-VK-034 covered cargo test -p xtask --offline native_smoke_audit_accepts_complete_three_platform_pass native_smoke_audit_rejects_blocked_or_incomplete_reports
|
S0-VK-034 covered cargo test -p xtask --offline native_smoke_audit_accepts_complete_required_platform_pass native_smoke_audit_rejects_blocked_or_incomplete_reports
|
||||||
S0-LIMIT-001 covered cargo test -p fparkan-binary --offline rejects_count_stride_overflow
|
S0-LIMIT-001 covered cargo test -p fparkan-binary --offline rejects_count_stride_overflow
|
||||||
S0-LIMIT-002 covered cargo test -p fparkan-binary --offline rejects_oversized_declared_allocation_before_read
|
S0-LIMIT-002 covered cargo test -p fparkan-binary --offline rejects_oversized_declared_allocation_before_read
|
||||||
L1-P1-NRES-001 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
|
L1-P1-NRES-001 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
|
||||||
|
|||||||
|
@@ -0,0 +1,68 @@
|
|||||||
|
# Stage 0 acceptance IDs
|
||||||
|
|
||||||
|
`L0-COPYRIGHT-001`
|
||||||
|
`L0-P1-001`
|
||||||
|
`L0-P1-002`
|
||||||
|
`L0-P2-001`
|
||||||
|
`L0-P2-002`
|
||||||
|
`S0-ARCH-001`
|
||||||
|
`S0-ARCH-002`
|
||||||
|
`S0-ARCH-003`
|
||||||
|
`S0-ARCH-004`
|
||||||
|
`S0-ARCH-005`
|
||||||
|
`S0-ARCH-006`
|
||||||
|
`S0-ARCH-007`
|
||||||
|
`S0-ARCH-008`
|
||||||
|
`S0-ARCH-009`
|
||||||
|
`S0-ARCH-010`
|
||||||
|
`S0-ARCH-011`
|
||||||
|
`S0-DIAG-001`
|
||||||
|
`S0-DIAG-002`
|
||||||
|
`S0-CORPUS-001`
|
||||||
|
`S0-CORPUS-002`
|
||||||
|
`S0-CORPUS-003`
|
||||||
|
`S0-CORPUS-004`
|
||||||
|
`S0-CORPUS-005`
|
||||||
|
`S0-CORPUS-006`
|
||||||
|
`S0-CLI-001`
|
||||||
|
`S0-CLI-002`
|
||||||
|
`S0-PLAT-001`
|
||||||
|
`S0-PLAT-002`
|
||||||
|
`S0-PLAT-003`
|
||||||
|
`S0-PLAT-004`
|
||||||
|
`S0-VK-001`
|
||||||
|
`S0-VK-002`
|
||||||
|
`S0-VK-003`
|
||||||
|
`S0-VK-004`
|
||||||
|
`S0-VK-005`
|
||||||
|
`S0-VK-006`
|
||||||
|
`S0-VK-007`
|
||||||
|
`S0-VK-008`
|
||||||
|
`S0-VK-009`
|
||||||
|
`S0-VK-010`
|
||||||
|
`S0-VK-011`
|
||||||
|
`S0-VK-012`
|
||||||
|
`S0-VK-013`
|
||||||
|
`S0-VK-014`
|
||||||
|
`S0-VK-015`
|
||||||
|
`S0-VK-016`
|
||||||
|
`S0-VK-017`
|
||||||
|
`S0-VK-018`
|
||||||
|
`S0-VK-019`
|
||||||
|
`S0-VK-020`
|
||||||
|
`S0-VK-021`
|
||||||
|
`S0-VK-022`
|
||||||
|
`S0-VK-023`
|
||||||
|
`S0-VK-024`
|
||||||
|
`S0-VK-025`
|
||||||
|
`S0-VK-026`
|
||||||
|
`S0-VK-027`
|
||||||
|
`S0-VK-028`
|
||||||
|
`S0-VK-029`
|
||||||
|
`S0-VK-030`
|
||||||
|
`S0-VK-031`
|
||||||
|
`S0-VK-032`
|
||||||
|
`S0-VK-033`
|
||||||
|
`S0-VK-034`
|
||||||
|
`S0-LIMIT-001`
|
||||||
|
`S0-LIMIT-002`
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
schema = 1
|
||||||
|
|
||||||
|
[stages]
|
||||||
|
"0" = [
|
||||||
|
"fparkan-binary",
|
||||||
|
"fparkan-corpus",
|
||||||
|
"fparkan-diagnostics",
|
||||||
|
"fparkan-platform",
|
||||||
|
"fparkan-platform-winit",
|
||||||
|
"fparkan-render",
|
||||||
|
"fparkan-render-vulkan",
|
||||||
|
"fparkan-test-support",
|
||||||
|
"fparkan-vulkan-smoke",
|
||||||
|
"xtask",
|
||||||
|
]
|
||||||
|
"1" = [
|
||||||
|
"fparkan-cli",
|
||||||
|
"fparkan-inspection",
|
||||||
|
"fparkan-nres",
|
||||||
|
"fparkan-path",
|
||||||
|
"fparkan-resource",
|
||||||
|
"fparkan-rsli",
|
||||||
|
"fparkan-vfs",
|
||||||
|
]
|
||||||
|
"2" = [
|
||||||
|
"fparkan-prototype",
|
||||||
|
]
|
||||||
|
"3" = [
|
||||||
|
"fparkan-assets",
|
||||||
|
"fparkan-material",
|
||||||
|
"fparkan-msh",
|
||||||
|
"fparkan-texm",
|
||||||
|
"fparkan-viewer",
|
||||||
|
]
|
||||||
|
"4" = [
|
||||||
|
"fparkan-animation",
|
||||||
|
"fparkan-fx",
|
||||||
|
]
|
||||||
|
"5" = [
|
||||||
|
"fparkan-game",
|
||||||
|
"fparkan-headless",
|
||||||
|
"fparkan-mission-format",
|
||||||
|
"fparkan-runtime",
|
||||||
|
"fparkan-terrain",
|
||||||
|
"fparkan-terrain-format",
|
||||||
|
"fparkan-world",
|
||||||
|
]
|
||||||
@@ -1,643 +0,0 @@
|
|||||||
# FParkan — аудит Stage 0 и план полного закрытия
|
|
||||||
|
|
||||||
**Проект:** `valentineus/fparkan`
|
|
||||||
**Проверенная ветка:** `devel` GitHub-зеркала
|
|
||||||
**Дата аудита:** 23 июня 2026 года
|
|
||||||
**Область:** только Stage 0 — Governance, reproducibility и Vulkan foundation
|
|
||||||
**Метод:** статический архитектурный и кодовый аудит
|
|
||||||
**Сборка и исполнение:** не выполнялись; `cargo build`, `cargo test`, Vulkan smoke и validation jobs не запускались
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Итоговый вердикт
|
|
||||||
|
|
||||||
**Stage 0 не закрыт и находится в статусе `BLOCKED`.**
|
|
||||||
|
|
||||||
Главный критерий Stage 0 — воспроизводимый репозиторий и минимальный настоящий Vulkan vertical slice на Windows, Linux и macOS. В проверенном состоянии:
|
|
||||||
|
|
||||||
- отсутствует `fparkan-platform-winit`;
|
|
||||||
- отсутствует `fparkan-render-vulkan`;
|
|
||||||
- отсутствуют Vulkan instance/device/surface/swapchain;
|
|
||||||
- `fparkan-game` использует `RecordingBackend`, а не GPU backend;
|
|
||||||
- workspace по-прежнему содержит SDL/OpenGL stub adapters;
|
|
||||||
- Rust toolchain закреплён только как изменяемый канал `stable`;
|
|
||||||
- `cargo xtask ci` не реализует полный канонический gate;
|
|
||||||
- нет подтверждённых артефактов Windows/Linux/macOS smoke jobs.
|
|
||||||
|
|
||||||
### Сводная оценка
|
|
||||||
|
|
||||||
| Группа требований | Статус | Основной блокер |
|
|
||||||
|---|---|---|
|
|
||||||
| Reproducibility и toolchain | **FAIL** | Toolchain не закреплён точной версией, MSRV не объявлен |
|
|
||||||
| Repository policy и CI | **FAIL** | Неполные fmt/test/clippy/doc/security gates |
|
|
||||||
| Platform abstraction | **FAIL** | Core API содержит OpenGL-specific contract; `winit` adapter отсутствует |
|
|
||||||
| Vulkan backend | **FAIL** | Нет Vulkan loader/device/surface/swapchain/pipeline |
|
|
||||||
| macOS portability | **FAIL** | Нет MoltenVK integration и portability handling |
|
|
||||||
| Offline shaders | **FAIL** | Нет SPIR-V build/validation/hash pipeline |
|
|
||||||
| Legacy cleanup | **FAIL** | SDL/GL stubs остаются workspace members |
|
|
||||||
| Headless isolation | **PASS на manifest-level** | Автоматическое доказательство dependency closure ещё требуется |
|
|
||||||
| Native acceptance | **FAIL / NOT RUNNABLE** | Нет реального backend и platform artifacts |
|
|
||||||
|
|
||||||
Stage 0 можно объявить закрытым только после прохождения реального Vulkan smoke на всех трёх системах и публикации machine-readable артефактов.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Область и ограничения аудита
|
|
||||||
|
|
||||||
Канонические требования взяты из документа:
|
|
||||||
|
|
||||||
- «План реализации stage 0–5: Vulkan revision»;
|
|
||||||
- <https://app.notion.com/p/387e79f2db3981778f94cdf34db5f93f>.
|
|
||||||
|
|
||||||
Проверялась ветка:
|
|
||||||
|
|
||||||
- <https://github.com/valentineus/fparkan/tree/devel>.
|
|
||||||
|
|
||||||
Ограничения:
|
|
||||||
|
|
||||||
1. Ветка `devel` является движущейся ссылкой. Следующий formal audit следует выполнять на закреплённом commit SHA или tag.
|
|
||||||
2. README указывает self-hosted repository как primary. Его закрытые CI runners и artifacts не были доступны.
|
|
||||||
3. Код не собирался и не запускался по условию аудита.
|
|
||||||
4. Vulkan runtime, validation layers, MoltenVK и native window creation не проверялись динамически.
|
|
||||||
5. Статический анализ достаточен для определения текущих архитектурных блокеров: требуемых adapters и зависимостей в workspace нет.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Матрица требований Stage 0
|
|
||||||
|
|
||||||
| Требование | Статус | Текущее состояние | Необходимо для закрытия |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Exact stable Rust toolchain | **FAIL** | `rust-toolchain.toml`: `channel = "stable"` | Закрепить точную версию, например `1.xx.y` |
|
|
||||||
| Объявленный MSRV | **FAIL** | `workspace.package.rust-version` отсутствует | Добавить `rust-version` и отдельный MSRV job |
|
|
||||||
| Полный `cargo xtask ci` | **FAIL** | Есть custom rustfmt, policy, workspace test и clippy | Добавить канонические fmt/test/clippy/doc/security gates |
|
|
||||||
| `--all-targets --all-features` | **FAIL** | Не используются текущим `ci` | Добавить к test/clippy/doc gates |
|
|
||||||
| Clippy `-D warnings` | **FAIL** | Явно не передаётся | Сделать предупреждения blocking |
|
|
||||||
| Rustdoc broken-link gate | **FAIL** | Отсутствует | Добавить `RUSTDOCFLAGS=-D warnings -D rustdoc::broken_intra_doc_links` |
|
|
||||||
| License/advisory/source policy | **PARTIAL / UNVERIFIED** | Есть custom policy и GPL workspace license | Подключить `cargo-deny` или эквивалент и хранить versioned policy |
|
|
||||||
| Typed TOML parsing | **FAIL** | Licensed manifest разбирается вручную построчно | `serde` + TOML schema + `deny_unknown_fields` |
|
|
||||||
| `cargo_metadata` policy | **FAIL** | Dependency rules не опираются на typed Cargo graph | Добавить `cargo_metadata` и package-ID based checks |
|
|
||||||
| CI matrix Windows/Linux/macOS | **UNVERIFIED / BLOCKER** | Доступных platform artifacts нет | Создать native matrix и сохранять reports |
|
|
||||||
| Backend-neutral platform API | **FAIL** | В core есть `GraphicsProfile`, GL/GLES versions и `WindowPort::present()` | Удалить GL context concepts; present перенести в renderer |
|
|
||||||
| `fparkan-platform-winit` | **FAIL** | В workspace только SDL-named stub | Реализовать настоящий event loop/window adapter |
|
|
||||||
| `fparkan-render-vulkan` | **FAIL** | В workspace только GL-named recording stub | Реализовать настоящий Vulkan backend |
|
|
||||||
| Vulkan loader/instance/device | **FAIL** | Vulkan bindings отсутствуют | Добавить `ash`, instance, device selection, queues |
|
|
||||||
| Surface/swapchain/present | **FAIL** | Отсутствуют | Реализовать platform surface и swapchain lifecycle |
|
|
||||||
| Indexed triangle | **FAIL** | Есть только command capture | Нарисовать реальный indexed triangle |
|
|
||||||
| Resize/out-of-date/suboptimal | **FAIL** | Swapchain отсутствует | Реализовать полную recreation policy |
|
|
||||||
| Deterministic capability report | **FAIL** | Device discovery отсутствует | Pure scoring policy + JSON capability report |
|
|
||||||
| macOS portability | **FAIL** | MoltenVK integration отсутствует | Portability enumeration, subset и packaged MoltenVK |
|
|
||||||
| Offline SPIR-V pipeline | **FAIL** | GL stub проверяет только synthetic markers | Pinned compiler, validator, descriptor manifest и hashes |
|
|
||||||
| Legacy adapter removal | **FAIL** | SDL/GL crates входят в workspace | Удалить crates и все references после замены |
|
|
||||||
| Game/viewer composition | **FAIL** | Game использует `RecordingBackend`; viewer — CLI inspector | Подключить winit + Vulkan только в composition roots |
|
|
||||||
| Headless isolation | **PASS на manifest-level** | Нет window/Vulkan dependency | Добавить automated Cargo metadata assertion |
|
|
||||||
| 300 frames + resize + validation=0 | **FAIL** | Невозможно выполнить без backend | Native smoke jobs на трёх OS |
|
|
||||||
| Negative Vulkan tests | **FAIL** | Нет Vulkan error model | Loader/device/queue/format failure fixtures |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Замечания
|
|
||||||
|
|
||||||
### S0-B01 — Workspace содержит удаляемые SDL/OpenGL stub crates
|
|
||||||
|
|
||||||
**Приоритет:** BLOCKER
|
|
||||||
**Файлы:** `Cargo.toml`, `adapters/fparkan-platform-sdl`, `adapters/fparkan-render-gl`
|
|
||||||
|
|
||||||
Root workspace включает оба прежних adapter crate. При этом:
|
|
||||||
|
|
||||||
- SDL adapter не зависит от SDL и содержит in-memory stubs;
|
|
||||||
- GL adapter не зависит от OpenGL и только сохраняет canonical command captures;
|
|
||||||
- их tests доказывают deterministic stub behavior, а не platform/GPU integration.
|
|
||||||
|
|
||||||
Это создаёт ложноположительный сигнал готовности backend-а.
|
|
||||||
|
|
||||||
**Рекомендация:**
|
|
||||||
|
|
||||||
1. До появления замены пометить crates как `legacy-proof` и исключить из default production composition.
|
|
||||||
2. Добавить policy, запрещающий приложениям зависеть от них.
|
|
||||||
3. После подключения `platform-winit` и `render-vulkan` удалить crates, lockfile references, docs и tests.
|
|
||||||
|
|
||||||
### S0-B02 — Core platform contract остаётся OpenGL-specific
|
|
||||||
|
|
||||||
**Приоритет:** BLOCKER
|
|
||||||
**Файл:** `crates/fparkan-platform/src/lib.rs`
|
|
||||||
|
|
||||||
Проблемы:
|
|
||||||
|
|
||||||
- `GraphicsProfile::DesktopCore/Embedded` описывает GL/GLES profile;
|
|
||||||
- `GraphicsContextRequest` описывает создание GL context;
|
|
||||||
- `WindowPort::present()` ошибочно закрепляет presentation за window abstraction;
|
|
||||||
- `PlatformEvent` содержит только `Quit`;
|
|
||||||
- отсутствуют resize, scale factor, focus, keyboard, mouse, suspend/resume и raw handles;
|
|
||||||
- `PlatformError::Backend` не содержит source/context.
|
|
||||||
|
|
||||||
Для Vulkan окно не выполняет present. Surface, swapchain, image acquisition и queue presentation принадлежат render adapter.
|
|
||||||
|
|
||||||
**Рекомендация:** platform crate должен предоставлять только:
|
|
||||||
|
|
||||||
- event/lifecycle model;
|
|
||||||
- physical и logical size;
|
|
||||||
- scale factor;
|
|
||||||
- normalized input;
|
|
||||||
- raw window/display handles;
|
|
||||||
- structured platform errors.
|
|
||||||
|
|
||||||
### S0-B03 — Реального Vulkan code path нет
|
|
||||||
|
|
||||||
**Приоритет:** BLOCKER
|
|
||||||
|
|
||||||
В inspected manifests отсутствуют `ash`, `ash-window`, `winit` и `raw-window-handle`. Следовательно, текущий код не может создать Vulkan instance/device/surface/swapchain.
|
|
||||||
|
|
||||||
`fparkan-game` выполняет backend-neutral capture через `RecordingBackend`. Это полезный CPU oracle, но не Vulkan renderer.
|
|
||||||
|
|
||||||
**Definition of fixed:** отдельный smoke executable открывает окно, создаёт Vulkan swapchain, рисует indexed triangle, обрабатывает resize и корректно завершается.
|
|
||||||
|
|
||||||
### S0-B04 — `cargo xtask ci` не соответствует exit gate
|
|
||||||
|
|
||||||
**Приоритет:** BLOCKER
|
|
||||||
**Файл:** `xtask/src/main.rs`
|
|
||||||
|
|
||||||
Текущий gate не подтверждает:
|
|
||||||
|
|
||||||
- все targets и features;
|
|
||||||
- clippy с `-D warnings`;
|
|
||||||
- rustdoc warnings и broken links;
|
|
||||||
- advisory/source policy;
|
|
||||||
- dependency denylist;
|
|
||||||
- отсутствие project-owned unsafe вне разрешённого Vulkan boundary;
|
|
||||||
- корректность typed acceptance manifests;
|
|
||||||
- platform-native smoke jobs.
|
|
||||||
|
|
||||||
Custom recursive rustfmt также может расходиться с canonical `cargo fmt --all -- --check`.
|
|
||||||
|
|
||||||
### S0-B05 — Toolchain не воспроизводим
|
|
||||||
|
|
||||||
**Приоритет:** BLOCKER
|
|
||||||
**Файл:** `rust-toolchain.toml`
|
|
||||||
|
|
||||||
Канал `stable` изменяется. Один и тот же commit может использовать разные компиляторы в разные дни. MSRV также не объявлен.
|
|
||||||
|
|
||||||
**Рекомендация:**
|
|
||||||
|
|
||||||
- закрепить точный Rust release;
|
|
||||||
- указать `rust-version`;
|
|
||||||
- обновлять toolchain отдельным reviewed PR;
|
|
||||||
- сохранять toolchain и SDK versions в acceptance report.
|
|
||||||
|
|
||||||
### S0-H01 — Нужен изолированный audited unsafe boundary
|
|
||||||
|
|
||||||
**Приоритет:** HIGH
|
|
||||||
|
|
||||||
`unsafe_code = "forbid"` правильно сохранять для backend-neutral crates. Однако Vulkan FFI требует локальных unsafe calls.
|
|
||||||
|
|
||||||
Нельзя ослаблять policy всему workspace.
|
|
||||||
|
|
||||||
**Целевая схема:**
|
|
||||||
|
|
||||||
- unsafe разрешён только в `fparkan-render-vulkan` low-level modules;
|
|
||||||
- `unsafe_op_in_unsafe_fn = deny`;
|
|
||||||
- каждый block имеет `// SAFETY:` comment;
|
|
||||||
- ownership/lifetime rules документированы;
|
|
||||||
- raw Vulkan handles не выходят в public neutral API;
|
|
||||||
- custom policy scanner проверяет allowlist.
|
|
||||||
|
|
||||||
### S0-H02 — Neutral render IDs не должны быть GPU allocation IDs
|
|
||||||
|
|
||||||
**Приоритет:** HIGH, не блокирует первый hardcoded triangle
|
|
||||||
**Файл:** `crates/fparkan-render/src/lib.rs`
|
|
||||||
|
|
||||||
`GpuMeshId` и `GpuMaterialId` появляются до существования GPU registry. Это смешивает CPU asset identity и backend-local allocation identity.
|
|
||||||
|
|
||||||
**Рекомендация:** использовать neutral `MeshAssetId`/`MaterialAssetId`; Vulkan adapter должен самостоятельно отображать их на buffers, images и descriptors.
|
|
||||||
|
|
||||||
### S0-M01 — Документация рассогласована с Vulkan revision
|
|
||||||
|
|
||||||
**Приоритет:** MEDIUM
|
|
||||||
|
|
||||||
`docs/tomes/07-implementation.md` сохраняет старую последовательность и multi-backend формулировки. Parity documentation ссылается на отсутствующий workspace crate, а active parity cases не определены.
|
|
||||||
|
|
||||||
**Рекомендация:** один versioned source of truth для stages и автоматическая проверка упомянутых crates, commands и backend names.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Сильные стороны, которые следует сохранить
|
|
||||||
|
|
||||||
- Workspace lint policy строгая и подходит для backend-neutral crates.
|
|
||||||
- `Cargo.lock` присутствует, а команды используют `--locked`.
|
|
||||||
- Synthetic и licensed corpus paths концептуально разделены.
|
|
||||||
- `fparkan-headless` не зависит от platform/render adapters на manifest-level.
|
|
||||||
- `fparkan-render` уже предоставляет deterministic command ordering, validation и canonical capture.
|
|
||||||
- Composition roots отделены от большинства core crates.
|
|
||||||
|
|
||||||
Эти элементы позволяют построить Vulkan foundation без переписывания CPU/data foundation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Целевая архитектура Stage 0
|
|
||||||
|
|
||||||
```text
|
|
||||||
apps/fparkan-game, apps/fparkan-viewer
|
|
||||||
│
|
|
||||||
├── fparkan-platform-winit
|
|
||||||
│ └── winit + raw-window-handle
|
|
||||||
│
|
|
||||||
└── fparkan-render-vulkan
|
|
||||||
├── ash-window
|
|
||||||
├── ash
|
|
||||||
├── surface / swapchain
|
|
||||||
├── device / queues
|
|
||||||
├── shaders / pipelines
|
|
||||||
└── synchronization / presentation
|
|
||||||
|
|
||||||
apps/fparkan-headless
|
|
||||||
└── runtime/core only
|
|
||||||
no winit, ash, MoltenVK or window dependencies
|
|
||||||
```
|
|
||||||
|
|
||||||
Разделение ответственности:
|
|
||||||
|
|
||||||
- `fparkan-platform`: события, input, lifecycle, sizes и handle access;
|
|
||||||
- `fparkan-platform-winit`: concrete window/event-loop implementation;
|
|
||||||
- `fparkan-render`: backend-neutral command/snapshot contracts;
|
|
||||||
- `fparkan-render-vulkan`: Vulkan resources, synchronization и present;
|
|
||||||
- game/viewer: composition root;
|
|
||||||
- headless: полностью изолированный путь.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. План полного закрытия Stage 0
|
|
||||||
|
|
||||||
Порядок PR важен. Vulkan adapter не следует строить поверх текущего GL-oriented platform contract.
|
|
||||||
|
|
||||||
### PR S0-01 — Reproducible toolchain и metadata
|
|
||||||
|
|
||||||
**Изменения**
|
|
||||||
|
|
||||||
- закрепить exact Rust toolchain;
|
|
||||||
- добавить `workspace.package.rust-version`;
|
|
||||||
- зафиксировать supported triples;
|
|
||||||
- добавить `cargo xtask doctor`;
|
|
||||||
- включать commit SHA, Rust version и platform SDK versions в reports.
|
|
||||||
|
|
||||||
**Acceptance**
|
|
||||||
|
|
||||||
- clean checkout формирует одинаковый metadata report;
|
|
||||||
- MSRV job собирает backend-neutral crates;
|
|
||||||
- pinned toolchain проходит полный synthetic gate.
|
|
||||||
|
|
||||||
### PR S0-02 — Typed xtask configuration
|
|
||||||
|
|
||||||
**Изменения**
|
|
||||||
|
|
||||||
- `serde` + TOML schemas для corpus/acceptance manifests;
|
|
||||||
- `deny_unknown_fields`;
|
|
||||||
- duplicate/missing/unknown-field validation;
|
|
||||||
- absolute canonical paths для local licensed manifest;
|
|
||||||
- `cargo_metadata` для dependency и workspace policy;
|
|
||||||
- удалить ручной line parser.
|
|
||||||
|
|
||||||
**Acceptance**
|
|
||||||
|
|
||||||
- malformed manifest всегда даёт non-zero exit;
|
|
||||||
- неизвестные поля не игнорируются;
|
|
||||||
- dependency policy работает по Cargo package IDs, targets и features.
|
|
||||||
|
|
||||||
### PR S0-03 — Полный synthetic CI gate
|
|
||||||
|
|
||||||
Обязательные команды:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo fmt --all -- --check
|
|
||||||
cargo test --workspace --all-targets --all-features --locked
|
|
||||||
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
|
|
||||||
RUSTDOCFLAGS="-D warnings -D rustdoc::broken_intra_doc_links" \
|
|
||||||
cargo doc --workspace --no-deps --all-features --locked
|
|
||||||
cargo deny check advisories bans licenses sources
|
|
||||||
cargo xtask policy
|
|
||||||
cargo xtask acceptance audit --strict
|
|
||||||
```
|
|
||||||
|
|
||||||
Добавить reports для каждого gate и запрет silent skip.
|
|
||||||
|
|
||||||
### PR S0-04 — Redesign `fparkan-platform`
|
|
||||||
|
|
||||||
**Изменения**
|
|
||||||
|
|
||||||
- удалить `GraphicsProfile`, `GraphicsContextRequest` и GL version negotiation;
|
|
||||||
- убрать `present()` из window port;
|
|
||||||
- добавить normalized keyboard/mouse events;
|
|
||||||
- physical/logical size и scale factor;
|
|
||||||
- focus, minimize, occlusion, suspend/resume;
|
|
||||||
- deterministic lifecycle state machine;
|
|
||||||
- structured errors с source chain.
|
|
||||||
|
|
||||||
**Synthetic tests**
|
|
||||||
|
|
||||||
- resize coalescing;
|
|
||||||
- zero-size/minimized window;
|
|
||||||
- scale-factor changes;
|
|
||||||
- focus loss clears held input;
|
|
||||||
- key repeat и modifiers;
|
|
||||||
- suspend/resume;
|
|
||||||
- deterministic event ordering.
|
|
||||||
|
|
||||||
### PR S0-05 — `fparkan-platform-winit`
|
|
||||||
|
|
||||||
**Изменения**
|
|
||||||
|
|
||||||
- winit event loop;
|
|
||||||
- native window lifecycle;
|
|
||||||
- raw window/display handles;
|
|
||||||
- platform-specific event normalization;
|
|
||||||
- отсутствие GPU ownership.
|
|
||||||
|
|
||||||
**Acceptance**
|
|
||||||
|
|
||||||
- window-only smoke на Windows, Linux и macOS;
|
|
||||||
- native event trace соответствует synthetic model.
|
|
||||||
|
|
||||||
### PR S0-06 — Vulkan low-level boundary
|
|
||||||
|
|
||||||
**Изменения**
|
|
||||||
|
|
||||||
- `ash` и `ash-window`;
|
|
||||||
- dynamic Vulkan loader;
|
|
||||||
- instance и debug messenger;
|
|
||||||
- physical device capability records;
|
|
||||||
- pure deterministic device scoring;
|
|
||||||
- graphics/present queue selection;
|
|
||||||
- deterministic capability JSON;
|
|
||||||
- audited unsafe allowlist.
|
|
||||||
|
|
||||||
**Negative tests**
|
|
||||||
|
|
||||||
- loader отсутствует;
|
|
||||||
- Vulkan 1.1 недоступен;
|
|
||||||
- graphics queue отсутствует;
|
|
||||||
- present queue отсутствует;
|
|
||||||
- `VK_KHR_swapchain` отсутствует;
|
|
||||||
- required surface format отсутствует.
|
|
||||||
|
|
||||||
### PR S0-07 — Swapchain, triangle и offline shaders
|
|
||||||
|
|
||||||
**Изменения**
|
|
||||||
|
|
||||||
- surface и swapchain;
|
|
||||||
- format/present-mode/image-count policy;
|
|
||||||
- render pass и graphics pipeline;
|
|
||||||
- indexed triangle;
|
|
||||||
- command pools/buffers;
|
|
||||||
- binary semaphores и fences;
|
|
||||||
- frames in flight;
|
|
||||||
- resize/out-of-date/suboptimal/zero extent handling;
|
|
||||||
- pinned offline shader compiler;
|
|
||||||
- SPIR-V validation;
|
|
||||||
- descriptor/push-constant manifest;
|
|
||||||
- shader content hashes.
|
|
||||||
|
|
||||||
### PR S0-08 — macOS portability proof
|
|
||||||
|
|
||||||
**Изменения**
|
|
||||||
|
|
||||||
- `VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR`;
|
|
||||||
- portability extension enumeration;
|
|
||||||
- `VK_KHR_portability_subset` enablement, если объявлен device;
|
|
||||||
- MoltenVK packaging strategy;
|
|
||||||
- deterministic portability report;
|
|
||||||
- `.app` bundle smoke.
|
|
||||||
|
|
||||||
### PR S0-09 — Composition roots и legacy removal
|
|
||||||
|
|
||||||
**Изменения**
|
|
||||||
|
|
||||||
- game/viewer подключают winit + Vulkan adapters;
|
|
||||||
- headless остаётся без window/GPU graph;
|
|
||||||
- удалить SDL/GL stub crates;
|
|
||||||
- очистить lockfile, policy и docs;
|
|
||||||
- заменить GPU-named neutral IDs на asset IDs;
|
|
||||||
- запретить stale backend names automated policy check-ом.
|
|
||||||
|
|
||||||
### PR S0-10 — Native acceptance matrix
|
|
||||||
|
|
||||||
**Jobs**
|
|
||||||
|
|
||||||
- Windows MSVC + system Vulkan loader;
|
|
||||||
- Linux X11 или Wayland surface;
|
|
||||||
- macOS Apple Silicon + MoltenVK;
|
|
||||||
- отдельный software-Vulkan Linux PR job допустим как быстрый gate;
|
|
||||||
- native GPU jobs остаются release evidence.
|
|
||||||
|
|
||||||
**Обязательный сценарий**
|
|
||||||
|
|
||||||
1. Создать окно.
|
|
||||||
2. Создать real Vulkan swapchain.
|
|
||||||
3. Показать indexed triangle.
|
|
||||||
4. Выполнить не менее 300 frames.
|
|
||||||
5. Изменить размер окна.
|
|
||||||
6. Пересоздать swapchain.
|
|
||||||
7. Корректно завершить event loop.
|
|
||||||
8. Получить `validation_error_count = 0`.
|
|
||||||
9. Сохранить capability, shader и validation reports как artifacts.
|
|
||||||
|
|
||||||
**Stage 0 закрывается только после merge S0-01…S0-10 и зелёных native artifacts.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Требуемая CI/acceptance модель
|
|
||||||
|
|
||||||
### 8.1 Synthetic PR gate
|
|
||||||
|
|
||||||
Должен работать без игровых каталогов и без silent skip:
|
|
||||||
|
|
||||||
1. fmt, clippy, docs, security и policy;
|
|
||||||
2. все unit/integration tests;
|
|
||||||
3. platform lifecycle state-machine tests;
|
|
||||||
4. device scoring tests на synthetic capability records;
|
|
||||||
5. swapchain policy tests;
|
|
||||||
6. shader manifest/hash tests;
|
|
||||||
7. Vulkan negative-path tests без обязательного GPU;
|
|
||||||
8. headless dependency assertion;
|
|
||||||
9. report schema validation.
|
|
||||||
|
|
||||||
Tests, требующие native GPU или licensed data, должны иметь отдельные suites и machine-readable ownership/reason, а не оставаться обычными `#[ignore]` без evidence trail.
|
|
||||||
|
|
||||||
### 8.2 Native platform gate
|
|
||||||
|
|
||||||
| Platform | Минимальный gate | Дополнительное evidence |
|
|
||||||
|---|---|---|
|
|
||||||
| Windows | system loader, swapchain, triangle, resize, 300 frames, validation=0 | Периодическая NVIDIA/AMD/Intel coverage |
|
|
||||||
| Linux | X11 или Wayland surface, swapchain, resize, validation=0 | Software Vulkan PR job + Mesa/NVIDIA native release jobs |
|
|
||||||
| macOS | MoltenVK, portability enumeration/subset, CAMetalLayer surface, resize, validation=0 | Apple Silicon как primary target |
|
|
||||||
|
|
||||||
### 8.3 Формат machine-readable отчёта
|
|
||||||
|
|
||||||
Минимальные поля:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"schema": 1,
|
|
||||||
"commit": "<sha>",
|
|
||||||
"target": "x86_64-pc-windows-msvc",
|
|
||||||
"rustc": "1.xx.y",
|
|
||||||
"vulkan_api": "1.1",
|
|
||||||
"device_name": "...",
|
|
||||||
"driver": "...",
|
|
||||||
"portability_subset": false,
|
|
||||||
"frames": 300,
|
|
||||||
"resize_count": 1,
|
|
||||||
"swapchain_recreate_count": 1,
|
|
||||||
"validation_error_count": 0,
|
|
||||||
"shader_manifest_hash": "...",
|
|
||||||
"result": "pass"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Definition of Done
|
|
||||||
|
|
||||||
Stage 0 считается закрытым, когда выполнены **все** пункты:
|
|
||||||
|
|
||||||
- [ ] Exact Rust toolchain закреплён.
|
|
||||||
- [ ] MSRV объявлен и проверяется.
|
|
||||||
- [ ] Full fmt/test/clippy/doc/security/source/license gate проходит.
|
|
||||||
- [ ] Typed TOML manifests используются.
|
|
||||||
- [ ] Dependency policy работает через `cargo_metadata`.
|
|
||||||
- [ ] Windows/Linux/macOS matrix сохраняет artifacts.
|
|
||||||
- [ ] `fparkan-platform` больше не содержит GL-specific context concepts.
|
|
||||||
- [ ] `fparkan-platform-winit` реализован.
|
|
||||||
- [ ] `fparkan-render-vulkan` реализован.
|
|
||||||
- [ ] Vulkan 1.1 instance/device/queues/surface/swapchain реализованы.
|
|
||||||
- [ ] Deterministic device scoring и capability report реализованы.
|
|
||||||
- [ ] Indexed triangle рисуется настоящим Vulkan backend.
|
|
||||||
- [ ] Resize, zero extent, out-of-date и suboptimal обработаны.
|
|
||||||
- [ ] MoltenVK portability path реализован.
|
|
||||||
- [ ] Offline SPIR-V validation и hash manifest реализованы.
|
|
||||||
- [ ] Unsafe разрешён только в audited Vulkan/FFI modules.
|
|
||||||
- [ ] Legacy SDL/GL adapters и references удалены.
|
|
||||||
- [ ] Game/viewer используют новые composition adapters.
|
|
||||||
- [ ] Headless dependency graph не содержит winit/Vulkan/MoltenVK.
|
|
||||||
- [ ] 300-frame + resize smoke проходит на трёх OS.
|
|
||||||
- [ ] Validation error count равен нулю на трёх OS.
|
|
||||||
- [ ] Acceptance reports включают commit SHA и сохраняются как artifacts.
|
|
||||||
|
|
||||||
Наличие crates или unit tests с соответствующими названиями само по себе не является закрытием Stage 0.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Рекомендуемые automated policy checks
|
|
||||||
|
|
||||||
Добавить в `cargo xtask policy`:
|
|
||||||
|
|
||||||
### Workspace denylist
|
|
||||||
|
|
||||||
- запрещены `fparkan-platform-sdl` и `fparkan-render-gl` после миграции;
|
|
||||||
- запрещены stale symbols `GraphicsProfile`, `DesktopCore`, `Embedded`, `Gles2` в canonical platform/render API;
|
|
||||||
- canonical docs не содержат OpenGL как production backend.
|
|
||||||
|
|
||||||
### Dependency rules
|
|
||||||
|
|
||||||
- headless не зависит от `winit`, `raw-window-handle`, `ash`, `ash-window` или Vulkan adapter;
|
|
||||||
- backend-neutral crates не зависят от concrete platform/render adapters;
|
|
||||||
- только composition roots связывают platform и renderer;
|
|
||||||
- raw Vulkan types не экспортируются из adapter public boundary.
|
|
||||||
|
|
||||||
### Unsafe rules
|
|
||||||
|
|
||||||
- project-owned unsafe разрешён только в exact allowlisted files/modules;
|
|
||||||
- каждый block содержит `SAFETY:`;
|
|
||||||
- `unsafe_op_in_unsafe_fn` запрещён;
|
|
||||||
- изменение allowlist требует отдельного reviewed diff.
|
|
||||||
|
|
||||||
### Test и report rules
|
|
||||||
|
|
||||||
- synthetic gate не получает licensed paths;
|
|
||||||
- ignored tests обязаны иметь registered reason и owner;
|
|
||||||
- acceptance IDs уникальны;
|
|
||||||
- reports проходят schema validation;
|
|
||||||
- report всегда содержит commit SHA и target triple.
|
|
||||||
|
|
||||||
### Documentation rules
|
|
||||||
|
|
||||||
- документированные crates и commands существуют;
|
|
||||||
- canonical stage version совпадает с acceptance schema;
|
|
||||||
- старые backend names отсутствуют;
|
|
||||||
- README не объявляет незакрытый Vulkan path реализованным.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Основные риски
|
|
||||||
|
|
||||||
| Риск | Последствие | Снижение |
|
|
||||||
|---|---|---|
|
|
||||||
| Vulkan adapter начнут до redesign platform API | Повторная переделка surface/lifecycle/present | Сначала S0-04, затем S0-05/S0-06 |
|
|
||||||
| `unsafe_code` ослабят всему workspace | Рост FFI и lifetime рисков | Изолированный audited adapter и allowlist scanner |
|
|
||||||
| Stubs будут приняты за production backend | Ложное закрытие Stage 0 | Удаление legacy crates и real native smoke |
|
|
||||||
| Linux software Vulkan будет единственным evidence | Не выявятся vendor-driver проблемы | Native Mesa/NVIDIA jobs перед release |
|
|
||||||
| macOS будет проверен без portability subset report | Скрытая несовместимость MoltenVK | Обязательное capability evidence |
|
|
||||||
| Shader compiler останется неприкреплённым | Невоспроизводимый SPIR-V | Pinned compiler + manifest hashes |
|
|
||||||
| GitHub mirror и primary repository разойдутся | Audit и release относятся к разному коду | Commit SHA, canonical remote и artifact metadata |
|
|
||||||
| Документация останется отдельным source of truth | Повторное рассогласование | Versioned stage schema и automated doc checks |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. Реестр доказательств
|
|
||||||
|
|
||||||
### Canonical requirement
|
|
||||||
|
|
||||||
- Vulkan revision: <https://app.notion.com/p/387e79f2db3981778f94cdf34db5f93f>
|
|
||||||
|
|
||||||
### Workspace и governance
|
|
||||||
|
|
||||||
- Root manifest: <https://github.com/valentineus/fparkan/blob/devel/Cargo.toml>
|
|
||||||
- Toolchain: <https://github.com/valentineus/fparkan/blob/devel/rust-toolchain.toml>
|
|
||||||
- Cargo config: <https://github.com/valentineus/fparkan/blob/devel/.cargo/config.toml>
|
|
||||||
- xtask manifest: <https://github.com/valentineus/fparkan/blob/devel/xtask/Cargo.toml>
|
|
||||||
- xtask implementation: <https://github.com/valentineus/fparkan/blob/devel/xtask/src/main.rs>
|
|
||||||
- README: <https://github.com/valentineus/fparkan/blob/devel/README.md>
|
|
||||||
|
|
||||||
### Platform и render
|
|
||||||
|
|
||||||
- Platform core: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-platform/src/lib.rs>
|
|
||||||
- SDL stub adapter: <https://github.com/valentineus/fparkan/blob/devel/adapters/fparkan-platform-sdl/src/lib.rs>
|
|
||||||
- Render core: <https://github.com/valentineus/fparkan/blob/devel/crates/fparkan-render/src/lib.rs>
|
|
||||||
- GL stub adapter: <https://github.com/valentineus/fparkan/blob/devel/adapters/fparkan-render-gl/src/lib.rs>
|
|
||||||
- Game composition: <https://github.com/valentineus/fparkan/blob/devel/apps/fparkan-game/src/main.rs>
|
|
||||||
- Viewer composition: <https://github.com/valentineus/fparkan/blob/devel/apps/fparkan-viewer/src/main.rs>
|
|
||||||
- Headless manifest: <https://github.com/valentineus/fparkan/blob/devel/apps/fparkan-headless/Cargo.toml>
|
|
||||||
|
|
||||||
### Documentation drift
|
|
||||||
|
|
||||||
- Implementation tome: <https://github.com/valentineus/fparkan/blob/devel/docs/tomes/07-implementation.md>
|
|
||||||
- Parity README: <https://github.com/valentineus/fparkan/blob/devel/parity/README.md>
|
|
||||||
- Parity cases: <https://github.com/valentineus/fparkan/blob/devel/parity/cases.toml>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 13. Финальное заключение
|
|
||||||
|
|
||||||
У проекта уже имеется пригодный backend-neutral фундамент: deterministic render commands, строгие neutral-crate lints, отдельный headless composition root и разделение synthetic/licensed tests. Однако Stage 0 пока представлен интерфейсными proof/stub crates, а не настоящим Vulkan vertical slice.
|
|
||||||
|
|
||||||
Критический путь:
|
|
||||||
|
|
||||||
```text
|
|
||||||
reproducible toolchain
|
|
||||||
→ complete CI/policy gate
|
|
||||||
→ backend-neutral platform redesign
|
|
||||||
→ winit adapter
|
|
||||||
→ Vulkan loader/device/surface/swapchain
|
|
||||||
→ indexed triangle + shaders + synchronization
|
|
||||||
→ MoltenVK portability
|
|
||||||
→ composition integration
|
|
||||||
→ legacy removal
|
|
||||||
→ three-platform acceptance artifacts
|
|
||||||
```
|
|
||||||
|
|
||||||
До прохождения этого пути рекомендуемый статус:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Stage 0: IN PROGRESS / BLOCKED
|
|
||||||
```
|
|
||||||
|
|
||||||
Главный критерий закрытия:
|
|
||||||
|
|
||||||
> Stage 0 завершён не тогда, когда существуют crates с названиями `winit` и `vulkan`, а когда один закреплённый commit создаёт настоящий Vulkan swapchain, рисует triangle, переживает resize и завершается без validation errors на Windows, Linux и macOS, сохраняя воспроизводимые machine-readable artifacts.
|
|
||||||
+1
-1
@@ -7,7 +7,7 @@ repository.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fparkan-corpus = { path = "../crates/fparkan-corpus" }
|
fparkan-corpus = { path = "../crates/fparkan-corpus" }
|
||||||
cargo_metadata = "0.23"
|
cargo_metadata = "0.21.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
toml = "1.0"
|
toml = "1.0"
|
||||||
|
|||||||
+1220
-245
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user