refactor(renderer): modularize crate and enable vulkan 1.3 features
This commit is contained in:
parent
1805ae6aa6
commit
3997fa5fd9
243
Cargo.lock
generated
243
Cargo.lock
generated
|
|
@ -130,7 +130,7 @@ version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
|
checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"objc2",
|
"objc2 0.5.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -144,6 +144,20 @@ name = "bytemuck"
|
||||||
version = "1.25.0"
|
version = "1.25.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck_derive"
|
||||||
|
version = "1.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
|
|
@ -437,6 +451,28 @@ dependencies = [
|
||||||
"wasip2",
|
"wasip2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glam"
|
||||||
|
version = "0.29.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gpu-allocator"
|
||||||
|
version = "0.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795"
|
||||||
|
dependencies = [
|
||||||
|
"ash",
|
||||||
|
"log",
|
||||||
|
"objc2 0.6.4",
|
||||||
|
"objc2-foundation 0.3.2",
|
||||||
|
"objc2-metal 0.3.2",
|
||||||
|
"presser",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"windows",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.0"
|
version = "0.17.0"
|
||||||
|
|
@ -701,6 +737,15 @@ dependencies = [
|
||||||
"objc2-encode",
|
"objc2-encode",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
|
||||||
|
dependencies = [
|
||||||
|
"objc2-encode",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-app-kit"
|
name = "objc2-app-kit"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
|
@ -710,10 +755,10 @@ dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
"block2",
|
"block2",
|
||||||
"libc",
|
"libc",
|
||||||
"objc2",
|
"objc2 0.5.2",
|
||||||
"objc2-core-data",
|
"objc2-core-data",
|
||||||
"objc2-core-image",
|
"objc2-core-image",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
"objc2-quartz-core",
|
"objc2-quartz-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -725,9 +770,9 @@ checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
"block2",
|
"block2",
|
||||||
"objc2",
|
"objc2 0.5.2",
|
||||||
"objc2-core-location",
|
"objc2-core-location",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -737,8 +782,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
|
checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block2",
|
"block2",
|
||||||
"objc2",
|
"objc2 0.5.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -749,8 +794,8 @@ checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
"block2",
|
"block2",
|
||||||
"objc2",
|
"objc2 0.5.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -760,9 +805,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
|
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block2",
|
"block2",
|
||||||
"objc2",
|
"objc2 0.5.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
"objc2-metal",
|
"objc2-metal 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -772,9 +817,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
|
checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block2",
|
"block2",
|
||||||
"objc2",
|
"objc2 0.5.2",
|
||||||
"objc2-contacts",
|
"objc2-contacts",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -793,7 +838,17 @@ dependencies = [
|
||||||
"block2",
|
"block2",
|
||||||
"dispatch",
|
"dispatch",
|
||||||
"libc",
|
"libc",
|
||||||
"objc2",
|
"objc2 0.5.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-foundation"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
"objc2 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -803,9 +858,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
|
checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block2",
|
"block2",
|
||||||
"objc2",
|
"objc2 0.5.2",
|
||||||
"objc2-app-kit",
|
"objc2-app-kit",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -816,8 +871,19 @@ checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
"block2",
|
"block2",
|
||||||
"objc2",
|
"objc2 0.5.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-metal"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
"objc2 0.6.4",
|
||||||
|
"objc2-foundation 0.3.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -828,9 +894,9 @@ checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
"block2",
|
"block2",
|
||||||
"objc2",
|
"objc2 0.5.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
"objc2-metal",
|
"objc2-metal 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -839,8 +905,8 @@ version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
|
checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"objc2",
|
"objc2 0.5.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -851,12 +917,12 @@ checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
"block2",
|
"block2",
|
||||||
"objc2",
|
"objc2 0.5.2",
|
||||||
"objc2-cloud-kit",
|
"objc2-cloud-kit",
|
||||||
"objc2-core-data",
|
"objc2-core-data",
|
||||||
"objc2-core-image",
|
"objc2-core-image",
|
||||||
"objc2-core-location",
|
"objc2-core-location",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
"objc2-link-presentation",
|
"objc2-link-presentation",
|
||||||
"objc2-quartz-core",
|
"objc2-quartz-core",
|
||||||
"objc2-symbols",
|
"objc2-symbols",
|
||||||
|
|
@ -871,8 +937,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
|
checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block2",
|
"block2",
|
||||||
"objc2",
|
"objc2 0.5.2",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -883,9 +949,9 @@ checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
"block2",
|
"block2",
|
||||||
"objc2",
|
"objc2 0.5.2",
|
||||||
"objc2-core-location",
|
"objc2-core-location",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -971,6 +1037,12 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "presser"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "3.5.0"
|
version = "3.5.0"
|
||||||
|
|
@ -1055,6 +1127,9 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ash",
|
"ash",
|
||||||
"ash-window",
|
"ash-window",
|
||||||
|
"bytemuck",
|
||||||
|
"glam",
|
||||||
|
"gpu-allocator",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -1673,12 +1748,107 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||||
|
dependencies = [
|
||||||
|
"windows-collections",
|
||||||
|
"windows-core",
|
||||||
|
"windows-future",
|
||||||
|
"windows-numerics",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-collections"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-future"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core",
|
||||||
|
"windows-link",
|
||||||
|
"windows-threading",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-numerics"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
|
@ -1722,6 +1892,15 @@ dependencies = [
|
||||||
"windows_x86_64_msvc",
|
"windows_x86_64_msvc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-threading"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_gnullvm"
|
name = "windows_aarch64_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
|
@ -1793,9 +1972,9 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"memmap2",
|
"memmap2",
|
||||||
"ndk",
|
"ndk",
|
||||||
"objc2",
|
"objc2 0.5.2",
|
||||||
"objc2-app-kit",
|
"objc2-app-kit",
|
||||||
"objc2-foundation",
|
"objc2-foundation 0.2.2",
|
||||||
"objc2-ui-kit",
|
"objc2-ui-kit",
|
||||||
"orbclient",
|
"orbclient",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,6 @@ ash-window = "0.13.0"
|
||||||
raw-window-handle = "0.6.2"
|
raw-window-handle = "0.6.2"
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
|
gpu-allocator = "0.28.0"
|
||||||
|
bytemuck = { version = "1.21", features = ["derive"] }
|
||||||
|
glam = "0.29"
|
||||||
|
|
|
||||||
98
crates/renderer/src/device.rs
Normal file
98
crates/renderer/src/device.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
use ash::{Device, Instance, khr, vk};
|
||||||
|
use crate::error::RendererError;
|
||||||
|
|
||||||
|
/// Picks a physical device (GPU) that supports the required features and extensions.
|
||||||
|
pub fn pick_physical_device(
|
||||||
|
instance: &Instance,
|
||||||
|
surface_loader: &khr::surface::Instance,
|
||||||
|
surface: vk::SurfaceKHR,
|
||||||
|
) -> Result<vk::PhysicalDevice, RendererError> {
|
||||||
|
let devices = unsafe { instance.enumerate_physical_devices()? };
|
||||||
|
|
||||||
|
for device in devices {
|
||||||
|
if is_device_suitable(instance, device, surface_loader, surface) {
|
||||||
|
let props = unsafe { instance.get_physical_device_properties(device) };
|
||||||
|
let name = unsafe { std::ffi::CStr::from_ptr(props.device_name.as_ptr()).to_string_lossy() };
|
||||||
|
tracing::info!("Selected GPU: \"{name}\"");
|
||||||
|
return Ok(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(RendererError::NoSuitableGpu)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a logical device and retrieves the graphics queue.
|
||||||
|
pub fn create_logical_device(
|
||||||
|
instance: &Instance,
|
||||||
|
physical_device: vk::PhysicalDevice,
|
||||||
|
queue_family_index: u32,
|
||||||
|
) -> Result<(Device, vk::Queue), RendererError> {
|
||||||
|
let priorities = [1.0];
|
||||||
|
let queue_info = vk::DeviceQueueCreateInfo::default()
|
||||||
|
.queue_family_index(queue_family_index)
|
||||||
|
.queue_priorities(&priorities);
|
||||||
|
|
||||||
|
let device_extensions = [khr::swapchain::NAME.as_ptr()];
|
||||||
|
|
||||||
|
// Enable Vulkan 1.3 features
|
||||||
|
let mut synchronization2_features = vk::PhysicalDeviceSynchronization2Features::default()
|
||||||
|
.synchronization2(true);
|
||||||
|
let mut dynamic_rendering_features = vk::PhysicalDeviceDynamicRenderingFeatures::default()
|
||||||
|
.dynamic_rendering(true);
|
||||||
|
|
||||||
|
let create_info = vk::DeviceCreateInfo::default()
|
||||||
|
.queue_create_infos(std::slice::from_ref(&queue_info))
|
||||||
|
.enabled_extension_names(&device_extensions)
|
||||||
|
.push_next(&mut synchronization2_features)
|
||||||
|
.push_next(&mut dynamic_rendering_features);
|
||||||
|
|
||||||
|
let device = unsafe { instance.create_device(physical_device, &create_info, None)? };
|
||||||
|
let graphics_queue = unsafe { device.get_device_queue(queue_family_index, 0) };
|
||||||
|
|
||||||
|
Ok((device, graphics_queue))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds a queue family that supports both graphics commands and presentation.
|
||||||
|
pub fn find_graphics_queue_family(
|
||||||
|
instance: &Instance,
|
||||||
|
physical_device: vk::PhysicalDevice,
|
||||||
|
surface_loader: &khr::surface::Instance,
|
||||||
|
surface: vk::SurfaceKHR,
|
||||||
|
) -> Result<u32, RendererError> {
|
||||||
|
let props = unsafe { instance.get_physical_device_queue_family_properties(physical_device) };
|
||||||
|
|
||||||
|
for (index, prop) in props.iter().enumerate() {
|
||||||
|
let index = index as u32;
|
||||||
|
let graphics = prop.queue_flags.contains(vk::QueueFlags::GRAPHICS);
|
||||||
|
let present = unsafe {
|
||||||
|
surface_loader.get_physical_device_surface_support(physical_device, index, surface)?
|
||||||
|
};
|
||||||
|
|
||||||
|
if graphics && present {
|
||||||
|
return Ok(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(RendererError::NoSuitableGpu)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_device_suitable(
|
||||||
|
instance: &Instance,
|
||||||
|
device: vk::PhysicalDevice,
|
||||||
|
surface_loader: &khr::surface::Instance,
|
||||||
|
surface: vk::SurfaceKHR,
|
||||||
|
) -> bool {
|
||||||
|
let extensions = unsafe { instance.enumerate_device_extension_properties(device).unwrap_or_default() };
|
||||||
|
let has_swapchain = extensions.iter().any(|ext| {
|
||||||
|
unsafe { std::ffi::CStr::from_ptr(ext.extension_name.as_ptr()) == khr::swapchain::NAME }
|
||||||
|
});
|
||||||
|
|
||||||
|
let formats = unsafe {
|
||||||
|
surface_loader.get_physical_device_surface_formats(device, surface).unwrap_or_default()
|
||||||
|
};
|
||||||
|
let present_modes = unsafe {
|
||||||
|
surface_loader.get_physical_device_surface_present_modes(device, surface).unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
|
has_swapchain && !formats.is_empty() && !present_modes.is_empty()
|
||||||
|
}
|
||||||
80
crates/renderer/src/instance.rs
Normal file
80
crates/renderer/src/instance.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
use std::ffi::{CStr, c_char};
|
||||||
|
use ash::{Entry, Instance, ext, vk};
|
||||||
|
use tracing::{debug, error, info, warn};
|
||||||
|
use crate::error::RendererError;
|
||||||
|
|
||||||
|
/// Creates a Vulkan instance and optionally a debug messenger.
|
||||||
|
pub fn create_instance(
|
||||||
|
entry: &Entry,
|
||||||
|
required_extensions: &[*const c_char],
|
||||||
|
) -> Result<(Instance, Option<ext::debug_utils::Instance>, vk::DebugUtilsMessengerEXT), RendererError> {
|
||||||
|
let mut extensions = required_extensions.to_vec();
|
||||||
|
let mut layers = Vec::new();
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
extensions.push(ext::debug_utils::NAME.as_ptr());
|
||||||
|
layers.push(c"VK_LAYER_KHRONOS_validation".as_ptr());
|
||||||
|
}
|
||||||
|
|
||||||
|
let app_info = vk::ApplicationInfo::default()
|
||||||
|
.api_version(vk::API_VERSION_1_3);
|
||||||
|
|
||||||
|
let create_info = vk::InstanceCreateInfo::default()
|
||||||
|
.application_info(&app_info)
|
||||||
|
.enabled_extension_names(&extensions)
|
||||||
|
.enabled_layer_names(&layers);
|
||||||
|
|
||||||
|
let instance = unsafe { entry.create_instance(&create_info, None)? };
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
let (debug_utils, debug_messenger) = {
|
||||||
|
let debug_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_debug_callback));
|
||||||
|
|
||||||
|
let utils = ext::debug_utils::Instance::new(entry, &instance);
|
||||||
|
let messenger = unsafe { utils.create_debug_utils_messenger(&debug_info, None)? };
|
||||||
|
|
||||||
|
(Some(utils), messenger)
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
let (debug_utils, debug_messenger) = (None, vk::DebugUtilsMessengerEXT::null());
|
||||||
|
|
||||||
|
Ok((instance, debug_utils, debug_messenger))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The callback function invoked by Vulkan's validation layers.
|
||||||
|
unsafe extern "system" fn vulkan_debug_callback(
|
||||||
|
message_severity: vk::DebugUtilsMessageSeverityFlagsEXT,
|
||||||
|
_message_type: vk::DebugUtilsMessageTypeFlagsEXT,
|
||||||
|
p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>,
|
||||||
|
_user_data: *mut std::ffi::c_void,
|
||||||
|
) -> vk::Bool32 {
|
||||||
|
let callback_data = unsafe { *p_callback_data };
|
||||||
|
|
||||||
|
let message = if callback_data.p_message.is_null() {
|
||||||
|
"".into()
|
||||||
|
} else {
|
||||||
|
unsafe { CStr::from_ptr(callback_data.p_message).to_string_lossy() }
|
||||||
|
};
|
||||||
|
|
||||||
|
match message_severity {
|
||||||
|
vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE => debug!("{message}"),
|
||||||
|
vk::DebugUtilsMessageSeverityFlagsEXT::INFO => info!("{message}"),
|
||||||
|
vk::DebugUtilsMessageSeverityFlagsEXT::WARNING => warn!("{message}"),
|
||||||
|
vk::DebugUtilsMessageSeverityFlagsEXT::ERROR => error!("{message}"),
|
||||||
|
_ => info!("{message}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
vk::FALSE
|
||||||
|
}
|
||||||
|
|
@ -1,70 +1,23 @@
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
mod instance;
|
||||||
|
mod device;
|
||||||
|
mod surface;
|
||||||
|
mod swapchain;
|
||||||
|
mod sync;
|
||||||
|
mod renderer;
|
||||||
|
|
||||||
use std::ffi::{CStr, c_char};
|
use std::ffi::c_char;
|
||||||
use ash::{Device, Entry, Instance, ext, khr, vk};
|
use ash::{Entry, vk};
|
||||||
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
|
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
|
||||||
use tracing::{debug, error, info, warn};
|
|
||||||
|
|
||||||
pub use error::RendererError;
|
pub use error::RendererError;
|
||||||
|
pub use renderer::{Renderer, MAX_FRAMES_IN_FLIGHT};
|
||||||
/// The core renderer structure holding the Vulkan instance and debug resources.
|
|
||||||
pub struct Renderer {
|
|
||||||
/// The entry point to the Vulkan library.
|
|
||||||
_entry: Entry,
|
|
||||||
/// The Vulkan instance handle.
|
|
||||||
instance: Instance,
|
|
||||||
/// The debug utils instance, if enabled (debug builds only).
|
|
||||||
debug_utils: Option<ext::debug_utils::Instance>,
|
|
||||||
/// The debug messenger handle, if enabled (debug builds only).
|
|
||||||
debug_messenger: vk::DebugUtilsMessengerEXT,
|
|
||||||
/// The selected physical device (GPU).
|
|
||||||
physical_device: vk::PhysicalDevice,
|
|
||||||
/// The logical device handle.
|
|
||||||
device: Device,
|
|
||||||
/// The handle to the graphics queue.
|
|
||||||
graphics_queue: vk::Queue,
|
|
||||||
/// The index of the graphics queue family.
|
|
||||||
graphics_queue_index: u32,
|
|
||||||
/// The surface loader instance.
|
|
||||||
surface_loader: khr::surface::Instance,
|
|
||||||
/// The window surface handle.
|
|
||||||
surface: vk::SurfaceKHR,
|
|
||||||
/// The swapchain loader instance.
|
|
||||||
swapchain_loader: khr::swapchain::Device,
|
|
||||||
/// The swapchain handle.
|
|
||||||
swapchain: vk::SwapchainKHR,
|
|
||||||
/// The images retrieved from the swapchain.
|
|
||||||
swapchain_images: Vec<vk::Image>,
|
|
||||||
/// The pixel format of the swapchain images.
|
|
||||||
swapchain_format: vk::Format,
|
|
||||||
/// The resolution of the swapchain images.
|
|
||||||
swapchain_extent: vk::Extent2D,
|
|
||||||
/// The views into the swapchain images.
|
|
||||||
swapchain_image_views: Vec<vk::ImageView>,
|
|
||||||
/// The pool used to allocate command buffers.
|
|
||||||
command_pool: vk::CommandPool,
|
|
||||||
/// The buffers used to record GPU commands (one per frame in flight).
|
|
||||||
command_buffers: Vec<vk::CommandBuffer>,
|
|
||||||
/// Semaphores signaled when an image is acquired (one per frame in flight).
|
|
||||||
image_available_semaphores: Vec<vk::Semaphore>,
|
|
||||||
/// Semaphores signaled when rendering is complete (one per frame in flight).
|
|
||||||
render_finished_semaphores: Vec<vk::Semaphore>,
|
|
||||||
/// Fences signaled when the GPU has finished a frame (one per frame in flight).
|
|
||||||
in_flight_fences: Vec<vk::Fence>,
|
|
||||||
/// Tracks which frame is using which swapchain image (one per swapchain image).
|
|
||||||
images_in_flight: Vec<vk::Fence>,
|
|
||||||
/// The index of the frame currently being processed (0..MAX_FRAMES_IN_FLIGHT).
|
|
||||||
current_frame: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Renderer {
|
impl Renderer {
|
||||||
/// Initializes the Vulkan renderer.
|
/// Initializes the Vulkan renderer.
|
||||||
///
|
///
|
||||||
/// This function loads the Vulkan library, creates an instance, selects a GPU,
|
/// This function loads the Vulkan library, creates an instance, selects a GPU,
|
||||||
/// and initializes a logical device with a graphics queue.
|
/// and initializes a logical device with a graphics queue.
|
||||||
///
|
|
||||||
/// `display_handle` and `window_handle` are used to create the OS-specific surface.
|
|
||||||
/// `required_extensions` are raw C-strings (pointers) provided by the windowing system.
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
display_handle: RawDisplayHandle,
|
display_handle: RawDisplayHandle,
|
||||||
window_handle: RawWindowHandle,
|
window_handle: RawWindowHandle,
|
||||||
|
|
@ -74,117 +27,51 @@ impl Renderer {
|
||||||
) -> Result<Self, RendererError> {
|
) -> Result<Self, RendererError> {
|
||||||
let entry = unsafe { Entry::load() }?;
|
let entry = unsafe { Entry::load() }?;
|
||||||
|
|
||||||
// Prepare extensions and layers
|
// 1. Instance and Debug Messenger
|
||||||
let mut extensions: Vec<*const c_char> = required_extensions.to_vec();
|
let (instance, debug_utils, debug_messenger) =
|
||||||
let mut layers: Vec<*const c_char> = Vec::new();
|
instance::create_instance(&entry, required_extensions)?;
|
||||||
|
|
||||||
// Enable debug extensions and validation layers in debug builds
|
// 2. Surface
|
||||||
#[cfg(debug_assertions)]
|
let (surface_loader, surface) =
|
||||||
{
|
surface::create_surface(&entry, &instance, display_handle, window_handle)?;
|
||||||
extensions.push(ext::debug_utils::NAME.as_ptr());
|
|
||||||
layers.push(c"VK_LAYER_KHRONOS_validation".as_ptr());
|
|
||||||
}
|
|
||||||
|
|
||||||
let app_info = vk::ApplicationInfo::default()
|
// 3. Physical Device (GPU)
|
||||||
.api_version(vk::API_VERSION_1_3);
|
let physical_device =
|
||||||
|
device::pick_physical_device(&instance, &surface_loader, surface)?;
|
||||||
|
|
||||||
let create_info = vk::InstanceCreateInfo::default()
|
// 4. Graphics Queue Index
|
||||||
.application_info(&app_info)
|
let graphics_queue_index =
|
||||||
.enabled_extension_names(&extensions)
|
device::find_graphics_queue_family(&instance, physical_device, &surface_loader, surface)?;
|
||||||
.enabled_layer_names(&layers);
|
|
||||||
|
|
||||||
// Create the Vulkan instance
|
|
||||||
let instance = unsafe { entry.create_instance(&create_info, None)? };
|
|
||||||
|
|
||||||
// Setup debug messenger for validation layer feedback
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
let (debug_utils, debug_messenger) = {
|
|
||||||
let debug_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_debug_callback));
|
|
||||||
|
|
||||||
let utils = ext::debug_utils::Instance::new(&entry, &instance);
|
|
||||||
let messenger = unsafe { utils.create_debug_utils_messenger(&debug_info, None)? };
|
|
||||||
|
|
||||||
(Some(utils), messenger)
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
let (debug_utils, debug_messenger) = (None, vk::DebugUtilsMessengerEXT::null());
|
|
||||||
|
|
||||||
// Create the OS surface
|
|
||||||
let surface = unsafe {
|
|
||||||
ash_window::create_surface(&entry, &instance, display_handle, window_handle, None)?
|
|
||||||
};
|
|
||||||
let surface_loader = khr::surface::Instance::new(&entry, &instance);
|
|
||||||
|
|
||||||
// Pick a physical device that supports the surface
|
|
||||||
let physical_device = Self::pick_physical_device(&instance, &surface_loader, surface)?;
|
|
||||||
|
|
||||||
// Find the graphics queue family (which must also support presentation)
|
|
||||||
let graphics_queue_index = Self::find_graphics_queue_family(&instance, physical_device, &surface_loader, surface)?;
|
|
||||||
|
|
||||||
// Create the logical device
|
// 5. Logical Device and Queue
|
||||||
let (device, graphics_queue) = Self::create_logical_device(&instance, physical_device, graphics_queue_index)?;
|
let (device, graphics_queue) =
|
||||||
|
device::create_logical_device(&instance, physical_device, graphics_queue_index)?;
|
||||||
|
|
||||||
// Create the swapchain
|
// 6. Swapchain
|
||||||
let (swapchain_loader, swapchain, swapchain_images, swapchain_format, swapchain_extent) =
|
let (swapchain_loader, swapchain, swapchain_images, swapchain_format, swapchain_extent) =
|
||||||
Self::create_swapchain(&instance, physical_device, &device, &surface_loader, surface, width, height)?;
|
swapchain::create_swapchain(&instance, physical_device, &device, &surface_loader, surface, width, height)?;
|
||||||
|
|
||||||
// Create image views for the swapchain images
|
// 7. Image Views
|
||||||
let image_views = Self::create_image_views(&device, &swapchain_images, swapchain_format)?;
|
let image_views =
|
||||||
|
swapchain::create_image_views(&device, &swapchain_images, swapchain_format)?;
|
||||||
|
|
||||||
// Create the command pool
|
// 8. Command Pool
|
||||||
let pool_create_info = vk::CommandPoolCreateInfo::default()
|
let pool_create_info = vk::CommandPoolCreateInfo::default()
|
||||||
.queue_family_index(graphics_queue_index)
|
.queue_family_index(graphics_queue_index)
|
||||||
// Allows reusing the command buffer every frame by resetting it
|
|
||||||
.flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER);
|
.flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER);
|
||||||
|
|
||||||
let command_pool = unsafe { device.create_command_pool(&pool_create_info, None)? };
|
let command_pool = unsafe { device.create_command_pool(&pool_create_info, None)? };
|
||||||
|
|
||||||
// Create synchronization primitives
|
// 9. Command Buffers
|
||||||
// We use MAX_FRAMES_IN_FLIGHT (2) to allow the CPU to stay one frame ahead of the GPU.
|
|
||||||
const MAX_FRAMES_IN_FLIGHT: usize = 2;
|
|
||||||
let mut image_available_semaphores = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT);
|
|
||||||
let mut render_finished_semaphores = Vec::with_capacity(swapchain_images.len());
|
|
||||||
let mut in_flight_fences = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT);
|
|
||||||
let mut command_buffers = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT);
|
|
||||||
|
|
||||||
let semaphore_info = vk::SemaphoreCreateInfo::default();
|
|
||||||
let fence_info = vk::FenceCreateInfo::default()
|
|
||||||
.flags(vk::FenceCreateFlags::SIGNALED);
|
|
||||||
|
|
||||||
let alloc_info = vk::CommandBufferAllocateInfo::default()
|
let alloc_info = vk::CommandBufferAllocateInfo::default()
|
||||||
.command_pool(command_pool)
|
.command_pool(command_pool)
|
||||||
.level(vk::CommandBufferLevel::PRIMARY)
|
.level(vk::CommandBufferLevel::PRIMARY)
|
||||||
.command_buffer_count(MAX_FRAMES_IN_FLIGHT as u32);
|
.command_buffer_count(MAX_FRAMES_IN_FLIGHT as u32);
|
||||||
|
|
||||||
let cbs = unsafe { device.allocate_command_buffers(&alloc_info)? };
|
let command_buffers = unsafe { device.allocate_command_buffers(&alloc_info)? };
|
||||||
|
|
||||||
for i in 0..MAX_FRAMES_IN_FLIGHT {
|
// 10. Synchronization Primitives
|
||||||
image_available_semaphores.push(unsafe { device.create_semaphore(&semaphore_info, None)? });
|
let sync = sync::create_sync_primitives(&device, MAX_FRAMES_IN_FLIGHT, swapchain_images.len())?;
|
||||||
in_flight_fences.push(unsafe { device.create_fence(&fence_info, None)? });
|
|
||||||
command_buffers.push(cbs[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We create one render-finished semaphore per swapchain image.
|
|
||||||
// This satisfies strict drivers (like AMD RADV) that require a semaphore
|
|
||||||
// to be tied to a specific image until it is re-acquired.
|
|
||||||
for _ in 0..swapchain_images.len() {
|
|
||||||
render_finished_semaphores.push(unsafe { device.create_semaphore(&semaphore_info, None)? });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initially, no image is in use by a frame.
|
|
||||||
// We use this to track which frame's fence is protecting which swapchain image.
|
|
||||||
let images_in_flight = vec![vk::Fence::null(); swapchain_images.len()];
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
_entry: entry,
|
_entry: entry,
|
||||||
|
|
@ -205,433 +92,8 @@ impl Renderer {
|
||||||
swapchain_image_views: image_views,
|
swapchain_image_views: image_views,
|
||||||
command_pool,
|
command_pool,
|
||||||
command_buffers,
|
command_buffers,
|
||||||
image_available_semaphores,
|
sync,
|
||||||
render_finished_semaphores,
|
|
||||||
in_flight_fences,
|
|
||||||
images_in_flight,
|
|
||||||
current_frame: 0,
|
current_frame: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Selects the best physical device (GPU) available on the system.
|
|
||||||
///
|
|
||||||
/// This function filters for devices supporting graphics operations and scores
|
|
||||||
/// them based on their type, preferring discrete GPUs.
|
|
||||||
fn pick_physical_device(
|
|
||||||
instance: &Instance,
|
|
||||||
surface_loader: &khr::surface::Instance,
|
|
||||||
surface: vk::SurfaceKHR,
|
|
||||||
) -> Result<vk::PhysicalDevice, RendererError> {
|
|
||||||
let devices = unsafe { instance.enumerate_physical_devices()? };
|
|
||||||
|
|
||||||
let selected = devices.into_iter()
|
|
||||||
.filter(|&device| {
|
|
||||||
// Check if the device has a queue family that supports graphics AND presentation
|
|
||||||
let props = unsafe { instance.get_physical_device_queue_family_properties(device) };
|
|
||||||
props.iter().enumerate().any(|(i, p)| {
|
|
||||||
let supports_graphics = p.queue_flags.contains(vk::QueueFlags::GRAPHICS);
|
|
||||||
let supports_present = unsafe {
|
|
||||||
surface_loader.get_physical_device_surface_support(device, i as u32, surface)
|
|
||||||
.unwrap_or(false)
|
|
||||||
};
|
|
||||||
supports_graphics && supports_present
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.max_by_key(|&device| {
|
|
||||||
// Score the device based on its type
|
|
||||||
let props = unsafe { instance.get_physical_device_properties(device) };
|
|
||||||
match props.device_type {
|
|
||||||
vk::PhysicalDeviceType::DISCRETE_GPU => 1000,
|
|
||||||
vk::PhysicalDeviceType::INTEGRATED_GPU => 100,
|
|
||||||
_ => 1,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.ok_or(RendererError::NoSuitableGpu)?;
|
|
||||||
|
|
||||||
let props = unsafe { instance.get_physical_device_properties(selected) };
|
|
||||||
info!("Selected GPU: {:?}", unsafe { CStr::from_ptr(props.device_name.as_ptr()).to_string_lossy() });
|
|
||||||
|
|
||||||
Ok(selected)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Finds the index of the first queue family that supports both graphics and presentation.
|
|
||||||
fn find_graphics_queue_family(
|
|
||||||
instance: &Instance,
|
|
||||||
physical_device: vk::PhysicalDevice,
|
|
||||||
surface_loader: &khr::surface::Instance,
|
|
||||||
surface: vk::SurfaceKHR,
|
|
||||||
) -> Result<u32, RendererError> {
|
|
||||||
let props = unsafe { instance.get_physical_device_queue_family_properties(physical_device) };
|
|
||||||
|
|
||||||
props.iter().enumerate()
|
|
||||||
.find(|&(i, p)| {
|
|
||||||
let supports_graphics = p.queue_flags.contains(vk::QueueFlags::GRAPHICS);
|
|
||||||
let supports_present = unsafe {
|
|
||||||
surface_loader.get_physical_device_surface_support(physical_device, i as u32, surface)
|
|
||||||
.unwrap_or(false)
|
|
||||||
};
|
|
||||||
supports_graphics && supports_present
|
|
||||||
})
|
|
||||||
.map(|(i, _)| i as u32)
|
|
||||||
.ok_or(RendererError::NoSuitableGpu)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a logical device and retrieves the handle to its graphics queue.
|
|
||||||
fn create_logical_device(
|
|
||||||
instance: &Instance,
|
|
||||||
physical_device: vk::PhysicalDevice,
|
|
||||||
queue_index: u32,
|
|
||||||
) -> Result<(Device, vk::Queue), RendererError> {
|
|
||||||
// Request a single queue from the graphics family
|
|
||||||
let queue_priorities = [1.0];
|
|
||||||
let queue_info = vk::DeviceQueueCreateInfo::default()
|
|
||||||
.queue_family_index(queue_index)
|
|
||||||
.queue_priorities(&queue_priorities);
|
|
||||||
|
|
||||||
// Enable modern Vulkan 1.3 features
|
|
||||||
let mut features_13 = vk::PhysicalDeviceVulkan13Features::default()
|
|
||||||
.dynamic_rendering(true) // Removes the need for RenderPasses/Framebuffers
|
|
||||||
.synchronization2(true); // Cleaner memory barriers and synchronization
|
|
||||||
|
|
||||||
// We need the swapchain extension to present images to the window
|
|
||||||
let device_extensions = [khr::swapchain::NAME.as_ptr()];
|
|
||||||
|
|
||||||
let create_info = vk::DeviceCreateInfo::default()
|
|
||||||
.queue_create_infos(std::slice::from_ref(&queue_info))
|
|
||||||
.enabled_extension_names(&device_extensions)
|
|
||||||
.push_next(&mut features_13);
|
|
||||||
|
|
||||||
let device = unsafe { instance.create_device(physical_device, &create_info, None)? };
|
|
||||||
let graphics_queue = unsafe { device.get_device_queue(queue_index, 0) };
|
|
||||||
|
|
||||||
Ok((device, graphics_queue))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a swapchain and retrieves its images.
|
|
||||||
///
|
|
||||||
/// This function negotiates with the surface to find a suitable color format,
|
|
||||||
/// presentation mode, and image resolution.
|
|
||||||
fn create_swapchain(
|
|
||||||
instance: &Instance,
|
|
||||||
physical_device: vk::PhysicalDevice,
|
|
||||||
device: &Device,
|
|
||||||
surface_loader: &khr::surface::Instance,
|
|
||||||
surface: vk::SurfaceKHR,
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
) -> Result<
|
|
||||||
(khr::swapchain::Device, vk::SwapchainKHR, Vec<vk::Image>, vk::Format, vk::Extent2D),
|
|
||||||
RendererError,
|
|
||||||
> {
|
|
||||||
let surface_capabilities = unsafe { surface_loader.get_physical_device_surface_capabilities(physical_device, surface)? };
|
|
||||||
|
|
||||||
let surface_formats = unsafe { surface_loader.get_physical_device_surface_formats(physical_device, surface)? };
|
|
||||||
|
|
||||||
let surface_present_modes = unsafe { surface_loader.get_physical_device_surface_present_modes(physical_device, surface)?};
|
|
||||||
|
|
||||||
// 1. Pick the best color format (Prefer SRGB for accurate colors)
|
|
||||||
let format = surface_formats.iter()
|
|
||||||
.find(|f| f.format == vk::Format::B8G8R8A8_SRGB && f.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR)
|
|
||||||
.unwrap_or(&surface_formats[0]);
|
|
||||||
|
|
||||||
// 2. Pick the best presentation mode (Mailbox = Triple Buffering, FIFO = VSync)
|
|
||||||
let present_mode = surface_present_modes.into_iter()
|
|
||||||
.find(|&m| m == vk::PresentModeKHR::MAILBOX)
|
|
||||||
.unwrap_or(vk::PresentModeKHR::FIFO);
|
|
||||||
|
|
||||||
// 3. Match the window resolution
|
|
||||||
// If the surface says u32::MAX, it means we can pick any size within bounds.
|
|
||||||
let extent = if surface_capabilities.current_extent.width != u32::MAX {
|
|
||||||
surface_capabilities.current_extent
|
|
||||||
} else {
|
|
||||||
vk::Extent2D {
|
|
||||||
width: width.clamp(surface_capabilities.min_image_extent.width, surface_capabilities.max_image_extent.width),
|
|
||||||
height: height.clamp(surface_capabilities.min_image_extent.height, surface_capabilities.max_image_extent.height),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut image_count = surface_capabilities.min_image_count + 1;
|
|
||||||
|
|
||||||
if surface_capabilities.max_image_count > 0 && image_count > surface_capabilities.max_image_count {
|
|
||||||
image_count = surface_capabilities.max_image_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
let swapchain_loader = khr::swapchain::Device::new(instance, device);
|
|
||||||
|
|
||||||
let create_info = vk::SwapchainCreateInfoKHR::default()
|
|
||||||
.surface(surface)
|
|
||||||
.min_image_count(image_count)
|
|
||||||
.image_format(format.format)
|
|
||||||
.image_color_space(format.color_space)
|
|
||||||
.image_extent(extent)
|
|
||||||
.image_array_layers(1)
|
|
||||||
.image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT)
|
|
||||||
.image_sharing_mode(vk::SharingMode::EXCLUSIVE)
|
|
||||||
.pre_transform(surface_capabilities.current_transform)
|
|
||||||
.composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE)
|
|
||||||
.present_mode(present_mode)
|
|
||||||
.clipped(true);
|
|
||||||
|
|
||||||
let swapchain = unsafe { swapchain_loader.create_swapchain(&create_info, None)? };
|
|
||||||
|
|
||||||
let images = unsafe { swapchain_loader.get_swapchain_images(swapchain)? };
|
|
||||||
|
|
||||||
Ok((swapchain_loader, swapchain, images, format.format, extent ))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates image views for the swapchain images.
|
|
||||||
///
|
|
||||||
/// An image view describes how to access an image (e.g. as a 2D texture).
|
|
||||||
fn create_image_views(
|
|
||||||
device: &Device,
|
|
||||||
images: &[vk::Image],
|
|
||||||
format: vk::Format,
|
|
||||||
) -> Result<Vec<vk::ImageView>, RendererError> {
|
|
||||||
let mut views = Vec::with_capacity(images.len());
|
|
||||||
|
|
||||||
for &image in images {
|
|
||||||
let create_info = vk::ImageViewCreateInfo::default()
|
|
||||||
.image(image)
|
|
||||||
.view_type(vk::ImageViewType::TYPE_2D)
|
|
||||||
.format(format)
|
|
||||||
.components(vk::ComponentMapping {
|
|
||||||
r: vk::ComponentSwizzle::IDENTITY,
|
|
||||||
g: vk::ComponentSwizzle::IDENTITY,
|
|
||||||
b: vk::ComponentSwizzle::IDENTITY,
|
|
||||||
a: vk::ComponentSwizzle::IDENTITY,
|
|
||||||
})
|
|
||||||
.subresource_range(vk::ImageSubresourceRange {
|
|
||||||
aspect_mask: vk::ImageAspectFlags::COLOR,
|
|
||||||
base_mip_level: 0,
|
|
||||||
level_count: 1,
|
|
||||||
base_array_layer: 0,
|
|
||||||
layer_count: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
let view = unsafe { device.create_image_view(&create_info, None)? };
|
|
||||||
views.push(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(views)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders a single frame.
|
|
||||||
///
|
|
||||||
/// This function handles synchronization, image acquisition, command recording
|
|
||||||
/// for clearing the screen, and presentation.
|
|
||||||
pub fn draw_frame(&mut self) -> Result<(), RendererError> {
|
|
||||||
let in_flight_fence = self.in_flight_fences[self.current_frame];
|
|
||||||
let image_available_semaphore = self.image_available_semaphores[self.current_frame];
|
|
||||||
let cmd = self.command_buffers[self.current_frame];
|
|
||||||
|
|
||||||
// 1. Wait for the current frame's GPU work to finish
|
|
||||||
unsafe {
|
|
||||||
self.device.wait_for_fences(&[in_flight_fence], true, u64::MAX)?;
|
|
||||||
self.device.reset_fences(&[in_flight_fence])?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Acquire an image from the swapchain
|
|
||||||
let (image_index, _is_suboptimal) = unsafe {
|
|
||||||
self.swapchain_loader.acquire_next_image(
|
|
||||||
self.swapchain,
|
|
||||||
u64::MAX,
|
|
||||||
image_available_semaphore,
|
|
||||||
vk::Fence::null(),
|
|
||||||
)?
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the acquired image is still being used by a previous frame, wait for it
|
|
||||||
let image_fence = self.images_in_flight[image_index as usize];
|
|
||||||
if image_fence != vk::Fence::null() {
|
|
||||||
unsafe { self.device.wait_for_fences(&[image_fence], true, u64::MAX)? };
|
|
||||||
}
|
|
||||||
// Mark the image as being in use by the current frame's fence
|
|
||||||
self.images_in_flight[image_index as usize] = in_flight_fence;
|
|
||||||
|
|
||||||
// Use the semaphore tied to this specific swapchain image for rendering completion
|
|
||||||
let render_finished_semaphore = self.render_finished_semaphores[image_index as usize];
|
|
||||||
|
|
||||||
// 3. Reset and begin recording the command buffer
|
|
||||||
unsafe {
|
|
||||||
self.device.reset_command_buffer(cmd, vk::CommandBufferResetFlags::empty())?;
|
|
||||||
let begin_info = vk::CommandBufferBeginInfo::default()
|
|
||||||
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
|
|
||||||
self.device.begin_command_buffer(cmd, &begin_info)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let image = self.swapchain_images[image_index as usize];
|
|
||||||
let view = self.swapchain_image_views[image_index as usize];
|
|
||||||
|
|
||||||
// 4. Transition the swapchain image to a layout suitable for drawing
|
|
||||||
let range = vk::ImageSubresourceRange {
|
|
||||||
aspect_mask: vk::ImageAspectFlags::COLOR,
|
|
||||||
base_mip_level: 0,
|
|
||||||
level_count: 1,
|
|
||||||
base_array_layer: 0,
|
|
||||||
layer_count: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let barrier_to_draw = vk::ImageMemoryBarrier2::default()
|
|
||||||
.image(image)
|
|
||||||
.subresource_range(range)
|
|
||||||
.src_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT)
|
|
||||||
.src_access_mask(vk::AccessFlags2::empty())
|
|
||||||
.dst_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT)
|
|
||||||
.dst_access_mask(vk::AccessFlags2::COLOR_ATTACHMENT_WRITE)
|
|
||||||
.old_layout(vk::ImageLayout::UNDEFINED)
|
|
||||||
.new_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
|
|
||||||
|
|
||||||
let dependency_info = vk::DependencyInfo::default()
|
|
||||||
.image_memory_barriers(std::slice::from_ref(&barrier_to_draw));
|
|
||||||
|
|
||||||
unsafe { self.device.cmd_pipeline_barrier2(cmd, &dependency_info) };
|
|
||||||
|
|
||||||
// 5. Begin Dynamic Rendering with a clear color
|
|
||||||
let color_attachment = vk::RenderingAttachmentInfo::default()
|
|
||||||
.image_view(view)
|
|
||||||
.image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
|
||||||
.load_op(vk::AttachmentLoadOp::CLEAR)
|
|
||||||
.store_op(vk::AttachmentStoreOp::STORE)
|
|
||||||
.clear_value(vk::ClearValue {
|
|
||||||
color: vk::ClearColorValue {
|
|
||||||
float32: [0.1, 0.2, 0.4, 1.0], // Project Catalyst Blue
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let rendering_info = vk::RenderingInfo::default()
|
|
||||||
.render_area(vk::Rect2D {
|
|
||||||
offset: vk::Offset2D { x: 0, y: 0 },
|
|
||||||
extent: self.swapchain_extent,
|
|
||||||
})
|
|
||||||
.layer_count(1)
|
|
||||||
.color_attachments(std::slice::from_ref(&color_attachment));
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
self.device.cmd_begin_rendering(cmd, &rendering_info);
|
|
||||||
// Future draw calls will go here
|
|
||||||
self.device.cmd_end_rendering(cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Transition the image back to Present layout
|
|
||||||
let barrier_to_present = vk::ImageMemoryBarrier2::default()
|
|
||||||
.image(image)
|
|
||||||
.subresource_range(range)
|
|
||||||
.src_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT)
|
|
||||||
.src_access_mask(vk::AccessFlags2::COLOR_ATTACHMENT_WRITE)
|
|
||||||
.dst_stage_mask(vk::PipelineStageFlags2::BOTTOM_OF_PIPE)
|
|
||||||
.dst_access_mask(vk::AccessFlags2::empty())
|
|
||||||
.old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
|
||||||
.new_layout(vk::ImageLayout::PRESENT_SRC_KHR);
|
|
||||||
|
|
||||||
let dependency_info = vk::DependencyInfo::default()
|
|
||||||
.image_memory_barriers(std::slice::from_ref(&barrier_to_present));
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
self.device.cmd_pipeline_barrier2(cmd, &dependency_info);
|
|
||||||
self.device.end_command_buffer(cmd)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. Submit the work to the GPU
|
|
||||||
let submit_info = vk::SubmitInfo::default()
|
|
||||||
.wait_semaphores(std::slice::from_ref(&image_available_semaphore))
|
|
||||||
.wait_dst_stage_mask(&[vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT])
|
|
||||||
.command_buffers(std::slice::from_ref(&cmd))
|
|
||||||
.signal_semaphores(std::slice::from_ref(&render_finished_semaphore));
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
self.device.queue_submit(self.graphics_queue, &[submit_info], in_flight_fence)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. Present the result to the screen
|
|
||||||
let present_info = vk::PresentInfoKHR::default()
|
|
||||||
.wait_semaphores(std::slice::from_ref(&render_finished_semaphore))
|
|
||||||
.swapchains(std::slice::from_ref(&self.swapchain))
|
|
||||||
.image_indices(std::slice::from_ref(&image_index));
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
self.swapchain_loader.queue_present(self.graphics_queue, &present_info)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advance the frame index for the next call
|
|
||||||
self.current_frame = (self.current_frame + 1) % self.in_flight_fences.len();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensures all Vulkan resources are destroyed in the correct order.
|
|
||||||
impl Drop for Renderer {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
unsafe {
|
|
||||||
// Ensure the GPU is finished before we start destroying things
|
|
||||||
let _ = self.device.device_wait_idle();
|
|
||||||
|
|
||||||
self.device.destroy_command_pool(self.command_pool, None);
|
|
||||||
|
|
||||||
// Destroy synchronization primitives for all frames
|
|
||||||
for &semaphore in &self.image_available_semaphores {
|
|
||||||
self.device.destroy_semaphore(semaphore, None);
|
|
||||||
}
|
|
||||||
for &semaphore in &self.render_finished_semaphores {
|
|
||||||
self.device.destroy_semaphore(semaphore, None);
|
|
||||||
}
|
|
||||||
for &fence in &self.in_flight_fences {
|
|
||||||
self.device.destroy_fence(fence, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy the swapchain
|
|
||||||
self.swapchain_loader.destroy_swapchain(self.swapchain, None);
|
|
||||||
|
|
||||||
// Destroy image views
|
|
||||||
for &view in &self.swapchain_image_views {
|
|
||||||
self.device.destroy_image_view(view, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy the logical device
|
|
||||||
self.device.destroy_device(None);
|
|
||||||
|
|
||||||
// Destroy the surface
|
|
||||||
self.surface_loader.destroy_surface(self.surface, None);
|
|
||||||
|
|
||||||
// Destroy the debug messenger if we created one
|
|
||||||
if let Some(utils) = &self.debug_utils {
|
|
||||||
utils.destroy_debug_utils_messenger(self.debug_messenger, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy the instance
|
|
||||||
self.instance.destroy_instance(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The callback function invoked by Vulkan's validation layers.
|
|
||||||
///
|
|
||||||
/// This function translates Vulkan debug messages into `tracing` events.
|
|
||||||
unsafe extern "system" fn vulkan_debug_callback(
|
|
||||||
message_severity: vk::DebugUtilsMessageSeverityFlagsEXT,
|
|
||||||
_message_type: vk::DebugUtilsMessageTypeFlagsEXT,
|
|
||||||
p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>,
|
|
||||||
_user_data: *mut std::ffi::c_void,
|
|
||||||
) -> vk::Bool32 {
|
|
||||||
// Safety: p_callback_data is guaranteed to be valid by the Vulkan spec
|
|
||||||
let callback_data = unsafe { *p_callback_data };
|
|
||||||
|
|
||||||
// Convert raw C string to Rust string for logging
|
|
||||||
let message = if callback_data.p_message.is_null() {
|
|
||||||
"".into()
|
|
||||||
} else {
|
|
||||||
unsafe { CStr::from_ptr(callback_data.p_message).to_string_lossy() }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Route severity to matching tracing macro
|
|
||||||
match message_severity {
|
|
||||||
vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE => debug!("{message}"),
|
|
||||||
vk::DebugUtilsMessageSeverityFlagsEXT::INFO => info!("{message}"),
|
|
||||||
vk::DebugUtilsMessageSeverityFlagsEXT::WARNING => warn!("{message}"),
|
|
||||||
vk::DebugUtilsMessageSeverityFlagsEXT::ERROR => error!("{message}"),
|
|
||||||
_ => info!("{message}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
// TRUE aborts the Vulkan call that triggered the warning
|
|
||||||
vk::FALSE
|
|
||||||
}
|
}
|
||||||
207
crates/renderer/src/renderer.rs
Normal file
207
crates/renderer/src/renderer.rs
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
use ash::{Device, Instance, khr, vk};
|
||||||
|
use crate::error::RendererError;
|
||||||
|
use crate::sync::SyncPrimitives;
|
||||||
|
|
||||||
|
pub const MAX_FRAMES_IN_FLIGHT: usize = 2;
|
||||||
|
|
||||||
|
/// The core renderer structure holding the Vulkan resources.
|
||||||
|
pub struct Renderer {
|
||||||
|
pub(crate) _entry: ash::Entry,
|
||||||
|
pub(crate) instance: Instance,
|
||||||
|
pub(crate) debug_utils: Option<ash::ext::debug_utils::Instance>,
|
||||||
|
pub(crate) debug_messenger: vk::DebugUtilsMessengerEXT,
|
||||||
|
pub(crate) physical_device: vk::PhysicalDevice,
|
||||||
|
pub(crate) device: Device,
|
||||||
|
pub(crate) graphics_queue: vk::Queue,
|
||||||
|
pub(crate) graphics_queue_index: u32,
|
||||||
|
pub(crate) surface_loader: khr::surface::Instance,
|
||||||
|
pub(crate) surface: vk::SurfaceKHR,
|
||||||
|
pub(crate) swapchain_loader: khr::swapchain::Device,
|
||||||
|
pub(crate) swapchain: vk::SwapchainKHR,
|
||||||
|
pub(crate) swapchain_images: Vec<vk::Image>,
|
||||||
|
pub(crate) swapchain_format: vk::Format,
|
||||||
|
pub(crate) swapchain_extent: vk::Extent2D,
|
||||||
|
pub(crate) swapchain_image_views: Vec<vk::ImageView>,
|
||||||
|
pub(crate) command_pool: vk::CommandPool,
|
||||||
|
pub(crate) command_buffers: Vec<vk::CommandBuffer>,
|
||||||
|
pub(crate) sync: SyncPrimitives,
|
||||||
|
pub(crate) current_frame: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Renderer {
|
||||||
|
/// Renders a single frame.
|
||||||
|
pub fn draw_frame(&mut self) -> Result<(), RendererError> {
|
||||||
|
let in_flight_fence = self.sync.in_flight[self.current_frame];
|
||||||
|
let image_available_semaphore = self.sync.image_available[self.current_frame];
|
||||||
|
let cmd = self.command_buffers[self.current_frame];
|
||||||
|
|
||||||
|
// 1. Wait for the current frame's GPU work to finish
|
||||||
|
unsafe {
|
||||||
|
self.device.wait_for_fences(&[in_flight_fence], true, u64::MAX)?;
|
||||||
|
self.device.reset_fences(&[in_flight_fence])?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Acquire an image from the swapchain
|
||||||
|
let (image_index, _is_suboptimal) = unsafe {
|
||||||
|
self.swapchain_loader.acquire_next_image(
|
||||||
|
self.swapchain,
|
||||||
|
u64::MAX,
|
||||||
|
image_available_semaphore,
|
||||||
|
vk::Fence::null(),
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the acquired image is still being used by a previous frame, wait for it
|
||||||
|
let image_fence = self.sync.images_in_flight[image_index as usize];
|
||||||
|
if image_fence != vk::Fence::null() {
|
||||||
|
unsafe { self.device.wait_for_fences(&[image_fence], true, u64::MAX)? };
|
||||||
|
}
|
||||||
|
// Mark the image as being in use by the current frame's fence
|
||||||
|
self.sync.images_in_flight[image_index as usize] = in_flight_fence;
|
||||||
|
|
||||||
|
// Use the semaphore tied to this specific swapchain image for rendering completion
|
||||||
|
let render_finished_semaphore = self.sync.render_finished[image_index as usize];
|
||||||
|
|
||||||
|
// 3. Reset and begin recording the command buffer
|
||||||
|
unsafe {
|
||||||
|
self.device.reset_command_buffer(cmd, vk::CommandBufferResetFlags::empty())?;
|
||||||
|
let begin_info = vk::CommandBufferBeginInfo::default()
|
||||||
|
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
|
||||||
|
self.device.begin_command_buffer(cmd, &begin_info)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let image = self.swapchain_images[image_index as usize];
|
||||||
|
let view = self.swapchain_image_views[image_index as usize];
|
||||||
|
|
||||||
|
// 4. Transition the swapchain image to a layout suitable for drawing
|
||||||
|
let range = vk::ImageSubresourceRange {
|
||||||
|
aspect_mask: vk::ImageAspectFlags::COLOR,
|
||||||
|
base_mip_level: 0,
|
||||||
|
level_count: 1,
|
||||||
|
base_array_layer: 0,
|
||||||
|
layer_count: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let barrier_to_draw = vk::ImageMemoryBarrier2::default()
|
||||||
|
.image(image)
|
||||||
|
.subresource_range(range)
|
||||||
|
.src_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT)
|
||||||
|
.src_access_mask(vk::AccessFlags2::empty())
|
||||||
|
.dst_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT)
|
||||||
|
.dst_access_mask(vk::AccessFlags2::COLOR_ATTACHMENT_WRITE)
|
||||||
|
.old_layout(vk::ImageLayout::UNDEFINED)
|
||||||
|
.new_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);
|
||||||
|
|
||||||
|
let dependency_info = vk::DependencyInfo::default()
|
||||||
|
.image_memory_barriers(std::slice::from_ref(&barrier_to_draw));
|
||||||
|
|
||||||
|
unsafe { self.device.cmd_pipeline_barrier2(cmd, &dependency_info) };
|
||||||
|
|
||||||
|
// 5. Begin Dynamic Rendering with a clear color
|
||||||
|
let color_attachment = vk::RenderingAttachmentInfo::default()
|
||||||
|
.image_view(view)
|
||||||
|
.image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
||||||
|
.load_op(vk::AttachmentLoadOp::CLEAR)
|
||||||
|
.store_op(vk::AttachmentStoreOp::STORE)
|
||||||
|
.clear_value(vk::ClearValue {
|
||||||
|
color: vk::ClearColorValue {
|
||||||
|
float32: [0.1, 0.2, 0.4, 1.0], // Project Catalyst Blue
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let rendering_info = vk::RenderingInfo::default()
|
||||||
|
.render_area(vk::Rect2D {
|
||||||
|
offset: vk::Offset2D { x: 0, y: 0 },
|
||||||
|
extent: self.swapchain_extent,
|
||||||
|
})
|
||||||
|
.layer_count(1)
|
||||||
|
.color_attachments(std::slice::from_ref(&color_attachment));
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
self.device.cmd_begin_rendering(cmd, &rendering_info);
|
||||||
|
// Future draw calls will go here
|
||||||
|
self.device.cmd_end_rendering(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Transition the image back to Present layout
|
||||||
|
let barrier_to_present = vk::ImageMemoryBarrier2::default()
|
||||||
|
.image(image)
|
||||||
|
.subresource_range(range)
|
||||||
|
.src_stage_mask(vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT)
|
||||||
|
.src_access_mask(vk::AccessFlags2::COLOR_ATTACHMENT_WRITE)
|
||||||
|
.dst_stage_mask(vk::PipelineStageFlags2::BOTTOM_OF_PIPE)
|
||||||
|
.dst_access_mask(vk::AccessFlags2::empty())
|
||||||
|
.old_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
||||||
|
.new_layout(vk::ImageLayout::PRESENT_SRC_KHR);
|
||||||
|
|
||||||
|
let dependency_info = vk::DependencyInfo::default()
|
||||||
|
.image_memory_barriers(std::slice::from_ref(&barrier_to_present));
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
self.device.cmd_pipeline_barrier2(cmd, &dependency_info);
|
||||||
|
self.device.end_command_buffer(cmd)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Submit the work to the GPU
|
||||||
|
let submit_info = vk::SubmitInfo::default()
|
||||||
|
.wait_semaphores(std::slice::from_ref(&image_available_semaphore))
|
||||||
|
.wait_dst_stage_mask(&[vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT])
|
||||||
|
.command_buffers(std::slice::from_ref(&cmd))
|
||||||
|
.signal_semaphores(std::slice::from_ref(&render_finished_semaphore));
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
self.device.queue_submit(self.graphics_queue, &[submit_info], in_flight_fence)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Present the result to the screen
|
||||||
|
let present_info = vk::PresentInfoKHR::default()
|
||||||
|
.wait_semaphores(std::slice::from_ref(&render_finished_semaphore))
|
||||||
|
.swapchains(std::slice::from_ref(&self.swapchain))
|
||||||
|
.image_indices(std::slice::from_ref(&image_index));
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
self.swapchain_loader.queue_present(self.graphics_queue, &present_info)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance the frame index for the next call
|
||||||
|
self.current_frame = (self.current_frame + 1) % MAX_FRAMES_IN_FLIGHT;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Renderer {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
let _ = self.device.device_wait_idle();
|
||||||
|
|
||||||
|
self.device.destroy_command_pool(self.command_pool, None);
|
||||||
|
|
||||||
|
// Use the safe cleanup function from sync module
|
||||||
|
let sync = std::ptr::read(&self.sync);
|
||||||
|
crate::sync::destroy_sync_primitives(&self.device, sync);
|
||||||
|
|
||||||
|
// Destroy the swapchain
|
||||||
|
self.swapchain_loader.destroy_swapchain(self.swapchain, None);
|
||||||
|
|
||||||
|
// Destroy image views
|
||||||
|
for &view in &self.swapchain_image_views {
|
||||||
|
self.device.destroy_image_view(view, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy the logical device
|
||||||
|
self.device.destroy_device(None);
|
||||||
|
|
||||||
|
// Destroy the surface
|
||||||
|
self.surface_loader.destroy_surface(self.surface, None);
|
||||||
|
|
||||||
|
// Destroy the debug messenger if we created one
|
||||||
|
if let Some(utils) = &self.debug_utils {
|
||||||
|
utils.destroy_debug_utils_messenger(self.debug_messenger, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy the instance
|
||||||
|
self.instance.destroy_instance(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
crates/renderer/src/surface.rs
Normal file
18
crates/renderer/src/surface.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
use ash::{Entry, Instance, khr, vk};
|
||||||
|
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
|
||||||
|
use crate::error::RendererError;
|
||||||
|
|
||||||
|
/// Creates a Vulkan surface for the given window.
|
||||||
|
pub fn create_surface(
|
||||||
|
entry: &Entry,
|
||||||
|
instance: &Instance,
|
||||||
|
display_handle: RawDisplayHandle,
|
||||||
|
window_handle: RawWindowHandle,
|
||||||
|
) -> Result<(khr::surface::Instance, vk::SurfaceKHR), RendererError> {
|
||||||
|
let surface = unsafe {
|
||||||
|
ash_window::create_surface(entry, instance, display_handle, window_handle, None)?
|
||||||
|
};
|
||||||
|
let surface_loader = khr::surface::Instance::new(entry, instance);
|
||||||
|
|
||||||
|
Ok((surface_loader, surface))
|
||||||
|
}
|
||||||
96
crates/renderer/src/swapchain.rs
Normal file
96
crates/renderer/src/swapchain.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
use ash::{Device, Instance, khr, vk};
|
||||||
|
use crate::error::RendererError;
|
||||||
|
|
||||||
|
/// Creates a swapchain and retrieves its images.
|
||||||
|
pub fn create_swapchain(
|
||||||
|
instance: &Instance,
|
||||||
|
physical_device: vk::PhysicalDevice,
|
||||||
|
device: &Device,
|
||||||
|
surface_loader: &khr::surface::Instance,
|
||||||
|
surface: vk::SurfaceKHR,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
) -> Result<(khr::swapchain::Device, vk::SwapchainKHR, Vec<vk::Image>, vk::Format, vk::Extent2D), RendererError> {
|
||||||
|
let surface_capabilities = unsafe {
|
||||||
|
surface_loader.get_physical_device_surface_capabilities(physical_device, surface)?
|
||||||
|
};
|
||||||
|
let surface_formats = unsafe {
|
||||||
|
surface_loader.get_physical_device_surface_formats(physical_device, surface)?
|
||||||
|
};
|
||||||
|
let surface_present_modes = unsafe {
|
||||||
|
surface_loader.get_physical_device_surface_present_modes(physical_device, surface)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let format = surface_formats.iter()
|
||||||
|
.find(|f| f.format == vk::Format::B8G8R8A8_SRGB && f.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR)
|
||||||
|
.unwrap_or(&surface_formats[0]);
|
||||||
|
|
||||||
|
let present_mode = surface_present_modes.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|&m| m == vk::PresentModeKHR::MAILBOX)
|
||||||
|
.unwrap_or(vk::PresentModeKHR::FIFO);
|
||||||
|
|
||||||
|
let extent = if surface_capabilities.current_extent.width != u32::MAX {
|
||||||
|
surface_capabilities.current_extent
|
||||||
|
} else {
|
||||||
|
vk::Extent2D {
|
||||||
|
width: width.clamp(surface_capabilities.min_image_extent.width, surface_capabilities.max_image_extent.width),
|
||||||
|
height: height.clamp(surface_capabilities.min_image_extent.height, surface_capabilities.max_image_extent.height),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let image_count = if surface_capabilities.max_image_count > 0 && surface_capabilities.min_image_count + 1 > surface_capabilities.max_image_count {
|
||||||
|
surface_capabilities.max_image_count
|
||||||
|
} else {
|
||||||
|
surface_capabilities.min_image_count + 1
|
||||||
|
};
|
||||||
|
|
||||||
|
let swapchain_loader = khr::swapchain::Device::new(instance, device);
|
||||||
|
|
||||||
|
let create_info = vk::SwapchainCreateInfoKHR::default()
|
||||||
|
.surface(surface)
|
||||||
|
.min_image_count(image_count)
|
||||||
|
.image_format(format.format)
|
||||||
|
.image_color_space(format.color_space)
|
||||||
|
.image_extent(extent)
|
||||||
|
.image_array_layers(1)
|
||||||
|
.image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT)
|
||||||
|
.image_sharing_mode(vk::SharingMode::EXCLUSIVE)
|
||||||
|
.pre_transform(surface_capabilities.current_transform)
|
||||||
|
.composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE)
|
||||||
|
.present_mode(present_mode)
|
||||||
|
.clipped(true);
|
||||||
|
|
||||||
|
let swapchain = unsafe { swapchain_loader.create_swapchain(&create_info, None)? };
|
||||||
|
let images = unsafe { swapchain_loader.get_swapchain_images(swapchain)? };
|
||||||
|
|
||||||
|
Ok((swapchain_loader, swapchain, images, format.format, extent))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates image views for the swapchain images.
|
||||||
|
pub fn create_image_views(
|
||||||
|
device: &Device,
|
||||||
|
images: &[vk::Image],
|
||||||
|
format: vk::Format,
|
||||||
|
) -> Result<Vec<vk::ImageView>, RendererError> {
|
||||||
|
let mut views = Vec::with_capacity(images.len());
|
||||||
|
|
||||||
|
for &image in images {
|
||||||
|
let create_info = vk::ImageViewCreateInfo::default()
|
||||||
|
.image(image)
|
||||||
|
.view_type(vk::ImageViewType::TYPE_2D)
|
||||||
|
.format(format)
|
||||||
|
.subresource_range(vk::ImageSubresourceRange {
|
||||||
|
aspect_mask: vk::ImageAspectFlags::COLOR,
|
||||||
|
base_mip_level: 0,
|
||||||
|
level_count: 1,
|
||||||
|
base_array_layer: 0,
|
||||||
|
layer_count: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
let view = unsafe { device.create_image_view(&create_info, None)? };
|
||||||
|
views.push(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(views)
|
||||||
|
}
|
||||||
58
crates/renderer/src/sync.rs
Normal file
58
crates/renderer/src/sync.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
use ash::{Device, vk};
|
||||||
|
use crate::error::RendererError;
|
||||||
|
|
||||||
|
/// Groups all synchronization primitives for the renderer.
|
||||||
|
pub struct SyncPrimitives {
|
||||||
|
pub image_available: Vec<vk::Semaphore>,
|
||||||
|
pub render_finished: Vec<vk::Semaphore>,
|
||||||
|
pub in_flight: Vec<vk::Fence>,
|
||||||
|
pub images_in_flight: Vec<vk::Fence>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates all synchronization primitives for the given number of frames and images.
|
||||||
|
pub fn create_sync_primitives(
|
||||||
|
device: &Device,
|
||||||
|
max_frames_in_flight: usize,
|
||||||
|
image_count: usize,
|
||||||
|
) -> Result<SyncPrimitives, RendererError> {
|
||||||
|
let semaphore_info = vk::SemaphoreCreateInfo::default();
|
||||||
|
let fence_info = vk::FenceCreateInfo::default()
|
||||||
|
.flags(vk::FenceCreateFlags::SIGNALED);
|
||||||
|
|
||||||
|
let mut image_available = Vec::with_capacity(max_frames_in_flight);
|
||||||
|
let mut render_finished = Vec::with_capacity(image_count);
|
||||||
|
let mut in_flight = Vec::with_capacity(max_frames_in_flight);
|
||||||
|
|
||||||
|
for _ in 0..max_frames_in_flight {
|
||||||
|
image_available.push(unsafe { device.create_semaphore(&semaphore_info, None)? });
|
||||||
|
in_flight.push(unsafe { device.create_fence(&fence_info, None)? });
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0..image_count {
|
||||||
|
render_finished.push(unsafe { device.create_semaphore(&semaphore_info, None)? });
|
||||||
|
}
|
||||||
|
|
||||||
|
let images_in_flight = vec![vk::Fence::null(); image_count];
|
||||||
|
|
||||||
|
Ok(SyncPrimitives {
|
||||||
|
image_available,
|
||||||
|
render_finished,
|
||||||
|
in_flight,
|
||||||
|
images_in_flight,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Destroys all synchronization primitives.
|
||||||
|
pub unsafe fn destroy_sync_primitives(device: &Device, sync: SyncPrimitives) {
|
||||||
|
unsafe {
|
||||||
|
for semaphore in sync.image_available {
|
||||||
|
device.destroy_semaphore(semaphore, None);
|
||||||
|
}
|
||||||
|
for semaphore in sync.render_finished {
|
||||||
|
device.destroy_semaphore(semaphore, None);
|
||||||
|
}
|
||||||
|
for fence in sync.in_flight {
|
||||||
|
device.destroy_fence(fence, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue