diff --git a/Cargo.lock b/Cargo.lock index cd7d923..546998f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,7 +44,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" dependencies = [ "android-properties", - "bitflags", + "bitflags 2.11.0", "cc", "jni", "libc", @@ -86,6 +86,26 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "ash-window" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52bca67b61cb81e5553babde81b8211f713cb6db79766f80168f3e5f40ea6c82" +dependencies = [ + "ash", + "raw-window-handle", + "raw-window-metal", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -104,12 +124,24 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block2" version = "0.6.2" @@ -169,7 +201,7 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" dependencies = [ - "bitflags", + "bitflags 2.11.0", "polling", "rustix", "slab", @@ -221,6 +253,36 @@ dependencies = [ "cc", ] +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + [[package]] name = "combine" version = "4.6.7" @@ -240,6 +302,46 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -287,7 +389,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags", + "bitflags 2.11.0", "objc2", ] @@ -317,10 +419,15 @@ name = "engine" version = "0.1.0" dependencies = [ "anyhow", + "glam", "log", "logging", "platform", + "renderer", + "resource_manager", "thiserror 2.0.18", + "utils", + "world", ] [[package]] @@ -364,6 +471,33 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "futures-core" version = "0.3.32" @@ -590,7 +724,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" dependencies = [ - "bitflags", + "bitflags 2.11.0", "serde", ] @@ -622,7 +756,7 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" dependencies = [ - "bitflags", + "bitflags 2.11.0", "libc", "plain", "redox_syscall 0.7.3", @@ -657,6 +791,15 @@ dependencies = [ "time", ] +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "memchr" version = "2.8.0" @@ -698,7 +841,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags", + "bitflags 2.11.0", "jni-sys 0.3.1", "log", "ndk-sys", @@ -759,6 +902,15 @@ dependencies = [ "syn", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + [[package]] name = "objc2" version = "0.6.4" @@ -774,7 +926,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags", + "bitflags 2.11.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -786,7 +938,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags", + "bitflags 2.11.0", "block2", "dispatch2", "objc2", @@ -798,7 +950,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags", + "bitflags 2.11.0", "libc", "objc2-core-foundation", ] @@ -809,7 +961,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ - "bitflags", + "bitflags 2.11.0", "objc2-core-foundation", "objc2-core-graphics", ] @@ -826,7 +978,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags", + "bitflags 2.11.0", "block2", "objc2", "objc2-core-foundation", @@ -838,7 +990,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags", + "bitflags 2.11.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -921,6 +1073,7 @@ dependencies = [ "build", "log", "thiserror 2.0.18", + "utils", "winit", ] @@ -930,7 +1083,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags", + "bitflags 2.11.0", "crc32fast", "fdeflate", "flate2", @@ -1011,13 +1164,25 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "raw-window-metal" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e8caa82e31bb98fee12fa8f051c94a6aa36b07cddb03f0d4fc558988360ff1" +dependencies = [ + "cocoa", + "core-graphics", + "objc", + "raw-window-handle", +] + [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.0", ] [[package]] @@ -1026,7 +1191,26 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "bitflags", + "bitflags 2.11.0", +] + +[[package]] +name = "renderer" +version = "0.1.0" +dependencies = [ + "anyhow", + "ash", + "ash-window", + "build", + "glam", + "log", + "platform", + "raw-window-handle", + "resource_manager", + "shaders", + "thiserror 2.0.18", + "utils", + "world", ] [[package]] @@ -1069,7 +1253,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -1235,7 +1419,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" dependencies = [ - "bitflags", + "bitflags 2.11.0", "calloop", "calloop-wayland-source", "cursor-icon", @@ -1450,6 +1634,14 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utils" +version = "0.1.0" +dependencies = [ + "glam", + "thiserror 2.0.18", +] + [[package]] name = "version_check" version = "0.9.5" @@ -1550,7 +1742,7 @@ version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags", + "bitflags 2.11.0", "rustix", "wayland-backend", "wayland-scanner", @@ -1562,7 +1754,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags", + "bitflags 2.11.0", "cursor-icon", "wayland-backend", ] @@ -1584,7 +1776,7 @@ version = "0.32.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" dependencies = [ - "bitflags", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -1596,7 +1788,7 @@ version = "20250721.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" dependencies = [ - "bitflags", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -1609,7 +1801,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" dependencies = [ - "bitflags", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -1622,7 +1814,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" dependencies = [ - "bitflags", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -1635,7 +1827,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ - "bitflags", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -1788,7 +1980,7 @@ version = "0.31.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2879d2854d1a43e48f67322d4bd097afcb6eb8f8f775c8de0260a71aea1df1aa" dependencies = [ - "bitflags", + "bitflags 2.11.0", "cfg_aliases", "cursor-icon", "dpi", @@ -1816,7 +2008,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d9c0d2cd93efec3a9f9ad819cfaf0834782403af7c0d248c784ec0c61761df" dependencies = [ "android-activity", - "bitflags", + "bitflags 2.11.0", "dpi", "ndk", "raw-window-handle", @@ -1831,7 +2023,7 @@ version = "0.31.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21310ca07851a49c348e0c2cc768e36b52ca65afda2c2354d78ed4b90074d8aa" dependencies = [ - "bitflags", + "bitflags 2.11.0", "block2", "dispatch2", "dpi", @@ -1870,7 +2062,7 @@ version = "0.31.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4f0ccd7abb43740e2c6124ac7cae7d865ecec74eec63783e8922577ac232583" dependencies = [ - "bitflags", + "bitflags 2.11.0", "cursor-icon", "dpi", "keyboard-types", @@ -1885,7 +2077,7 @@ version = "0.31.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51ea1fb262e7209f265f12bd0cc792c399b14355675e65531e9c8a87db287d46" dependencies = [ - "bitflags", + "bitflags 2.11.0", "dpi", "orbclient", "raw-window-handle", @@ -1901,7 +2093,7 @@ version = "0.31.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680a356e798837d8eb274d4556e83bceaf81698194e31aafc5cfb8a9f2fab643" dependencies = [ - "bitflags", + "bitflags 2.11.0", "block2", "dispatch2", "dpi", @@ -1923,7 +2115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce5afb2ba07da603f84b722c95f9f9396d2cedae3944fb6c0cda4a6f88de545" dependencies = [ "ahash", - "bitflags", + "bitflags 2.11.0", "calloop", "cursor-icon", "dpi", @@ -1950,7 +2142,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c2490a953fb776fbbd5e295d54f1c3847f4f15b6c3929ec53c09acda6487a92" dependencies = [ "atomic-waker", - "bitflags", + "bitflags 2.11.0", "concurrent-queue", "cursor-icon", "dpi", @@ -1972,7 +2164,7 @@ version = "0.31.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "644ea78af0e858aa3b092e5d1c67c41995a98220c81813f1353b28bc8bb91eaa" dependencies = [ - "bitflags", + "bitflags 2.11.0", "cursor-icon", "dpi", "raw-window-handle", @@ -1989,7 +2181,7 @@ version = "0.31.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa5b600756534c7041aa93cd0d244d44b09fca1b89e202bd1cd80dd9f3636c46" dependencies = [ - "bitflags", + "bitflags 2.11.0", "bytemuck", "calloop", "cursor-icon", @@ -2027,6 +2219,7 @@ name = "world" version = "0.1.0" dependencies = [ "glam", + "utils", ] [[package]] @@ -2074,7 +2267,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags", + "bitflags 2.11.0", "dlib", "log", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index 1ea5e00..b84f943 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ members = [ "Engine/Source/platform", "Engine/Source/run", "Engine/Source/resource_manager", + "Engine/Source/utils", + "Engine/Source/renderer", "Engine/Source/world", "Engine/Shaders" ] @@ -29,15 +31,20 @@ derive-getters = "0.5" # Engine modules build = { path = "Engine/Source/build" } +utils = { path = "Engine/Source/utils" } logging = { path = "Engine/Source/logging" } engine = { path = "Engine/Source/engine" } platform = { path = "Engine/Source/platform" } +renderer = { path = "Engine/Source/renderer" } resource_manager = { path = "Engine/Source/resource_manager" } world = { path = "Engine/Source/world" } shaders = { path = "Engine/Shaders" } # Main Engine Dependencies winit = "0.31.0-beta.2" +ash = { version = "0.38", features = ["loaded"] } +ash-window = "0.13" +raw-window-handle = "0.6" glam = "0.32" gltf = "1" shaderc = "0.10" diff --git a/Engine/Shaders/shaders/shader.frag b/Engine/Shaders/shaders/shader.frag index 873f541..bc2c524 100644 --- a/Engine/Shaders/shaders/shader.frag +++ b/Engine/Shaders/shaders/shader.frag @@ -1,6 +1,6 @@ #version 450 -layout(binding = 1) uniform sampler2D texSampler; +layout(set = 2, binding = 2) uniform sampler2D texSampler; layout(location = 0) in vec3 fragColor; layout(location = 1) in vec2 fragTexCoord; diff --git a/Engine/Shaders/shaders/shader.vert b/Engine/Shaders/shaders/shader.vert index 784312b..4c9b477 100644 --- a/Engine/Shaders/shaders/shader.vert +++ b/Engine/Shaders/shaders/shader.vert @@ -1,10 +1,13 @@ #version 450 -layout(binding = 0) uniform ModelViewProjection { - mat4 model; +layout(set = 0, binding = 0) uniform SceneUbo { mat4 view; mat4 proj; -} mvp; +} scene; + +layout(set = 1, binding = 1) uniform ProxyUbo { + mat4 model; +} proxy; layout(location = 0) in vec3 inPosition; layout(location = 1) in vec3 inColor; @@ -14,7 +17,7 @@ layout(location = 0) out vec3 fragColor; layout(location = 1) out vec2 fragTexCoord; void main() { - gl_Position = mvp.proj * mvp.view * mvp.model * vec4(inPosition, 1.0); + gl_Position = scene.proj * scene.view * proxy.model * vec4(inPosition, 1.0); fragColor = inColor; fragTexCoord = inTexCoord; } diff --git a/Engine/Shaders/src/lib.rs b/Engine/Shaders/src/lib.rs index fc03b05..a27a8b7 100644 --- a/Engine/Shaders/src/lib.rs +++ b/Engine/Shaders/src/lib.rs @@ -1,10 +1,12 @@ //! This crate contains all the shaders used in the engine. +use std::ffi::CStr; + /// A simple struct that holds a block of shader bytecode and /// the name of the shader's entrypoint. pub struct Shader { code: &'static [u8], - entrypoint: &'static str, + entrypoint: &'static CStr, } impl Shader { @@ -12,8 +14,19 @@ impl Shader { pub const fn code(&self) -> &[u8] { self.code } + /// Returns the code but in blocks of u32 + pub fn code_as_u32(&self) -> Vec { + assert!( + self.code.len().is_multiple_of(4), + "SPIR-V size must be a multiple of 4" + ); + self.code + .chunks_exact(4) + .map(|c| u32::from_le_bytes(c.try_into().unwrap())) + .collect() + } /// Returns the entrypoint of this shader - pub const fn entrypoint(&self) -> &str { + pub const fn entrypoint(&self) -> &CStr { self.entrypoint } } @@ -27,5 +40,5 @@ macro_rules! shader { }; } -pub const VERT: Shader = shader!("shader.vert", "main"); -pub const FRAG: Shader = shader!("shader.frag", "main"); +pub const VERT: Shader = shader!("shader.vert", c"main"); +pub const FRAG: Shader = shader!("shader.frag", c"main"); diff --git a/Engine/Source/engine/Cargo.toml b/Engine/Source/engine/Cargo.toml index d81f4ca..061970f 100644 --- a/Engine/Source/engine/Cargo.toml +++ b/Engine/Source/engine/Cargo.toml @@ -12,4 +12,10 @@ thiserror.workspace = true log.workspace = true logging.workspace = true +utils.workspace = true platform.workspace = true +renderer.workspace = true +world.workspace = true +resource_manager.workspace = true + +glam.workspace = true diff --git a/Engine/Source/engine/src/errors.rs b/Engine/Source/engine/src/errors.rs deleted file mode 100644 index 3f4054b..0000000 --- a/Engine/Source/engine/src/errors.rs +++ /dev/null @@ -1,13 +0,0 @@ -use thiserror::Error; - -pub type InitResult = std::result::Result; -#[derive(Debug, Error)] -pub enum EngineInitError {} - -pub type RenderResult = std::result::Result; -#[derive(Debug, Error)] -pub enum EngineRenderError {} - -pub type ShutdownResult = std::result::Result; -#[derive(Debug, Error)] -pub enum EngineShutdownError {} diff --git a/Engine/Source/engine/src/lib.rs b/Engine/Source/engine/src/lib.rs index 83dfe77..2dbfbb9 100644 --- a/Engine/Source/engine/src/lib.rs +++ b/Engine/Source/engine/src/lib.rs @@ -1,31 +1,46 @@ -use std::time::Instant; +use std::{collections::HashMap, f32::consts::PI, ffi::CString, str::FromStr, time::Instant}; use anyhow::Context; - -use crate::errors::{InitResult, RenderResult, ShutdownResult}; - -mod errors; +use world::{World, WorldId}; /// This is the main struct that holds global engine state. pub struct Engine { + // order is important as renderer should be dropped before platform + renderer: renderer::Renderer, platform: platform::Platform, + + next_world_id: WorldId, + worlds: HashMap, + is_requesting_exit: bool, exit_error: Option, last_tick: Instant, } impl Engine { - pub fn init() -> InitResult { + pub fn init() -> anyhow::Result { let logger = logging::Logger::new(true, true, true); logging::init(logger); - let platform = platform::Platform::init(); + let version = utils::Version::from_str(env!("CARGO_PKG_VERSION"))?; + let name = "DirkEngine"; + + let platform = platform::Platform::init().context("platform init")?; + let renderer = renderer::Renderer::init( + renderer::RendererCreateInfo { + engine_name: CString::from_str(name)?, + engine_version: version, + app_name: CString::from_str(name)?, + app_version: version, + }, + platform.main_window(), + ) + .context("renderer init")?; /* A rough idea of the flow of the C++ Engine * * Intialize Main Engine Objects: * - EventManager - * - Renderer * - World * * ImGui: @@ -35,12 +50,96 @@ impl Engine { * * Create main viewport */ - Ok(Self { - is_requesting_exit: false, + let mut engine = Self { platform, + renderer, + + next_world_id: 0, + worlds: HashMap::new(), + + is_requesting_exit: false, exit_error: None, last_tick: Instant::now(), - }) + }; + + // TODO: this should be initialized when needed, not now + engine + .renderer + .upload_model( + resource_manager::ResourceManager::load_model("Shrek").context("loading shrek")?, + ) + .context("uploading shrek")?; + engine + .renderer + .upload_model( + resource_manager::ResourceManager::load_model("Duck").context("loading duck")?, + ) + .context("uploading duck")?; + + let world_id = engine.create_world()?; + + // THIS IS JUST TEMPORARY FOR TESTING + { + use world::components; + let world = engine.worlds.get_mut(&world_id).unwrap(); + + let player = world.spawn(); + world.insert( + player, + components::Transform { + location: glam::vec3(0., 1000., 1000.), + rotation: glam::vec3(0., PI / 2., PI / 2.), + scale: glam::Vec3::splat(1.), + }, + ); + world.insert( + player, + components::Camera { + fov: (45_f32).to_radians(), + near_clip: 0.1, + far_clip: 100000., + width: 100., + height: 100., + }, + ); + + let shrek = world.spawn(); + world.insert( + shrek, + components::Transform { + location: glam::Vec3::ZERO, + rotation: glam::Vec3::ZERO, + scale: glam::Vec3::splat(1.), + }, + ); + world.insert( + shrek, + components::Renderable { + model: "Shrek".to_string(), + }, + ); + + let duck = world.spawn(); + world.insert( + duck, + components::Transform { + location: glam::vec3(100., 0., 0.), + rotation: glam::Vec3::ZERO, + scale: glam::Vec3::splat(1.), + }, + ); + world.insert( + shrek, + components::Renderable { + model: "Duck".to_string(), + }, + ); + + // TODO: see engine::create_world + engine.renderer.create_scene(world)?; + } + + Ok(engine) } /// Engine tick. /// Returns if the engine should continue ticking. @@ -51,6 +150,10 @@ impl Engine { let delta_time = self.capture_delta_time(); + // TODO: renders too fast and semaphores have problem. + // remove when rendering takes longer + std::thread::sleep(std::time::Duration::from_millis(10)); + self.platform.tick(delta_time).context("ticking platform")?; if self.is_requesting_exit() { return Ok(false); @@ -61,11 +164,14 @@ impl Engine { * * World Tick * Main Viewport tick - * Render */ - Ok(self.is_requesting_exit()) + + self.render().context("tick: render")?; + + Ok(!self.is_requesting_exit()) } - pub fn render(&self) -> RenderResult<()> { + pub fn render(&mut self) -> anyhow::Result<()> { + self.renderer.render()?; /* Renderer::render * * ImGui: @@ -78,7 +184,7 @@ impl Engine { */ Ok(()) } - pub fn shutdown(&self) -> ShutdownResult<()> { + pub fn shutdown(&mut self) -> anyhow::Result<()> { /* * Shutdown ImGui (renderer then platform) * @@ -106,4 +212,22 @@ impl Engine { self.last_tick = current_time; delta } + + fn create_world(&mut self) -> anyhow::Result { + let id = self.next_world_id; + self.next_world_id += 1; + + let world = World::new(id); + // TODO: have the world submitted here, once not having camera doesn't panic. + // self.renderer + // .create_scene(&world) + // .context("create renderer scene")?; + self.worlds.insert(id, world); + Ok(id) + } + #[allow(unused)] + fn destroy_world(&mut self, id: WorldId) { + // TODO: delete renderer scene + self.worlds.remove(&id); + } } diff --git a/Engine/Source/platform/Cargo.toml b/Engine/Source/platform/Cargo.toml index 8132f23..806a34b 100644 --- a/Engine/Source/platform/Cargo.toml +++ b/Engine/Source/platform/Cargo.toml @@ -12,5 +12,7 @@ anyhow.workspace = true thiserror.workspace = true winit.workspace = true +utils.workspace = true + [build-dependencies] build.workspace = true diff --git a/Engine/Source/platform/src/errors.rs b/Engine/Source/platform/src/errors.rs index 5ef3dac..7dc8af6 100644 --- a/Engine/Source/platform/src/errors.rs +++ b/Engine/Source/platform/src/errors.rs @@ -1,9 +1,13 @@ use thiserror::Error; -pub type Result = std::result::Result; +pub type Result = std::result::Result; #[derive(Debug, Error)] -pub enum PlatformError { +pub enum Error { #[error("Error in winit event loop: {0}")] EventLoopError(#[from] winit::error::EventLoopError), + #[error("Error fetching handle: {0}")] + HandleError(#[from] winit::raw_window_handle::HandleError), + #[error("Application exited with code {0}")] + AppExited(i32), } diff --git a/Engine/Source/platform/src/handler.rs b/Engine/Source/platform/src/handler.rs index f339943..4718c33 100644 --- a/Engine/Source/platform/src/handler.rs +++ b/Engine/Source/platform/src/handler.rs @@ -11,10 +11,10 @@ use winit::{ use crate::window::Window; /// The object with actual platform state. It handles responding to platform events. -#[derive(Default)] pub struct PlatformHandler { can_create_surfaces: bool, windows: HashMap, + main_window: WindowId, } impl PlatformHandler { @@ -32,11 +32,35 @@ impl PlatformHandler { self.windows.insert(window_id, window); Ok(window_id) } + pub fn main_window(&self) -> &Window { + self.windows + .get(&self.main_window) + .expect("there should always be a main window") + } + pub fn is_initialized(&self) -> bool { + self.can_create_surfaces + } + pub fn shutdown(&mut self) { + let count = self.windows.len(); + self.windows.clear(); + debug!("Closed {count} window(s) during platform shutdown"); + } +} + +impl Default for PlatformHandler { + fn default() -> Self { + Self { + can_create_surfaces: false, + windows: HashMap::new(), + main_window: WindowId::from_raw(0), + } + } } impl ApplicationHandler for PlatformHandler { fn can_create_surfaces(&mut self, event_loop: &dyn winit::event_loop::ActiveEventLoop) { - self.create_window(event_loop) + self.main_window = self + .create_window(event_loop) .expect("failed to create main window"); self.can_create_surfaces = true } @@ -55,6 +79,7 @@ impl ApplicationHandler for PlatformHandler { WindowEvent::CloseRequested | WindowEvent::Destroyed => { debug!("Closing Window={window_id:?}"); self.windows.remove(&window_id); + todo!("if main window: shut down the engine") } WindowEvent::SurfaceResized(size) => { window.resize(size); diff --git a/Engine/Source/platform/src/lib.rs b/Engine/Source/platform/src/lib.rs index cff9c0b..fa22432 100644 --- a/Engine/Source/platform/src/lib.rs +++ b/Engine/Source/platform/src/lib.rs @@ -5,11 +5,19 @@ //! //! The DirkEngine's platform API is build on the winit crate. -use winit::event_loop::{EventLoop, run_on_demand::EventLoopExtRunOnDemand}; +use std::time::Duration; + +use log::info; +use winit::event_loop::{ + EventLoop, + pump_events::{EventLoopExtPumpEvents, PumpStatus}, +}; mod errors; mod handler; mod window; +pub use errors::Error; +pub use window::Window; use errors::Result; use handler::PlatformHandler; @@ -21,14 +29,55 @@ pub struct Platform { } impl Platform { - pub fn init() -> Self { - Self { - handler: handler::PlatformHandler::default(), - event_loop: EventLoop::new().expect("failed to create empty winit event loop"), + pub fn init() -> Result { + let mut platform = Self { + handler: PlatformHandler::default(), + event_loop: EventLoop::new().expect("failed to create winit event loop"), + }; + + // Pump until `can_create_surfaces` fires and the main window exists. + // Each call returns quickly; the OS dispatches the startup events + // within the first few iterations. + while !platform.handler.is_initialized() { + match platform + .event_loop + .pump_app_events(Some(Duration::ZERO), &mut platform.handler) + { + PumpStatus::Exit(code) => return Err(Error::AppExited(code)), + PumpStatus::Continue => {} + } + } + + info!("initialized platform"); + Ok(platform) + } + /// Process all pending OS events without blocking. Returns `Ok(true)` + /// when the application has requested to exit (e.g. last window closed). + pub fn tick(&mut self, _delta_time: f32) -> Result { + match self + .event_loop + .pump_app_events(Some(Duration::ZERO), &mut self.handler) + { + PumpStatus::Exit(code) => { + info!("Event loop exited with code {code}"); + Ok(true) + } + PumpStatus::Continue => Ok(false), } } - pub fn tick(&mut self, _delta_time: f32) -> Result<()> { - // TODO: maybe listen on a separate thread in the future - Ok(self.event_loop.run_app_on_demand(&mut self.handler)?) + + pub fn main_window(&self) -> &Window { + self.handler.main_window() + } +} + +impl Drop for Platform { + fn drop(&mut self) { + info!("Shutting down platform"); + self.handler.shutdown(); + // One final pump so winit can process the window destruction + // events before we tear everything down. + self.event_loop + .pump_app_events(Some(Duration::ZERO), &mut self.handler); } } diff --git a/Engine/Source/platform/src/window.rs b/Engine/Source/platform/src/window.rs index 003ffe3..1230f23 100644 --- a/Engine/Source/platform/src/window.rs +++ b/Engine/Source/platform/src/window.rs @@ -2,6 +2,7 @@ use log::debug; use winit::{ dpi::PhysicalSize, keyboard::ModifiersState, + raw_window_handle::{HasDisplayHandle, HasWindowHandle}, window::{Theme, WindowId}, }; @@ -36,6 +37,9 @@ impl Window { self.window.request_redraw(); } + pub fn size(&self) -> PhysicalSize { + self.window.surface_size() + } /// Update if window is focused. This only updates internal state, do /// not call if you want to focus the window; pub fn focused(&mut self, focused: bool) { @@ -55,3 +59,21 @@ impl Window { &self.modifiers } } + +impl HasWindowHandle for Window { + fn window_handle( + &self, + ) -> Result, winit::raw_window_handle::HandleError> + { + self.window.window_handle() + } +} + +impl HasDisplayHandle for Window { + fn display_handle( + &self, + ) -> Result, winit::raw_window_handle::HandleError> + { + self.window.display_handle() + } +} diff --git a/Engine/Source/renderer/Cargo.toml b/Engine/Source/renderer/Cargo.toml new file mode 100644 index 0000000..81aa08e --- /dev/null +++ b/Engine/Source/renderer/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "renderer" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[dependencies] +log.workspace = true +thiserror.workspace = true +anyhow.workspace = true + +utils.workspace = true +platform.workspace = true +resource_manager.workspace = true +world.workspace = true +shaders.workspace = true + +ash.workspace = true +ash-window.workspace = true +raw-window-handle.workspace = true +glam.workspace = true + +[build-dependencies] +build.workspace = true diff --git a/Engine/Source/renderer/build.rs b/Engine/Source/renderer/build.rs new file mode 100644 index 0000000..b321798 --- /dev/null +++ b/Engine/Source/renderer/build.rs @@ -0,0 +1,10 @@ +fn main() { + build::configure_platform(); + + println!("cargo:rustc-check-cfg=cfg(validation)"); + + let profile = std::env::var("PROFILE").unwrap_or_default(); + if profile != "release" { + println!("cargo:rustc-cfg=validation"); + } +} diff --git a/Engine/Source/renderer/src/command_pool.rs b/Engine/Source/renderer/src/command_pool.rs new file mode 100644 index 0000000..8a47c07 --- /dev/null +++ b/Engine/Source/renderer/src/command_pool.rs @@ -0,0 +1,156 @@ +use std::{marker::PhantomData, ops::Deref}; + +use ash::{Device, vk}; + +use crate::{Queues, Result, physical_device::QueueFamilyIndices}; + +#[derive(Debug)] +pub struct Graphics; +#[derive(Debug)] +pub struct Transfer; +#[derive(Debug)] +#[allow(unused)] +pub struct Compute; + +/// Wrapper for [vk::CommandPool]. +#[derive(Debug)] +pub struct CommandPool { + /// The command pool + pool: vk::CommandPool, + /// The queue commands will be submitted to + queue: vk::Queue, + pool_type: PhantomData, +} + +pub trait Pool { + fn get_index(families: &QueueFamilyIndices) -> u32; + fn get_queue(queues: &Queues) -> vk::Queue; +} + +impl Pool for Compute { + fn get_index(families: &QueueFamilyIndices) -> u32 { + families.compute + } + fn get_queue(queues: &Queues) -> vk::Queue { + queues.compute + } +} + +impl Pool for Transfer { + fn get_index(families: &QueueFamilyIndices) -> u32 { + families.transfer + } + fn get_queue(queues: &Queues) -> vk::Queue { + queues.transfer + } +} + +impl Pool for Graphics { + fn get_index(families: &QueueFamilyIndices) -> u32 { + families.graphics + } + fn get_queue(queues: &Queues) -> vk::Queue { + queues.graphics + } +} + +impl CommandPool { + /// Will build a command pool with the specified settings. + /// Please make sure `pool_type` matches the families of the queue + /// index and the full queue object. + pub fn build( + device: &Device, + queues: &Queues, + families: &QueueFamilyIndices, + flags: vk::CommandPoolCreateFlags, + ) -> Result { + let index = Type::get_index(families); + let queue = Type::get_queue(queues); + + let info = vk::CommandPoolCreateInfo::default() + .flags(flags) + .queue_family_index(index); + + let pool = unsafe { device.create_command_pool(&info, None)? }; + + Ok(Self { + pool, + queue, + pool_type: PhantomData, + }) + } + pub fn destroy(&self, device: &Device) { + unsafe { + device.destroy_command_pool(self.pool, None); + } + } + pub fn allocate_buffer(&self, device: &Device) -> Result { + let allocate_info = vk::CommandBufferAllocateInfo::default() + .command_pool(self.pool) + .level(vk::CommandBufferLevel::PRIMARY) + .command_buffer_count(1); + let buff = unsafe { device.allocate_command_buffers(&allocate_info)?[0] }; + + Ok(CommandBuffer { + buff, + queue: self.queue, + }) + } + + pub fn begin_single_time(&self, device: &Device) -> Result { + let alloc_info = vk::CommandBufferAllocateInfo::default() + .command_pool(self.pool) + .level(vk::CommandBufferLevel::PRIMARY) + .command_buffer_count(1); + + let buff = unsafe { device.allocate_command_buffers(&alloc_info)?[0] }; + + let begin_info = vk::CommandBufferBeginInfo::default() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); + + unsafe { device.begin_command_buffer(buff, &begin_info)? }; + Ok(CommandBuffer { + buff, + queue: self.queue, + }) + } +} + +/// Wrapper for [vk::CommandBuffer]. +pub struct CommandBuffer { + /// The buffer + buff: vk::CommandBuffer, + /// The queue to submit to + queue: vk::Queue, +} + +impl CommandBuffer { + pub fn raw(&self) -> vk::CommandBuffer { + self.buff + } + pub fn submit( + &self, + device: &Device, + submit_info: vk::SubmitInfo, + fence: vk::Fence, + ) -> Result<()> { + unsafe { device.queue_submit(self.queue, std::slice::from_ref(&submit_info), fence)? }; + Ok(()) + } + pub fn end_and_submit(&self, device: &Device) -> Result<()> { + let info = vk::SubmitInfo::default().command_buffers(std::slice::from_ref(&self.buff)); + unsafe { + device.end_command_buffer(self.buff)?; + device.queue_submit(self.queue, std::slice::from_ref(&info), vk::Fence::null())?; + }; + Ok(()) + } +} + +impl Deref for CommandBuffer { + type Target = vk::CommandBuffer; + + fn deref(&self) -> &Self::Target { + &self.buff + } +} diff --git a/Engine/Source/renderer/src/errors.rs b/Engine/Source/renderer/src/errors.rs new file mode 100644 index 0000000..40d8219 --- /dev/null +++ b/Engine/Source/renderer/src/errors.rs @@ -0,0 +1,44 @@ +use ash::vk; +use raw_window_handle::HandleError; +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Vulkan error: {0}")] + VulkanError(#[from] ash::vk::Result), + + #[error("Error loading Vulkan functions: {0}")] + Loading(#[from] ash::LoadingError), + #[error("platform error: {0}")] + Platform(#[from] platform::Error), + #[error("resource manager error: {0}")] + ResourceManager(#[from] resource_manager::Error), + + #[error("no suitable graphics device found")] + NoDeviceFound, + #[error("failed to find supported format")] + NoSupportedFormat, + #[error("failed to find a suitable memory type")] + NoSuitableMemoryType, + + #[error("instance extension {0} not found")] + ExtensionNotFound(String), + #[error("validation layer {0} not found")] + ValidationLayerNotFound(String), + + #[error("the layout {0:?} is not supported as a source. implement it")] + UnsupportedSourceLayout(vk::ImageLayout), + #[error("the layout {0:?} is not supported as a destination. implement it")] + UnsupportedDesinationLayout(vk::ImageLayout), + + #[error("suboptimal surface")] + SuboptimalSurface, +} + +impl From for Error { + fn from(value: HandleError) -> Self { + Error::Platform(value.into()) + } +} diff --git a/Engine/Source/renderer/src/layouts.rs b/Engine/Source/renderer/src/layouts.rs new file mode 100644 index 0000000..729ec79 --- /dev/null +++ b/Engine/Source/renderer/src/layouts.rs @@ -0,0 +1,110 @@ +use ash::vk; + +use crate::{Error, Renderer, Result, command_pool::CommandBuffer}; + +impl Renderer { + pub fn transition_image_layout( + &self, + cmd: &CommandBuffer, + image: vk::Image, + old_layout: vk::ImageLayout, + new_layout: vk::ImageLayout, + mip_levels: u32, + base_mip: u32, + ) -> Result<()> { + let (src_access, src_stage) = Self::src_layout_info(old_layout)?; + let (dst_access, dst_stage) = Self::dst_layout_info(new_layout)?; + + let barrier = vk::ImageMemoryBarrier::default() + .old_layout(old_layout) + .new_layout(new_layout) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .src_access_mask(src_access) + .dst_access_mask(dst_access) + .image(image) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: base_mip, + level_count: mip_levels, + base_array_layer: 0, + layer_count: 1, + }); + + unsafe { + self.device.cmd_pipeline_barrier( + cmd.raw(), + src_stage, + dst_stage, + vk::DependencyFlags::empty(), + &[], + &[], + &[barrier], + ) + }; + Ok(()) + } + fn src_layout_info( + layout: vk::ImageLayout, + ) -> Result<(vk::AccessFlags, vk::PipelineStageFlags)> { + match layout { + vk::ImageLayout::UNDEFINED => Ok(( + vk::AccessFlags::empty(), + vk::PipelineStageFlags::TOP_OF_PIPE, + )), + vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL => Ok(( + vk::AccessFlags::COLOR_ATTACHMENT_WRITE, + vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, + )), + vk::ImageLayout::TRANSFER_DST_OPTIMAL => Ok(( + vk::AccessFlags::TRANSFER_WRITE, + vk::PipelineStageFlags::TRANSFER, + )), + vk::ImageLayout::TRANSFER_SRC_OPTIMAL => Ok(( + vk::AccessFlags::TRANSFER_READ, + vk::PipelineStageFlags::TRANSFER, + )), + vk::ImageLayout::PRESENT_SRC_KHR => Ok(( + vk::AccessFlags::empty(), + vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, + )), + vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL => Ok(( + vk::AccessFlags::SHADER_READ, + vk::PipelineStageFlags::FRAGMENT_SHADER, + )), + _ => Err(Error::UnsupportedSourceLayout(layout)), + } + } + fn dst_layout_info( + layout: vk::ImageLayout, + ) -> Result<(vk::AccessFlags, vk::PipelineStageFlags)> { + match layout { + vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL => Ok(( + vk::AccessFlags::COLOR_ATTACHMENT_WRITE, + vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, + )), + vk::ImageLayout::TRANSFER_DST_OPTIMAL => Ok(( + vk::AccessFlags::TRANSFER_WRITE, + vk::PipelineStageFlags::TRANSFER, + )), + vk::ImageLayout::TRANSFER_SRC_OPTIMAL => Ok(( + vk::AccessFlags::TRANSFER_READ, + vk::PipelineStageFlags::TRANSFER, + )), + vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL => Ok(( + vk::AccessFlags::SHADER_READ, + vk::PipelineStageFlags::FRAGMENT_SHADER, + )), + vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL => Ok(( + vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_READ + | vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE, + vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS, + )), + vk::ImageLayout::PRESENT_SRC_KHR => Ok(( + vk::AccessFlags::empty(), + vk::PipelineStageFlags::BOTTOM_OF_PIPE, + )), + _ => Err(Error::UnsupportedDesinationLayout(layout)), + } + } +} diff --git a/Engine/Source/renderer/src/lib.rs b/Engine/Source/renderer/src/lib.rs new file mode 100644 index 0000000..0776a22 --- /dev/null +++ b/Engine/Source/renderer/src/lib.rs @@ -0,0 +1,1342 @@ +#[cfg(validation)] +use std::os::raw::c_void; +use std::{ + collections::{HashMap, HashSet}, + ffi::{CStr, CString}, +}; + +#[cfg(validation)] +use ash::ext::debug_utils; +#[cfg(platform_linux)] +use ash::khr::wayland_surface; +use ash::{ + Device, Entry, Instance, + khr::{surface, swapchain}, + vk, +}; +use log::{debug, error, info, trace, warn}; + +mod errors; +pub use errors::{Error, Result}; + +mod physical_device; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; + +mod model; +use model::*; + +mod scene; +use scene::Scene; + +mod window; +use window::{Window, WindowId}; + +mod pipeline; +use pipeline::GraphicsPipeline; + +mod command_pool; +use command_pool::{CommandBuffer, CommandPool, Graphics, Transfer}; + +mod layouts; +mod render_pass; + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +struct Vertex { + position: [f32; 3], + normal: [f32; 3], + texcoord: [f32; 2], +} + +impl Vertex { + const fn binding_description() -> vk::VertexInputBindingDescription { + vk::VertexInputBindingDescription { + binding: 0, + stride: size_of::() as u32, + input_rate: vk::VertexInputRate::VERTEX, + } + } + const fn attribute_description() -> [vk::VertexInputAttributeDescription; 3] { + [ + vk::VertexInputAttributeDescription { + location: 0, + binding: 0, + format: vk::Format::R32G32B32_SFLOAT, + offset: std::mem::offset_of!(Self, position) as u32, + }, + vk::VertexInputAttributeDescription { + location: 1, + binding: 0, + format: vk::Format::R32G32B32_SFLOAT, + offset: std::mem::offset_of!(Self, normal) as u32, + }, + vk::VertexInputAttributeDescription { + location: 2, + binding: 0, + format: vk::Format::R32G32_SFLOAT, + offset: std::mem::offset_of!(Self, texcoord) as u32, + }, + ] + } +} + +fn make_version(version: utils::Version) -> u32 { + vk::make_api_version(0, version.major(), version.minor(), version.patch()) +} + +const MAX_FRAMES_IN_FLIGHT: usize = 2; +const DEVICE_EXTENSIONS: &[&str] = + &[unsafe { std::str::from_utf8_unchecked(swapchain::NAME.to_bytes()) }]; +#[cfg(validation)] +const VALIDATION_LAYERS: &[*const i8] = &[c"VK_LAYER_KHRONOS_validation".as_ptr()]; + +#[derive(Debug)] +struct Frame { + /// Command pool to allocate command buffers on every frame + command_pool: CommandPool, + /// Main synchronization fence + fence: vk::Fence, + // TODO: have one primary command buffer that is allocated once and + // secondary command for each scene. Should be allocated every time + // there is a change in scene count. If not reallocated, reset. +} + +impl Frame { + fn destroy(&self, device: &Device) { + self.command_pool.destroy(device); + unsafe { + device.destroy_fence(self.fence, None); + } + } +} + +/// This struct is owned by [Renderer] and stores +/// all the different descriptor set layouts used by +/// the renderer. +/// Every field should be a descriptor set layout with a +/// propper comment explain what the layout is and where +/// it is used. +struct DescriptorLayouts { + // TODO: much better comments for descriptor set layouts + /// Per scene layout. Holds view & proj matrices for rendering. + scene: vk::DescriptorSetLayout, + /// Per object layout. For model matrix. + object: vk::DescriptorSetLayout, + /// Per material layout. For texture descriptor. + material: vk::DescriptorSetLayout, +} + +impl DescriptorLayouts { + fn destroy(&self, device: &Device) { + unsafe { + device.destroy_descriptor_set_layout(self.scene, None); + device.destroy_descriptor_set_layout(self.object, None); + device.destroy_descriptor_set_layout(self.material, None); + } + } +} + +pub struct RendererCreateInfo { + pub engine_name: CString, + pub engine_version: utils::Version, + pub app_name: CString, + pub app_version: utils::Version, +} + +struct Queues { + graphics: vk::Queue, + compute: vk::Queue, + transfer: vk::Queue, + present: vk::Queue, +} + +pub struct RendererProperties { + msaa_samples: vk::SampleCountFlags, + #[allow(unused)] + anisotropy: bool, + surface_format: vk::SurfaceFormatKHR, + queue_family_indices: physical_device::QueueFamilyIndices, + depth_format: vk::Format, + present_mode: vk::PresentModeKHR, +} + +/// The Renderer struct that holds all render state and is called upon to handle +/// all rendering operations +pub struct Renderer { + entry: Entry, + + // Renderer Resources + instance: Instance, + device: Device, + queues: Queues, + physical_device: vk::PhysicalDevice, + /// For single use buffers. + /// Used for texture uploads and layout transitions. + transfer_pool: CommandPool, + /// For single use buffers. + /// Used for mip generation. + graphics_pool: CommandPool, + + properties: RendererProperties, + /// The ID of the main window in [Renderer::windows] field. + main_window: WindowId, + /// All of the [window::Window]s constructed from [platform::Window]s. + windows: HashMap, + /// All the uploaded [resource_manager::Model]s. + models: HashMap, + /// All of the internal [world::World] representations. + scenes: HashMap, + /// All the descriptor layouts used in the renderer. + layouts: DescriptorLayouts, + material_descriptor_pool: vk::DescriptorPool, + + frames: [Frame; MAX_FRAMES_IN_FLIGHT], + current_frame: usize, + + // Extensions + surface_loader: surface::Instance, + swapchain_loader: swapchain::Device, + #[cfg(validation)] + debug_utils_loader: debug_utils::Instance, + #[cfg(validation)] + debug_messenger: vk::DebugUtilsMessengerEXT, + + graphics_pipeline: GraphicsPipeline, +} + +impl Renderer { + pub fn init(create_info: RendererCreateInfo, window: &platform::Window) -> Result { + info!("Intializing Vulkan..."); + + let entry = unsafe { Entry::load()? }; + + let (instance, debug_utils_loader, debug_messenger) = { + let app_info = vk::ApplicationInfo::default() + .application_name(create_info.app_name.as_c_str()) + .application_version(make_version(create_info.app_version)) + .engine_name(create_info.engine_name.as_c_str()) + .engine_version(make_version(create_info.engine_version)) + .api_version(vk::API_VERSION_1_3); + + // Collect extensions + let mut extensions: Vec<*const i8> = vec![surface::NAME.as_ptr()]; + + #[cfg(platform_linux)] + extensions.push(wayland_surface::NAME.as_ptr()); + + let mut instance_create_info = + vk::InstanceCreateInfo::default().application_info(&app_info); + + #[cfg(validation)] + let mut debug_create_info: vk::DebugUtilsMessengerCreateInfoEXT; + #[cfg(validation)] + { + info!("using validation layers"); + extensions.push(debug_utils::NAME.as_ptr()); + + let severity_flags = vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE + | vk::DebugUtilsMessageSeverityFlagsEXT::INFO + | vk::DebugUtilsMessageSeverityFlagsEXT::WARNING + | vk::DebugUtilsMessageSeverityFlagsEXT::ERROR; + + let message_type_flags = vk::DebugUtilsMessageTypeFlagsEXT::GENERAL + | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE + | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION; + + debug_create_info = vk::DebugUtilsMessengerCreateInfoEXT::default() + .message_severity(severity_flags) + .message_type(message_type_flags) + .pfn_user_callback(Some(debug_callback)); + + let validation_layers = VALIDATION_LAYERS; + + // check validation layer support + { + let available = unsafe { + entry + .enumerate_instance_layer_properties() + .unwrap_or_default() + }; + for &required in validation_layers { + let required = unsafe { CStr::from_ptr(required) }; + let found = available.iter().any( + |ext| unsafe { CStr::from_ptr(ext.layer_name.as_ptr()) } == required, + ); + + if !found { + return Err(Error::ValidationLayerNotFound( + required.to_string_lossy().into_owned(), + )); + } + } + } + + instance_create_info = instance_create_info + .enabled_layer_names(VALIDATION_LAYERS) + .push_next(&mut debug_create_info); + } + + // check required instance extensions + { + let available = unsafe { + entry + .enumerate_instance_extension_properties(None) + .unwrap_or_default() + }; + for &required in &extensions { + let required = unsafe { CStr::from_ptr(required) }; + let found = available.iter().any( + |ext| unsafe { CStr::from_ptr(ext.extension_name.as_ptr()) } == required, + ); + + if !found { + return Err(Error::ExtensionNotFound( + required.to_string_lossy().into_owned(), + )); + } + } + } + + instance_create_info = instance_create_info.enabled_extension_names(&extensions); + + let instance = unsafe { entry.create_instance(&instance_create_info, None)? }; + + let (debug_utils_loader, debug_messenger) = { + let loader = debug_utils::Instance::new(&entry, &instance); + let messenger = + unsafe { loader.create_debug_utils_messenger(&debug_create_info, None)? }; + (loader, messenger) + }; + + (instance, debug_utils_loader, debug_messenger) + }; + + let (surface_loader, surface) = { + let surface = unsafe { + ash_window::create_surface( + &entry, + &instance, + window.display_handle()?.as_raw(), + window.window_handle()?.as_raw(), + None, + )? + }; + let loader = surface::Instance::new(&entry, &instance); + + (loader, surface) + }; + + // PHYSICAL DEVICE + let (physical_device, properties) = { + let (device_info, queues) = physical_device::PhysicalDeviceSelector::new() + .require_extensions(DEVICE_EXTENSIONS) + .require(|info| info.features.geometry_shader == vk::TRUE) + .select(&instance, &surface_loader, surface) + .ok_or(Error::NoDeviceFound)?; + + info!( + "Physical device selected: {:#?} (vendor: {}, id: {}, api: {}, driver: {})", + device_info + .properties + .device_name_as_c_str() + .unwrap_or_default(), + device_info.properties.vendor_id, + device_info.properties.device_id, + device_info.properties.api_version, + device_info.properties.driver_version + ); + + let formats = unsafe { + surface_loader.get_physical_device_surface_formats(device_info.handle, surface)? + }; + + let surface_format = formats + .iter() + .find(|format| { + format.format == vk::Format::B8G8R8A8_SRGB + && format.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR + }) + .copied() + .unwrap_or(formats[0]); + + let depth_format = *{ + let candidates = &[ + vk::Format::D32_SFLOAT, + vk::Format::D32_SFLOAT_S8_UINT, + vk::Format::D24_UNORM_S8_UINT, + ]; + let features = vk::FormatFeatureFlags::DEPTH_STENCIL_ATTACHMENT; + + candidates + .iter() + .find(|&f| { + let properties = unsafe { + instance.get_physical_device_format_properties(device_info.handle, *f) + }; + properties.optimal_tiling_features.contains(features) + }) + .ok_or(Error::NoSupportedFormat) + }?; + + let msaa_samples = *{ + let counts = device_info + .properties + .limits + .framebuffer_color_sample_counts + & device_info + .properties + .limits + .framebuffer_depth_sample_counts; + [ + vk::SampleCountFlags::TYPE_64, + vk::SampleCountFlags::TYPE_32, + vk::SampleCountFlags::TYPE_16, + vk::SampleCountFlags::TYPE_8, + vk::SampleCountFlags::TYPE_4, + vk::SampleCountFlags::TYPE_2, + ] + .iter() + .find(|&flag| counts.contains(*flag)) + .unwrap_or(&vk::SampleCountFlags::TYPE_1) + }; + + let present_mode = { + let modes = unsafe { + surface_loader + .get_physical_device_surface_present_modes(device_info.handle, surface)? + }; + + *modes + .iter() + .find(|&mode| *mode == vk::PresentModeKHR::MAILBOX) + .unwrap_or(&vk::PresentModeKHR::FIFO) + }; + + let properties = RendererProperties { + msaa_samples, + anisotropy: device_info.features.sampler_anisotropy == vk::TRUE, + surface_format, + queue_family_indices: queues, + depth_format, + present_mode, + }; + + (device_info.handle, properties) + }; + + // DEVICE + let device = { + let unique_families: HashSet = [ + properties.queue_family_indices.graphics, + properties.queue_family_indices.present, + properties.queue_family_indices.compute, + properties.queue_family_indices.transfer, + ] + .iter() + .cloned() + .collect(); + + // only one queue per family, so all 1.0 priority + let queue_priorities = vec![1.0_f32]; + let queue_create_infos: Vec = unique_families + .iter() + .map(|&family| { + vk::DeviceQueueCreateInfo::default() + .queue_family_index(family) + .queue_priorities(&queue_priorities) + }) + .collect(); + + let physical_device_features = + vk::PhysicalDeviceFeatures::default().sampler_anisotropy(true); + let mut vulkan13_features = + vk::PhysicalDeviceVulkan13Features::default().dynamic_rendering(true); + + let extensions: Vec<*const i8> = DEVICE_EXTENSIONS + .iter() + .map(|name| unsafe { std::mem::transmute(name.as_ptr()) }) + .collect(); + let device_create_info = vk::DeviceCreateInfo::default() + .queue_create_infos(&queue_create_infos) + .enabled_features(&physical_device_features) + .enabled_extension_names(&extensions) + .push_next(&mut vulkan13_features); + + unsafe { instance.create_device(physical_device, &device_create_info, None)? } + }; + + // QUEUES + let queues = { + let indices = &properties.queue_family_indices; + Queues { + graphics: unsafe { device.get_device_queue(indices.graphics, 0) }, + present: unsafe { device.get_device_queue(indices.present, 0) }, + compute: unsafe { device.get_device_queue(indices.compute, 0) }, + transfer: unsafe { device.get_device_queue(indices.transfer, 0) }, + } + }; + + // SWAP CHAIN + let swapchain_loader = swapchain::Device::new(&instance, &device); + + // IN FLIGHT FRAMES + let frames: Result> = (0..MAX_FRAMES_IN_FLIGHT) + .map(|_| { + let command_pool = CommandPool::build( + &device, + &queues, + &properties.queue_family_indices, + vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER, + )?; + let fence = unsafe { + device.create_fence( + &vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED), + None, + )? + }; + + Ok(Frame { + command_pool, + fence, + }) + }) + .collect(); + let frames: [Frame; MAX_FRAMES_IN_FLIGHT] = frames?.try_into().unwrap(); + + // LAYOUTS + let layouts = DescriptorLayouts { + scene: { + let binding = vk::DescriptorSetLayoutBinding::default() + .binding(0) + .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER) + .descriptor_count(1) + .stage_flags(vk::ShaderStageFlags::VERTEX); + + let info = vk::DescriptorSetLayoutCreateInfo::default() + .bindings(std::slice::from_ref(&binding)); + + unsafe { device.create_descriptor_set_layout(&info, None)? } + }, + object: { + let binding = vk::DescriptorSetLayoutBinding::default() + .binding(1) + .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER) + .descriptor_count(1) + .stage_flags(vk::ShaderStageFlags::VERTEX); + + let info = vk::DescriptorSetLayoutCreateInfo::default() + .bindings(std::slice::from_ref(&binding)); + + unsafe { device.create_descriptor_set_layout(&info, None)? } + }, + material: { + let binding = vk::DescriptorSetLayoutBinding::default() + .binding(2) + .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) + .descriptor_count(1) + .stage_flags(vk::ShaderStageFlags::FRAGMENT); + + let info = vk::DescriptorSetLayoutCreateInfo::default() + .bindings(std::slice::from_ref(&binding)); + + unsafe { device.create_descriptor_set_layout(&info, None)? } + }, + }; + + let graphics_pipeline = GraphicsPipeline::build(&device, &layouts, &properties)?; + + // MATERIAL DESCRIPTOR SETS + let material_descriptor_pool = { + const MAX_MATERIAL_DESCRIPTOR_SET: u32 = 256; + let pool_size = vk::DescriptorPoolSize { + ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER, + descriptor_count: MAX_MATERIAL_DESCRIPTOR_SET, + }; + let pool_info = vk::DescriptorPoolCreateInfo::default() + .pool_sizes(std::slice::from_ref(&pool_size)) + .max_sets(MAX_MATERIAL_DESCRIPTOR_SET); + + unsafe { device.create_descriptor_pool(&pool_info, None)? } + }; + + let transfer_pool = CommandPool::build( + &device, + &queues, + &properties.queue_family_indices, + vk::CommandPoolCreateFlags::TRANSIENT, + )?; + let graphics_pool = CommandPool::build( + &device, + &queues, + &properties.queue_family_indices, + vk::CommandPoolCreateFlags::TRANSIENT, + )?; + + let mut renderer = Self { + entry, + instance, + device, + queues, + physical_device, + properties, + transfer_pool, + graphics_pool, + main_window: window.id().into_raw(), + windows: HashMap::new(), + models: HashMap::new(), + scenes: HashMap::new(), + layouts, + material_descriptor_pool, + frames, + current_frame: 0, + surface_loader, + swapchain_loader, + debug_utils_loader, + debug_messenger, + + graphics_pipeline, + }; + + let window_size = window.size(); + let size = vk::Extent2D { + width: window_size.width, + height: window_size.height, + }; + let window = window::Window::build(window.id().into_raw(), &renderer, surface, size)?; + renderer.windows.insert(window.id(), window); + + Ok(renderer) + } + + pub fn render(&mut self) -> Result<()> { + let window = self.windows.get_mut(&self.main_window).unwrap(); + let (swapchain_img, idx) = window.next_image(&self.swapchain_loader)?; + let (render_finished_semaphore, image_available_semaphore) = window.current_semaphores(); + let size = window.extent(); + let swapchain = window.swapchain(); + + let frame = self.get_current_frame(); + + unsafe { + self.device + .wait_for_fences(std::slice::from_ref(&frame.fence), true, u64::MAX)?; + self.device + .reset_fences(std::slice::from_ref(&frame.fence))?; + } + + let cmd = frame.command_pool.allocate_buffer(&self.device)?; + + unsafe { + self.device + .begin_command_buffer(cmd.raw(), &vk::CommandBufferBeginInfo::default())? + } + + self.transition_image_layout( + &cmd, + swapchain_img.image, + vk::ImageLayout::UNDEFINED, + vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL, + 1, + 0, + )?; + + for scene in self.scenes.values() { + scene.render(self, &cmd, size, swapchain_img.view)?; + } + + self.transition_image_layout( + &cmd, + swapchain_img.image, + vk::ImageLayout::UNDEFINED, + vk::ImageLayout::PRESENT_SRC_KHR, + 1, + 0, + )?; + + unsafe { self.device.end_command_buffer(cmd.raw())? } + + let submit_info = vk::SubmitInfo::default() + .wait_dst_stage_mask(std::slice::from_ref( + &vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, + )) + .command_buffers(std::slice::from_ref(&cmd)) + .wait_semaphores(std::slice::from_ref(&image_available_semaphore)) + .signal_semaphores(std::slice::from_ref(&render_finished_semaphore)); + + unsafe { + self.device.queue_submit( + self.queues.graphics, + std::slice::from_ref(&submit_info), + frame.fence, + )? + } + + let present_info = vk::PresentInfoKHR::default() + .wait_semaphores(std::slice::from_ref(&render_finished_semaphore)) + .swapchains(std::slice::from_ref(&swapchain)) + .image_indices(std::slice::from_ref(&idx)); + + unsafe { + self.swapchain_loader + .queue_present(self.queues.present, &present_info)? + }; + + self.current_frame = (self.current_frame + 1) % MAX_FRAMES_IN_FLIGHT; + Ok(()) + } + fn get_current_frame(&self) -> &Frame { + &self.frames[self.current_frame] + } + + // SCENES + + pub fn create_scene(&mut self, world: &world::World) -> Result<()> { + self.scenes.insert(world.id(), Scene::build(self, world)?); + Ok(()) + } + + // WINDOW MANAGEMENT + + pub fn create_window(&mut self, plat_window: &platform::Window) -> Result { + let surface = unsafe { + ash_window::create_surface( + &self.entry, + &self.instance, + plat_window.display_handle()?.as_raw(), + plat_window.window_handle()?.as_raw(), + None, + )? + }; + + let window_size = plat_window.size(); + let size = vk::Extent2D { + width: window_size.width, + height: window_size.height, + }; + + let window = window::Window::build(plat_window.id().into_raw(), self, surface, size)?; + self.windows.insert(window.id(), window); + Ok(plat_window.id().into_raw()) + } + // TODO: resize window + /* + pub fn resize_window(&mut self, id: WindowId, width: u32, height: u32) -> Result<()> { + if let Some(window) = self.windows.get_mut(&id) { + // Wait for the GPU to be idle before touching the swapchain. + unsafe { self.device.device_wait_idle()? }; + window.resize(self, vk::Extent2D { width, height })?; + } + Ok(()) + } + */ + + fn create_swap_chain( + &self, + surface: vk::SurfaceKHR, + window_size: vk::Extent2D, + ) -> Result<(vk::SwapchainKHR, vk::Extent2D, Vec)> { + let capabilities = unsafe { + self.surface_loader + .get_physical_device_surface_capabilities(self.physical_device, surface)? + }; + + let extent = if capabilities.current_extent.width != u32::MAX { + capabilities.current_extent + } else { + vk::Extent2D { + width: window_size.width.clamp( + capabilities.min_image_extent.width, + capabilities.max_image_extent.width, + ), + height: window_size.height.clamp( + capabilities.min_image_extent.height, + capabilities.max_image_extent.height, + ), + } + }; + + let mut image_count = capabilities.min_image_count + 1; + if capabilities.max_image_count > 0 && image_count > capabilities.max_image_count { + image_count = capabilities.max_image_count; + } + + let indices = &self.properties.queue_family_indices; + // Deduplicate — concurrent mode requires unique family indices + let mut unique_indices: Vec = + vec![indices.graphics, indices.present, indices.transfer]; + unique_indices.sort_unstable(); + unique_indices.dedup(); + + let (sharing_mode, indices_slice): (vk::SharingMode, &[u32]) = if unique_indices.len() > 1 { + (vk::SharingMode::CONCURRENT, &unique_indices) + } else { + (vk::SharingMode::EXCLUSIVE, &[]) + }; + + let create_info = vk::SwapchainCreateInfoKHR::default() + .surface(surface) + .min_image_count(image_count) + .image_format(self.properties.surface_format.format) + .image_color_space(self.properties.surface_format.color_space) + .image_extent(extent) + .image_array_layers(1) + .image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT) + .image_sharing_mode(sharing_mode) + .queue_family_indices(indices_slice) + .pre_transform(capabilities.current_transform) + .composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE) + .present_mode(self.properties.present_mode) + .clipped(true); + + let swapchain = unsafe { self.swapchain_loader.create_swapchain(&create_info, None)? }; + let images = unsafe { self.swapchain_loader.get_swapchain_images(swapchain)? }; + + let swap_images = images + .into_iter() + .map(|image| { + let view = self.create_image_view( + image, + self.properties.surface_format.format, + vk::ImageAspectFlags::COLOR, + 1, + )?; + Ok(window::SwapchainImage { image, view }) + }) + .collect::>>()?; + + Ok((swapchain, extent, swap_images)) + } + + // UPLOADING TO THE RENDERER + + pub fn upload_model(&mut self, model: resource_manager::Model) -> Result<&Model> { + let primitives = model + .meshes() + .iter() + .flat_map(|m| m.primitives().iter()) + .map(|p| self.upload_primitive(p)) + .collect::>()?; + + let textures: Vec<_> = model + .textures() + .iter() + .map(|t| self.upload_texture(t)) + .collect::>()?; + + let material_count = model.materials().len(); + // Allocate one set per material + let layouts: Vec = vec![self.layouts.material; material_count]; + + let material_sets: Vec = if material_count > 0 { + let alloc_info = vk::DescriptorSetAllocateInfo::default() + .descriptor_pool(self.material_descriptor_pool) + .set_layouts(&layouts); + + unsafe { self.device.allocate_descriptor_sets(&alloc_info)? } + } else { + Vec::new() + }; + + // Write the base-colour sampler into each set that has one + for (i, mat) in model.materials().iter().enumerate() { + let Some(&tex_idx) = mat.base_color_texture().as_ref() else { + continue; // leave this set in its default (null) state + }; + + let tex = &textures[tex_idx]; + + let image_info = vk::DescriptorImageInfo::default() + .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) + .image_view(tex.view) + .sampler(tex.sampler); + + let write = vk::WriteDescriptorSet::default() + .dst_set(material_sets[i]) + .dst_binding(2) // matches layouts.material + .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) + .image_info(std::slice::from_ref(&image_info)); + + unsafe { self.device.update_descriptor_sets(&[write], &[]) }; + } + + self.models.insert( + model.name().to_string(), + Model { + name: model.name().to_owned(), + primitives, + textures, + materials: model.materials().to_vec(), + material_sets, + }, + ); + Ok(self.models.get(model.name()).unwrap()) + } + + fn get_model(&self, name: &str) -> Option<&Model> { + self.models.get(name) + } + + fn upload_primitive(&self, prim: &resource_manager::Primitive) -> Result { + let vertices: Vec = prim + .positions() + .iter() + .enumerate() + .map(|(i, &position)| Vertex { + position, + normal: prim.normals().get(i).copied().unwrap_or([0.0, 1.0, 0.0]), + texcoord: prim.texcoords().get(i).copied().unwrap_or([0.0, 0.0]), + }) + .collect(); + + let (vertex_buffer, vertex_buffer_memory) = + self.upload_slice(&vertices, vk::BufferUsageFlags::VERTEX_BUFFER)?; + + let (index_buffer, index_buffer_memory) = + self.upload_slice(prim.indices(), vk::BufferUsageFlags::INDEX_BUFFER)?; + + Ok(Primitive { + vertex_buffer, + vertex_buffer_memory, + index_buffer, + index_buffer_memory, + index_count: prim.indices().len() as u32, + material: *prim.material(), + }) + } + fn upload_texture(&self, tex: &resource_manager::Texture) -> Result { + let mip_levels = Self::mip_levels(*tex.width(), *tex.height()); + let size = (tex.pixels().len()) as vk::DeviceSize; + let format = vk::Format::R8G8B8A8_SRGB; + + let (staging_buf, staging_mem) = self.create_buffer( + size, + vk::BufferUsageFlags::TRANSFER_SRC, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, + )?; + + unsafe { + let ptr = self + .device + .map_memory(staging_mem, 0, size, vk::MemoryMapFlags::empty())? + as *mut u8; + ptr.copy_from_nonoverlapping(tex.pixels().as_ptr(), tex.pixels().len()); + self.device.unmap_memory(staging_mem); + } + + let (image, memory) = self.create_image( + vk::Extent2D { + width: *tex.width(), + height: *tex.height(), + }, + format, + vk::ImageTiling::OPTIMAL, + // TRANSFER_SRC needed for mip creation + vk::ImageUsageFlags::TRANSFER_DST + | vk::ImageUsageFlags::TRANSFER_SRC + | vk::ImageUsageFlags::SAMPLED, + vk::MemoryPropertyFlags::DEVICE_LOCAL, + (mip_levels, vk::SampleCountFlags::TYPE_1), + )?; + + let cmd = self.graphics_pool.begin_single_time(&self.device)?; + + self.transition_image_layout( + &cmd, + image, + vk::ImageLayout::UNDEFINED, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + mip_levels, // all mip levels start undefined + 0, + )?; + + let region = vk::BufferImageCopy::default() + .image_subresource(vk::ImageSubresourceLayers { + aspect_mask: vk::ImageAspectFlags::COLOR, + mip_level: 0, + base_array_layer: 0, + layer_count: 1, + }) + .image_extent(vk::Extent3D { + width: *tex.width(), + height: *tex.height(), + depth: 1, + }); + + unsafe { + self.device.cmd_copy_buffer_to_image( + cmd.raw(), + staging_buf, + image, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + &[region], + ); + } + + self.generate_mipmaps(&cmd, image, *tex.width(), *tex.height(), mip_levels)?; + cmd.end_and_submit(&self.device)?; + + unsafe { + self.device.destroy_buffer(staging_buf, None); + self.device.free_memory(staging_mem, None); + } + + let view = + self.create_image_view(image, format, vk::ImageAspectFlags::COLOR, mip_levels)?; + let sampler = self.create_sampler(mip_levels)?; + + Ok(Texture { + image, + memory, + view, + sampler, + mip_levels, + }) + } + + // IMAGE UTILITIES + + fn create_image_view( + &self, + image: vk::Image, + format: vk::Format, + aspect_flags: vk::ImageAspectFlags, + mip_levels: u32, + ) -> Result { + let create_info = vk::ImageViewCreateInfo::default() + .image(image) + .view_type(vk::ImageViewType::TYPE_2D) + .format(format) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: aspect_flags, + base_mip_level: 0, + level_count: mip_levels, + base_array_layer: 0, + layer_count: 1, + }); + + Ok(unsafe { self.device.create_image_view(&create_info, None)? }) + } + fn create_image( + &self, + size: vk::Extent2D, + format: vk::Format, + tiling: vk::ImageTiling, + usage: vk::ImageUsageFlags, + properties: vk::MemoryPropertyFlags, + (mip_levels, num_samples): (u32, vk::SampleCountFlags), + ) -> Result<(vk::Image, vk::DeviceMemory)> { + let image_info = vk::ImageCreateInfo::default() + .image_type(vk::ImageType::TYPE_2D) + .format(format) + .extent(vk::Extent3D { + width: size.width, + height: size.height, + depth: 1, + }) + .mip_levels(mip_levels) + .array_layers(1) + .samples(num_samples) + .tiling(tiling) + .usage(usage) + .sharing_mode(vk::SharingMode::EXCLUSIVE) + .initial_layout(vk::ImageLayout::UNDEFINED); + + let image = unsafe { self.device.create_image(&image_info, None)? }; + + let mem_req = unsafe { self.device.get_image_memory_requirements(image) }; + + let alloc_info = vk::MemoryAllocateInfo::default() + .allocation_size(mem_req.size) + .memory_type_index(self.find_memory_type(mem_req.memory_type_bits, properties)?); + + let memory = unsafe { self.device.allocate_memory(&alloc_info, None)? }; + + unsafe { self.device.bind_image_memory(image, memory, 0)? }; + + Ok((image, memory)) + } + fn generate_mipmaps( + &self, + cmd: &CommandBuffer, + image: vk::Image, + width: u32, + height: u32, + mip_levels: u32, + ) -> Result<()> { + let mut mip_width = width; + let mut mip_height = height; + + for level in 1..mip_levels { + let base_mip = level - 1; + // Transition previous level: TRANSFER_DST → TRANSFER_SRC + self.transition_image_layout( + cmd, + image, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + vk::ImageLayout::TRANSFER_SRC_OPTIMAL, + 1, + base_mip, + )?; + + let next_w = (mip_width / 2).max(1); + let next_h = (mip_height / 2).max(1); + + let blit = vk::ImageBlit::default() + .src_subresource(vk::ImageSubresourceLayers { + aspect_mask: vk::ImageAspectFlags::COLOR, + mip_level: level - 1, + base_array_layer: 0, + layer_count: 1, + }) + .src_offsets([ + vk::Offset3D { x: 0, y: 0, z: 0 }, + vk::Offset3D { + x: mip_width as i32, + y: mip_height as i32, + z: 1, + }, + ]) + .dst_subresource(vk::ImageSubresourceLayers { + aspect_mask: vk::ImageAspectFlags::COLOR, + mip_level: level, + base_array_layer: 0, + layer_count: 1, + }) + .dst_offsets([ + vk::Offset3D { x: 0, y: 0, z: 0 }, + vk::Offset3D { + x: next_w as i32, + y: next_h as i32, + z: 1, + }, + ]); + + unsafe { + self.device.cmd_blit_image( + cmd.raw(), + image, + vk::ImageLayout::TRANSFER_SRC_OPTIMAL, + image, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + &[blit], + vk::Filter::LINEAR, + ); + } + + // Previous level is fully consumed — transition to shader-readable + self.transition_image_layout( + cmd, + image, + vk::ImageLayout::TRANSFER_SRC_OPTIMAL, + vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, + 1, + base_mip, + )?; + + mip_width = next_w; + mip_height = next_h; + } + + // Transition the final mip level (never used as a blit source) + self.transition_image_layout( + cmd, + image, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, + 1, + mip_levels - 1, + ) + } + fn create_sampler(&self, mip_levels: u32) -> Result { + let props = unsafe { + self.instance + .get_physical_device_properties(self.physical_device) + }; + let max_aniso = props.limits.max_sampler_anisotropy; + + let sampler_info = vk::SamplerCreateInfo::default() + .mag_filter(vk::Filter::LINEAR) + .min_filter(vk::Filter::LINEAR) + .mipmap_mode(vk::SamplerMipmapMode::LINEAR) + .address_mode_u(vk::SamplerAddressMode::REPEAT) + .address_mode_v(vk::SamplerAddressMode::REPEAT) + .address_mode_w(vk::SamplerAddressMode::REPEAT) + .mip_lod_bias(0.0) + .anisotropy_enable(true) + .max_anisotropy(max_aniso) // use hardware maximum + .compare_enable(false) + .min_lod(0.0) + .max_lod(mip_levels as f32) + .border_color(vk::BorderColor::INT_OPAQUE_BLACK) + .unnormalized_coordinates(false); + + Ok(unsafe { self.device.create_sampler(&sampler_info, None)? }) + } + + // BUFFER UTILITIES + + fn upload_slice( + &self, + data: &[T], + usage: vk::BufferUsageFlags, + ) -> Result<(vk::Buffer, vk::DeviceMemory)> { + let size = std::mem::size_of_val(data) as vk::DeviceSize; + + let (staging_buf, staging_mem) = self.create_buffer( + size, + vk::BufferUsageFlags::TRANSFER_SRC, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, + )?; + + unsafe { + let ptr = self + .device + .map_memory(staging_mem, 0, size, vk::MemoryMapFlags::empty())? + as *mut T; + ptr.copy_from_nonoverlapping(data.as_ptr(), data.len()); + self.device.unmap_memory(staging_mem); + } + + let (device_buf, device_mem) = self.create_buffer( + size, + vk::BufferUsageFlags::TRANSFER_DST | usage, + vk::MemoryPropertyFlags::DEVICE_LOCAL, + )?; + + self.copy_buffer(staging_buf, device_buf, size)?; + unsafe { + self.device.destroy_buffer(staging_buf, None); + self.device.free_memory(staging_mem, None); + } + + Ok((device_buf, device_mem)) + } + fn copy_buffer(&self, src: vk::Buffer, dst: vk::Buffer, size: vk::DeviceSize) -> Result<()> { + let cmd = self.transfer_pool.begin_single_time(&self.device)?; + + let region = vk::BufferCopy { + src_offset: 0, + dst_offset: 0, + size, + }; + unsafe { self.device.cmd_copy_buffer(cmd.raw(), src, dst, &[region]) }; + + cmd.end_and_submit(&self.device)?; + Ok(()) + } + fn create_buffer( + &self, + size: vk::DeviceSize, + usage: vk::BufferUsageFlags, + properties: vk::MemoryPropertyFlags, + ) -> Result<(vk::Buffer, vk::DeviceMemory)> { + let buffer_info = vk::BufferCreateInfo::default() + .size(size) + .usage(usage) + .sharing_mode(vk::SharingMode::EXCLUSIVE); + + let buffer = unsafe { self.device.create_buffer(&buffer_info, None)? }; + let requirements = unsafe { self.device.get_buffer_memory_requirements(buffer) }; + let memory_type = self.find_memory_type(requirements.memory_type_bits, properties)?; + + let alloc_info = vk::MemoryAllocateInfo::default() + .allocation_size(requirements.size) + .memory_type_index(memory_type); + + let memory = unsafe { self.device.allocate_memory(&alloc_info, None)? }; + + unsafe { self.device.bind_buffer_memory(buffer, memory, 0)? }; + + Ok((buffer, memory)) + } + + // EXTRA UTILS + + fn mip_levels(width: u32, height: u32) -> u32 { + // How many times can we halve the larger dimension before hitting 1px? + (width.max(height) as f32).log2().floor() as u32 + 1 + } + fn find_memory_type( + &self, + type_filter: u32, + properties: vk::MemoryPropertyFlags, + ) -> Result { + let mem_props = unsafe { + self.instance + .get_physical_device_memory_properties(self.physical_device) + }; + + (0..mem_props.memory_type_count) + .find(|&i| { + let type_match = type_filter & (1 << i) != 0; + let prop_match = mem_props.memory_types[i as usize] + .property_flags + .contains(properties); + type_match && prop_match + }) + .ok_or(Error::NoSuitableMemoryType) + } + fn create_shader_module( + device: &Device, + shader: &'static shaders::Shader, + ) -> Result { + let code = shader.code_as_u32(); + let info = vk::ShaderModuleCreateInfo::default().code(code.as_slice()); + Ok(unsafe { device.create_shader_module(&info, None)? }) + } +} + +impl Drop for Renderer { + fn drop(&mut self) { + unsafe { + self.device.device_wait_idle().ok(); + } + info!("cleaning up renderer"); + + self.scenes + .iter() + .for_each(|(_, s)| s.destroy(&self.device)); + self.models.values().for_each(|m| m.destroy(&self.device)); + self.windows + .values() + .for_each(|w| w.destroy(&self.device, &self.surface_loader, &self.swapchain_loader)); + self.frames.iter().for_each(|f| f.destroy(&self.device)); + self.graphics_pipeline.destroy(&self.device); + self.layouts.destroy(&self.device); + + self.graphics_pool.destroy(&self.device); + self.transfer_pool.destroy(&self.device); + unsafe { + self.device + .destroy_descriptor_pool(self.material_descriptor_pool, None); + self.device.destroy_device(None); + + #[cfg(validation)] + self.debug_utils_loader + .destroy_debug_utils_messenger(self.debug_messenger, None); + + self.instance.destroy_instance(None); + } + } +} + +#[cfg(validation)] +extern "system" fn debug_callback( + severity: vk::DebugUtilsMessageSeverityFlagsEXT, + _message_type: vk::DebugUtilsMessageTypeFlagsEXT, + callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT, + _user_data: *mut c_void, +) -> vk::Bool32 { + let message = unsafe { CStr::from_ptr((*callback_data).p_message).to_string_lossy() }; + + // TODO: logging should be better (see tracing crate) + match severity { + vk::DebugUtilsMessageSeverityFlagsEXT::ERROR => error!("[Vulkan] {}", message), + vk::DebugUtilsMessageSeverityFlagsEXT::WARNING => warn!("[Vulkan] {}", message), + vk::DebugUtilsMessageSeverityFlagsEXT::INFO => info!("[Vulkan] {}", message), + vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE => debug!("[Vulkan] {}", message), + _ => trace!("[Vulkan] {}", message), + } + + vk::FALSE +} diff --git a/Engine/Source/renderer/src/model.rs b/Engine/Source/renderer/src/model.rs new file mode 100644 index 0000000..801a14f --- /dev/null +++ b/Engine/Source/renderer/src/model.rs @@ -0,0 +1,67 @@ +use ash::{Device, vk}; + +/// Complete GPU model. +#[derive(Clone)] +pub struct Model { + pub name: String, + pub primitives: Vec, + pub textures: Vec, + pub materials: Vec, + /// One descriptor set per entry in `materials`. + /// `vk::DescriptorSet::null()` if the material has no base-colour texture. + pub material_sets: Vec, +} + +impl Model { + pub fn destroy(&self, device: &Device) { + for prim in &self.primitives { + prim.destroy(device); + } + for tex in &self.textures { + tex.destroy(device); + } + } +} + +/// All GPU-side handles for a single texture. +#[derive(Clone)] +pub struct Texture { + pub image: vk::Image, + pub memory: vk::DeviceMemory, + pub view: vk::ImageView, + pub sampler: vk::Sampler, + pub mip_levels: u32, +} + +impl Texture { + fn destroy(&self, device: &Device) { + unsafe { + device.destroy_sampler(self.sampler, None); + device.destroy_image_view(self.view, None); + device.destroy_image(self.image, None); + device.free_memory(self.memory, None); + } + } +} + +/// GPU-side handles for a single glTF primitive. +#[derive(Clone)] +pub struct Primitive { + pub vertex_buffer: vk::Buffer, + pub vertex_buffer_memory: vk::DeviceMemory, + pub index_buffer: vk::Buffer, + pub index_buffer_memory: vk::DeviceMemory, + pub index_count: u32, + pub material: Option, +} + +impl Primitive { + fn destroy(&self, device: &Device) { + unsafe { + device.destroy_buffer(self.vertex_buffer, None); + device.free_memory(self.vertex_buffer_memory, None); + device.destroy_buffer(self.index_buffer, None); + device.free_memory(self.index_buffer_memory, None); + } + } +} diff --git a/Engine/Source/renderer/src/physical_device.rs b/Engine/Source/renderer/src/physical_device.rs new file mode 100644 index 0000000..87f37ac --- /dev/null +++ b/Engine/Source/renderer/src/physical_device.rs @@ -0,0 +1,163 @@ +use ash::{khr::surface, vk}; + +pub struct PhysicalDeviceInfo { + pub handle: vk::PhysicalDevice, + pub properties: vk::PhysicalDeviceProperties, + pub features: vk::PhysicalDeviceFeatures, + pub memory_properties: vk::PhysicalDeviceMemoryProperties, + pub queue_families: Vec, + pub extensions: Vec, +} + +impl PhysicalDeviceInfo { + pub fn query(instance: &ash::Instance, handle: vk::PhysicalDevice) -> Self { + unsafe { + Self { + handle, + properties: instance.get_physical_device_properties(handle), + features: instance.get_physical_device_features(handle), + memory_properties: instance.get_physical_device_memory_properties(handle), + queue_families: instance.get_physical_device_queue_family_properties(handle), + extensions: instance + .enumerate_device_extension_properties(handle) + .unwrap_or_default(), + } + } + } +} + +#[derive(Debug, Clone)] +pub struct QueueFamilyIndices { + pub graphics: u32, + pub compute: u32, + pub transfer: u32, + pub present: u32, +} + +impl QueueFamilyIndices { + pub fn resolve( + info: &PhysicalDeviceInfo, + surface_loader: &surface::Instance, + surface: vk::SurfaceKHR, + ) -> Option { + let families = &info.queue_families; + + let graphics = families + .iter() + .position(|f| f.queue_flags.contains(vk::QueueFlags::GRAPHICS))? + as u32; + + let compute = families + .iter() + .position(|f| { + f.queue_flags.contains(vk::QueueFlags::COMPUTE) + && !f.queue_flags.contains(vk::QueueFlags::GRAPHICS) + }) + .unwrap_or(graphics as usize) as u32; + + let transfer = families + .iter() + .position(|f| { + f.queue_flags.contains(vk::QueueFlags::TRANSFER) + && !f.queue_flags.contains(vk::QueueFlags::GRAPHICS) + && !f.queue_flags.contains(vk::QueueFlags::COMPUTE) + }) + .unwrap_or(compute as usize) as u32; + + let present = families.iter().enumerate().position(|(i, _)| unsafe { + surface_loader + .get_physical_device_surface_support(info.handle, i as u32, surface) + .unwrap_or(false) + })? as u32; + + Some(Self { + graphics, + compute, + transfer, + present, + }) + } +} + +fn score_device(info: &PhysicalDeviceInfo) -> u32 { + let mut score = 0u32; + + // Strongly prefer discrete GPUs + score += match info.properties.device_type { + vk::PhysicalDeviceType::DISCRETE_GPU => 10_000, + vk::PhysicalDeviceType::INTEGRATED_GPU => 1_000, + vk::PhysicalDeviceType::VIRTUAL_GPU => 500, + _ => 0, + }; + + // Reward larger device-local VRAM + let vram_mb = info + .memory_properties + .memory_heaps + .iter() + .take(info.memory_properties.memory_heap_count as usize) + .filter(|h| h.flags.contains(vk::MemoryHeapFlags::DEVICE_LOCAL)) + .map(|h| h.size / (1024 * 1024)) + .sum::(); + score += (vram_mb / 512).min(5_000) as u32; // cap contribution at ~2.5 GB equivalent + + // Reward higher Vulkan API version support + score += vk::api_version_minor(info.properties.api_version) * 100; + + score +} + +type Requirement = dyn Fn(&PhysicalDeviceInfo) -> bool; + +/// A simple struct to help select a physical device for vulkan. +/// Add requirements that implement the [DeviceRequirement] trait. +/// When selecting, will make sure all requirements are met, or will +/// return [None]. +pub struct PhysicalDeviceSelector { + requirements: Vec>, +} + +impl PhysicalDeviceSelector { + pub fn new() -> Self { + Self { + requirements: vec![], + } + } + + pub fn require_extensions(self, extensions: &'static [&'static str]) -> Self { + self.require(move |info: &PhysicalDeviceInfo| { + extensions.iter().all(|&extension| { + info.extensions.iter().any(|e| { + unsafe { std::ffi::CStr::from_ptr(e.extension_name.as_ptr()) } + .to_str() + .unwrap_or("") + == extension + }) + }) + }) + } + + pub fn require bool + 'static>(mut self, req: F) -> Self { + self.requirements.push(Box::new(req)); + self + } + + pub fn select( + &self, + instance: &ash::Instance, + surface_loader: &surface::Instance, + surface: vk::SurfaceKHR, + ) -> Option<(PhysicalDeviceInfo, QueueFamilyIndices)> { + let devices = unsafe { instance.enumerate_physical_devices().ok()? }; + devices + .into_iter() + .map(|handle| PhysicalDeviceInfo::query(instance, handle)) + .filter(|info| { + self.requirements.iter().all(|req| req(info)) // ← just call it + }) + .filter_map(|info| { + QueueFamilyIndices::resolve(&info, surface_loader, surface).map(|q| (info, q)) + }) + .max_by_key(|(info, _)| score_device(info)) + } +} diff --git a/Engine/Source/renderer/src/pipeline.rs b/Engine/Source/renderer/src/pipeline.rs new file mode 100644 index 0000000..ef45fe7 --- /dev/null +++ b/Engine/Source/renderer/src/pipeline.rs @@ -0,0 +1,140 @@ +use ash::{Device, vk}; + +use crate::{ + DescriptorLayouts, Renderer, RendererProperties, Result, Vertex, command_pool::CommandBuffer, +}; + +/// This struct holds the graphics pipeline & stuff. +pub struct GraphicsPipeline { + pipeline_layout: vk::PipelineLayout, + graphics_pipeline: vk::Pipeline, +} + +impl GraphicsPipeline { + pub fn build( + device: &Device, + layouts: &DescriptorLayouts, + properties: &RendererProperties, + ) -> Result { + let set_layouts = [layouts.scene, layouts.object, layouts.material]; + let layout_info = vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts); + let pipeline_layout = unsafe { device.create_pipeline_layout(&layout_info, None)? }; + + let vert = Renderer::create_shader_module(device, &shaders::VERT)?; + let vert_name = shaders::VERT.entrypoint(); + + let frag = Renderer::create_shader_module(device, &shaders::FRAG)?; + let frag_name = shaders::FRAG.entrypoint(); + + let shader_stages = [ + vk::PipelineShaderStageCreateInfo::default() + .stage(vk::ShaderStageFlags::VERTEX) + .module(vert) + .name(vert_name), + vk::PipelineShaderStageCreateInfo::default() + .stage(vk::ShaderStageFlags::FRAGMENT) + .module(frag) + .name(frag_name), + ]; + + let binding_description = Vertex::binding_description(); + let attribute_description = Vertex::attribute_description(); + let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::default() + .vertex_binding_descriptions(std::slice::from_ref(&binding_description)) + .vertex_attribute_descriptions(&attribute_description); + + let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::default() + .topology(vk::PrimitiveTopology::TRIANGLE_LIST) + .primitive_restart_enable(false); + + let dynamic_state = vk::PipelineDynamicStateCreateInfo::default() + .dynamic_states(&[vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR]); + let viewport_state = vk::PipelineViewportStateCreateInfo::default() + .viewport_count(1) + .scissor_count(1); + + let rasterizer = vk::PipelineRasterizationStateCreateInfo::default() + .depth_clamp_enable(false) + .rasterizer_discard_enable(false) // enabling this disables output to frame buffer + .polygon_mode(vk::PolygonMode::FILL) + .line_width(1.) + .cull_mode(vk::CullModeFlags::BACK) + .front_face(vk::FrontFace::COUNTER_CLOCKWISE) + .depth_bias_enable(false); + + let multisampling = vk::PipelineMultisampleStateCreateInfo::default() + .sample_shading_enable(false) + .rasterization_samples(properties.msaa_samples); + + let color_blend_attachment = vk::PipelineColorBlendAttachmentState::default() + .blend_enable(false) + .color_write_mask(vk::ColorComponentFlags::RGBA); + + let color_blending = vk::PipelineColorBlendStateCreateInfo::default() + .logic_op_enable(false) + .attachments(std::slice::from_ref(&color_blend_attachment)); + + let depth_test_info = vk::PipelineDepthStencilStateCreateInfo::default() + .depth_test_enable(true) + .depth_write_enable(true) + .depth_compare_op(vk::CompareOp::LESS); + + let mut pipeline_rendering_info = vk::PipelineRenderingCreateInfo::default() + .color_attachment_formats(std::slice::from_ref(&properties.surface_format.format)) + .depth_attachment_format(properties.depth_format); + + let pipeline_info = vk::GraphicsPipelineCreateInfo::default() + .stages(&shader_stages) + .vertex_input_state(&vertex_input_info) + .input_assembly_state(&input_assembly) + .viewport_state(&viewport_state) + .rasterization_state(&rasterizer) + .multisample_state(&multisampling) + .depth_stencil_state(&depth_test_info) + .color_blend_state(&color_blending) + .dynamic_state(&dynamic_state) + .layout(pipeline_layout) + .subpass(0) + .base_pipeline_handle(vk::Pipeline::null()) + .base_pipeline_index(-1) + .push_next(&mut pipeline_rendering_info); + + let graphics_pipeline = unsafe { + device + .create_graphics_pipelines( + vk::PipelineCache::null(), + std::slice::from_ref(&pipeline_info), + None, + ) + .map_err(|(_, err)| err)?[0] + }; + + unsafe { + device.destroy_shader_module(vert, None); + device.destroy_shader_module(frag, None); + } + + Ok(Self { + graphics_pipeline, + pipeline_layout, + }) + } + pub fn bind(&self, renderer: &Renderer, cmd: &CommandBuffer) { + unsafe { + renderer.device.cmd_bind_pipeline( + cmd.raw(), + vk::PipelineBindPoint::GRAPHICS, + self.graphics_pipeline, + ) + } + } + pub fn layout(&self) -> vk::PipelineLayout { + self.pipeline_layout + } + pub fn destroy(&self, device: &Device) { + unsafe { + device.destroy_pipeline_layout(self.pipeline_layout, None); + device.destroy_pipeline(self.graphics_pipeline, None); + } + } +} diff --git a/Engine/Source/renderer/src/render_pass.rs b/Engine/Source/renderer/src/render_pass.rs new file mode 100644 index 0000000..e1cc410 --- /dev/null +++ b/Engine/Source/renderer/src/render_pass.rs @@ -0,0 +1,120 @@ +use ash::{Device, vk}; + +use crate::{Renderer, Result, command_pool::CommandBuffer}; + +/// This struct holds the graphics pipeline & stuff. +/// It can be called on to begin the pass (begin rendering, +/// bind graphics pipeline, ...) +pub struct RenderPass { + color: vk::ImageView, + color_image: vk::Image, + color_memory: vk::DeviceMemory, + depth: vk::ImageView, + depth_image: vk::Image, + depth_memory: vk::DeviceMemory, +} + +impl RenderPass { + pub fn build(renderer: &Renderer, size: vk::Extent2D) -> Result { + let (color_image, color_memory) = renderer.create_image( + size, + renderer.properties.surface_format.format, + vk::ImageTiling::OPTIMAL, + vk::ImageUsageFlags::TRANSIENT_ATTACHMENT | vk::ImageUsageFlags::COLOR_ATTACHMENT, + vk::MemoryPropertyFlags::DEVICE_LOCAL, + (1, renderer.properties.msaa_samples), + )?; + let color = renderer.create_image_view( + color_image, + renderer.properties.surface_format.format, + vk::ImageAspectFlags::COLOR, + 1, + )?; + + let (depth_image, depth_memory) = renderer.create_image( + size, + renderer.properties.depth_format, + vk::ImageTiling::OPTIMAL, + vk::ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT, + vk::MemoryPropertyFlags::DEVICE_LOCAL, + (1, renderer.properties.msaa_samples), + )?; + let depth = renderer.create_image_view( + depth_image, + renderer.properties.depth_format, + vk::ImageAspectFlags::DEPTH, + 1, + )?; + + Ok(Self { + color, + color_image, + color_memory, + depth, + depth_image, + depth_memory, + }) + } + pub fn destroy(&self, device: &Device) { + unsafe { + device.destroy_image_view(self.color, None); + device.destroy_image(self.color_image, None); + device.free_memory(self.color_memory, None); + + device.destroy_image_view(self.depth, None); + device.destroy_image(self.depth_image, None); + device.free_memory(self.depth_memory, None); + } + } + pub fn begin( + &self, + renderer: &Renderer, + cmd: &CommandBuffer, + size: vk::Extent2D, + out: vk::ImageView, + ) { + let color_attachement = vk::RenderingAttachmentInfo::default() + .load_op(vk::AttachmentLoadOp::CLEAR) + .store_op(vk::AttachmentStoreOp::STORE) + .image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .image_view(self.color) + .resolve_image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .resolve_mode(vk::ResolveModeFlags::AVERAGE) + .resolve_image_view(out) + .clear_value(vk::ClearValue { + color: vk::ClearColorValue { + float32: [0., 0., 0., 1.], + }, + }); + + let depth_info = vk::RenderingAttachmentInfo::default() + .load_op(vk::AttachmentLoadOp::CLEAR) + .store_op(vk::AttachmentStoreOp::DONT_CARE) + .image_layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL) + .image_view(self.depth) + .clear_value(vk::ClearValue { + depth_stencil: vk::ClearDepthStencilValue { + depth: 1., + stencil: 0, + }, + }); + + let rendering_info = vk::RenderingInfo::default() + .render_area(vk::Rect2D { + offset: vk::Offset2D { x: 0, y: 0 }, + extent: size, + }) + .color_attachments(std::slice::from_ref(&color_attachement)) + .depth_attachment(&depth_info) + .layer_count(1); + + unsafe { + renderer + .device + .cmd_begin_rendering(cmd.raw(), &rendering_info) + }; + } + pub fn end(&self, renderer: &Renderer, cmd: &CommandBuffer) { + unsafe { renderer.device.cmd_end_rendering(cmd.raw()) } + } +} diff --git a/Engine/Source/renderer/src/scene.rs b/Engine/Source/renderer/src/scene.rs new file mode 100644 index 0000000..1584751 --- /dev/null +++ b/Engine/Source/renderer/src/scene.rs @@ -0,0 +1,362 @@ +use std::ffi::c_void; + +use ash::{Device, vk}; +use world::{World, components}; + +use crate::{ + MAX_FRAMES_IN_FLIGHT, Renderer, Result, command_pool::CommandBuffer, model, + render_pass::RenderPass, +}; + +/// This scene is created from a [world::World]. +/// It should then be updated whenever the world is updated. +/// +/// Handles rendering all the [world::components::Renderable] objects +/// of the world. +pub struct Scene { + /// The entities to render. + proxies: Vec, + /// View matrix calculated from camera position. + view: glam::Mat4, + /// Projection matrix calculated from screen settings. + proj: glam::Mat4, + + descriptor_pool: vk::DescriptorPool, + ubo: [UboData; MAX_FRAMES_IN_FLIGHT], + descriptor_sets: [vk::DescriptorSet; MAX_FRAMES_IN_FLIGHT], + render_pass: RenderPass, +} + +struct SceneUbo { + view: glam::Mat4, + proj: glam::Mat4, +} + +#[derive(Debug)] +struct UboData { + buffer: vk::Buffer, + memory: vk::DeviceMemory, + mapped: *mut c_void, +} + +impl UboData { + fn destroy(&self, device: &Device) { + unsafe { + device.destroy_buffer(self.buffer, None); + device.free_memory(self.memory, None); + } + } +} + +impl Scene { + /// Builds a [Scene]. + /// Constructs the renderer stuff like command pools, descriptor sets, ... from + /// the [Renderer] and all world proxy stuff from [World]. + pub fn build(renderer: &Renderer, world: &World) -> Result { + // TODO: load all the models that are used by the scene proxies + let (camera, camera_trans) = Self::get_camera(world); + + let proxy_count = world + .query_double::() + .len() as u32; + + let pool_sizes = [ + vk::DescriptorPoolSize { + ty: vk::DescriptorType::UNIFORM_BUFFER, + // scene UBOs + object UBOs, all × frames in flight + descriptor_count: (1 + proxy_count) * MAX_FRAMES_IN_FLIGHT as u32, + }, + vk::DescriptorPoolSize { + ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER, + // rough upper bound on material textures + descriptor_count: proxy_count * MAX_FRAMES_IN_FLIGHT as u32, + }, + ]; + + let pool_info = vk::DescriptorPoolCreateInfo::default() + .pool_sizes(&pool_sizes) + .max_sets((1 + proxy_count * 2) * MAX_FRAMES_IN_FLIGHT as u32); + + let descriptor_pool = unsafe { renderer.device.create_descriptor_pool(&pool_info, None)? }; + + // Allocate scene-level sets (one per frame) + let layouts = [renderer.layouts.scene; MAX_FRAMES_IN_FLIGHT]; + let alloc_info = vk::DescriptorSetAllocateInfo::default() + .descriptor_pool(descriptor_pool) + .set_layouts(&layouts); + + let scene_desc_sets: [vk::DescriptorSet; MAX_FRAMES_IN_FLIGHT] = unsafe { + renderer + .device + .allocate_descriptor_sets(&alloc_info)? + .try_into() + .unwrap() + }; + + let ubo: Vec = (0..MAX_FRAMES_IN_FLIGHT) + .map(|_| { + let size = size_of::() as u64; + let (buffer, memory) = renderer.create_buffer( + size, + vk::BufferUsageFlags::UNIFORM_BUFFER, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, + )?; + + let mapped = unsafe { + renderer + .device + .map_memory(memory, 0, size, vk::MemoryMapFlags::empty())? + }; + + Ok(UboData { + buffer, + memory, + mapped, + }) + }) + .collect::>>()?; + let ubo: [UboData; MAX_FRAMES_IN_FLIGHT] = ubo.try_into().unwrap(); + + let buffer_infos: [vk::DescriptorBufferInfo; MAX_FRAMES_IN_FLIGHT] = + std::array::from_fn(|i| { + vk::DescriptorBufferInfo::default() + .buffer(ubo[i].buffer) + .range(size_of::() as u64) + .offset(0) + }); + + let descriptor_writes: [vk::WriteDescriptorSet; MAX_FRAMES_IN_FLIGHT] = + std::array::from_fn(|i| { + vk::WriteDescriptorSet::default() + .dst_set(scene_desc_sets[i]) + .dst_binding(0) + .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER) + .buffer_info(std::slice::from_ref(&buffer_infos[i])) + }); + + unsafe { + renderer + .device + .update_descriptor_sets(&descriptor_writes, &[]) + }; + + let window = renderer.windows.get(&renderer.main_window).unwrap(); + let render_pass = RenderPass::build(renderer, window.extent())?; + + let mut scene = Self { + proxies: Vec::new(), + view: camera_trans.view(), + proj: camera.projection(), + descriptor_pool, + ubo, + descriptor_sets: scene_desc_sets, + render_pass, + }; + scene.proxies = scene.make_scene_proxies(renderer, world)?; + Ok(scene) + } + pub fn destroy(&self, device: &Device) { + self.proxies.iter().for_each(|proxy| proxy.destroy(device)); + self.render_pass.destroy(device); + self.ubo.iter().for_each(|ubo| ubo.destroy(device)); + unsafe { device.destroy_descriptor_pool(self.descriptor_pool, None) }; + } + // TODO: on tick, worlds should be sent to update scenes + #[allow(unused)] + /// This function will reconstruct the internal world data with the new input world. + /// This includes: [SceneProxy]s, view matrix & projection matrix. + pub fn rebuild(&mut self, renderer: &Renderer, world: &World) -> Result<()> { + let (camera, camera_trans) = Self::get_camera(world); + self.proxies = self.make_scene_proxies(renderer, world)?; + self.view = camera_trans.view(); + self.proj = camera.projection(); + Ok(()) + } + fn make_scene_proxies(&self, renderer: &Renderer, world: &World) -> Result> { + world + .query_double::() + .iter() + .map(|&entity| { + // already made sure the entity has the component + let renderable = world.get::(entity).unwrap(); + let transform = world.get::(entity).unwrap(); + SceneProxy::build(renderer, self, &renderable.model, transform.matrix()) + }) + .collect::>>() + } + fn get_camera(world: &World) -> (&components::Camera, &components::Transform) { + // TODO: don't just get the first camera + error handling if no camera + let camera_entity = world.query_double::()[0]; + ( + world.get::(camera_entity).unwrap(), + world.get::(camera_entity).unwrap(), + ) + } + pub fn render( + &self, + renderer: &Renderer, + cmd: &CommandBuffer, + size: vk::Extent2D, + view: vk::ImageView, + ) -> Result<()> { + let device = &renderer.device; + + self.render_pass.begin(renderer, cmd, size, view); + renderer.graphics_pipeline.bind(renderer, cmd); + + let viewport = vk::Viewport::default() + .width(size.width as f32) + .height(size.height as f32) + .min_depth(0.) + .max_depth(1.); + unsafe { renderer.device.cmd_set_viewport(cmd.raw(), 0, &[viewport]) }; + + let scissor = vk::Rect2D::default() + .offset(vk::Offset2D::default()) + .extent(size); + unsafe { renderer.device.cmd_set_scissor(cmd.raw(), 0, &[scissor]) }; + + let mut descriptor_sets = [ + self.descriptor_sets[renderer.current_frame], + vk::DescriptorSet::null(), + vk::DescriptorSet::null(), + ]; + + for proxy in &self.proxies { + for prim in &proxy.model.primitives { + let mat_set = prim + .material + .and_then(|idx| proxy.model.material_sets.get(idx).copied()) + .unwrap_or(vk::DescriptorSet::null()); + + descriptor_sets[1] = proxy.sets[renderer.current_frame]; + descriptor_sets[2] = mat_set; + + unsafe { + device.cmd_bind_descriptor_sets( + cmd.raw(), + vk::PipelineBindPoint::GRAPHICS, + renderer.graphics_pipeline.layout(), + 0, + &descriptor_sets, + &[], + ); + device.cmd_bind_vertex_buffers(cmd.raw(), 0, &[prim.vertex_buffer], &[0]); + device.cmd_bind_index_buffer( + cmd.raw(), + prim.index_buffer, + 0, + vk::IndexType::UINT32, + ); + device.cmd_draw_indexed(cmd.raw(), prim.index_count, 1, 0, 0, 0); + } + } + } + + self.render_pass.end(renderer, cmd); + Ok(()) + } +} + +/// A renderable entity's representation for the renderer. +/// Owned by [Scene], constructed from [world::components::Renderable] and +/// [world::components::Transform]. +pub struct SceneProxy { + /// The name of the model. Used to request a [crate::model::Model] from the + /// renderer at render time. + model: model::Model, + /// The model matrix used for rendering. Constructed from the + /// [world::components::Transform] of the entity. + model_matrix: glam::Mat4, + ubo: [UboData; MAX_FRAMES_IN_FLIGHT], + sets: [vk::DescriptorSet; MAX_FRAMES_IN_FLIGHT], +} + +struct ProxyUbo { + model: glam::Mat4, +} + +impl SceneProxy { + pub fn build( + renderer: &Renderer, + scene: &Scene, + model: &str, + model_matrix: glam::Mat4, + ) -> Result { + let model = renderer + .get_model(model) + .expect("should have the model") + .clone(); + + let size = size_of::() as u64; + let ubo: Vec = (0..MAX_FRAMES_IN_FLIGHT) + .map(|_| { + let (buffer, memory) = renderer.create_buffer( + size, + vk::BufferUsageFlags::UNIFORM_BUFFER, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, + )?; + + let mapped = unsafe { + renderer + .device + .map_memory(memory, 0, size, vk::MemoryMapFlags::empty())? + }; + + Ok(UboData { + buffer, + memory, + mapped, + }) + }) + .collect::>>()?; + let ubo: [UboData; MAX_FRAMES_IN_FLIGHT] = ubo.try_into().unwrap(); + + // Allocate scene-level sets (one per frame) + let layouts = [renderer.layouts.object; MAX_FRAMES_IN_FLIGHT]; + let alloc_info = vk::DescriptorSetAllocateInfo::default() + .descriptor_pool(scene.descriptor_pool) + .set_layouts(&layouts); + + let sets: [vk::DescriptorSet; MAX_FRAMES_IN_FLIGHT] = unsafe { + renderer + .device + .allocate_descriptor_sets(&alloc_info)? + .try_into() + .unwrap() + }; + + let buffer_infos: [vk::DescriptorBufferInfo; MAX_FRAMES_IN_FLIGHT] = + std::array::from_fn(|i| { + vk::DescriptorBufferInfo::default() + .buffer(ubo[i].buffer) + .range(size) + .offset(0) + }); + + let descriptor_writes: [vk::WriteDescriptorSet; MAX_FRAMES_IN_FLIGHT] = + std::array::from_fn(|i| { + vk::WriteDescriptorSet::default() + .dst_set(sets[i]) + .dst_binding(1) + .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER) + .buffer_info(std::slice::from_ref(&buffer_infos[i])) + }); + + unsafe { + renderer + .device + .update_descriptor_sets(&descriptor_writes, &[]) + }; + + Ok(Self { + model, + model_matrix, + ubo, + sets, + }) + } + fn destroy(&self, device: &Device) { + self.ubo.iter().for_each(|ubo| ubo.destroy(device)); + } +} diff --git a/Engine/Source/renderer/src/window.rs b/Engine/Source/renderer/src/window.rs new file mode 100644 index 0000000..209d3bd --- /dev/null +++ b/Engine/Source/renderer/src/window.rs @@ -0,0 +1,129 @@ +use ash::{ + Device, + khr::{surface, swapchain}, + vk, +}; + +use crate::{Error, Renderer, Result}; + +pub type WindowId = usize; + +#[derive(Clone)] +pub struct SwapchainImage { + pub image: vk::Image, + pub view: vk::ImageView, +} +impl SwapchainImage { + pub fn destroy(&self, device: &Device) { + // don't destroy image as it is owned + // by swap chain + unsafe { + device.destroy_image_view(self.view, None); + } + } +} + +/// The renderer's representation of a platform window. +/// Holds the swapchain, surface & other related state. +/// Doesn't actually do any of the rendering of the game. +pub struct Window { + id: WindowId, + surface: vk::SurfaceKHR, + swapchain: vk::SwapchainKHR, + images: Vec, + extent: vk::Extent2D, + + /// The semaphores associated with each swapchain image + semaphores: Vec<(vk::Semaphore, vk::Semaphore)>, + /// The current index of the semaphores + semaphore_count: usize, +} + +impl Window { + pub fn build( + id: WindowId, + renderer: &Renderer, + surface: vk::SurfaceKHR, + size: vk::Extent2D, + ) -> Result { + let (swapchain, extent, images) = renderer.create_swap_chain(surface, size)?; + + let semaphore_info = vk::SemaphoreCreateInfo::default(); + let create_semaphore = || unsafe { + Ok::(renderer.device.create_semaphore(&semaphore_info, None)?) + }; + + let semaphores = (0..images.len()) + .map(|_| Ok((create_semaphore()?, create_semaphore()?))) + .collect::>>()?; + + Ok(Self { + id, + surface, + swapchain, + extent, + images, + semaphores, + semaphore_count: 0, + }) + } + /// Returns the window's ID + pub fn id(&self) -> WindowId { + self.id + } + pub fn extent(&self) -> vk::Extent2D { + self.extent + } + pub fn swapchain(&self) -> vk::SwapchainKHR { + self.swapchain + } + /// (render_finished_semaphore, image_available_semaphore) + pub fn current_semaphores(&self) -> (vk::Semaphore, vk::Semaphore) { + self.semaphores[self.semaphore_count] + } + pub fn next_image( + &mut self, + swapchain_loader: &swapchain::Device, + ) -> Result<(SwapchainImage, u32)> { + self.semaphore_count = (self.semaphore_count + 1) % self.semaphores.len(); + let (_, image_available_semaphore) = self.semaphores[self.semaphore_count]; + + let (image_index, suboptimal) = unsafe { + swapchain_loader.acquire_next_image( + self.swapchain, + u64::MAX, + image_available_semaphore, + vk::Fence::null(), + )? + }; + + if suboptimal { + return Err(Error::SuboptimalSurface); + } + + Ok((self.images[image_index as usize].clone(), image_index)) + } + pub fn resize(&mut self, renderer: &Renderer, in_size: vk::Extent2D) -> Result<()> { + let (swapchain, extent, images) = renderer.create_swap_chain(self.surface, in_size)?; + self.swapchain = swapchain; + self.extent = extent; + self.images = images; + Ok(()) + } + pub fn destroy( + &self, + device: &Device, + surface: &surface::Instance, + swapchain: &swapchain::Device, + ) { + self.images.iter().for_each(|i| i.destroy(device)); + unsafe { + self.semaphores.iter().for_each(|&(s1, s2)| { + device.destroy_semaphore(s1, None); + device.destroy_semaphore(s2, None); + }); + swapchain.destroy_swapchain(self.swapchain, None); + surface.destroy_surface(self.surface, None); + } + } +} diff --git a/Engine/Source/resource_manager/build.rs b/Engine/Source/resource_manager/build.rs index 7959570..3c897fe 100644 --- a/Engine/Source/resource_manager/build.rs +++ b/Engine/Source/resource_manager/build.rs @@ -1,5 +1,8 @@ fn main() { - let assets_path = "../../Assets"; + let assets_path = format!( + "{}/../../Assets", + std::env::var("CARGO_MANIFEST_DIR").unwrap() + ); println!("cargo:rustc-env=ASSETS_PATH={assets_path}"); let models_path = format!("{}/models", assets_path); diff --git a/Engine/Source/resource_manager/src/lib.rs b/Engine/Source/resource_manager/src/lib.rs index 6994d23..606403c 100644 --- a/Engine/Source/resource_manager/src/lib.rs +++ b/Engine/Source/resource_manager/src/lib.rs @@ -21,6 +21,8 @@ const MODELS_PATH: &str = env!("MODELS_PATH"); /// Internal representation of a model loaded from a gltf model. #[derive(derive_getters::Getters)] pub struct Model { + /// The name of the model. Derived from the file name. + name: String, meshes: Vec, textures: Vec, materials: Vec, @@ -59,7 +61,7 @@ pub struct Texture { } /// A material. Stores indices into the model's textures array. -#[derive(derive_getters::Getters)] +#[derive(derive_getters::Getters, Clone)] pub struct Material { /// An optional index into the model's textures array. base_color_texture: Option, @@ -105,6 +107,7 @@ impl ResourceManager { .collect(); Ok(Model { + name: name.to_string(), meshes, textures, materials, diff --git a/Engine/Source/run/src/main.rs b/Engine/Source/run/src/main.rs index 399588d..2bf16b9 100644 --- a/Engine/Source/run/src/main.rs +++ b/Engine/Source/run/src/main.rs @@ -14,6 +14,9 @@ fn run() -> anyhow::Result<()> { fn main() { match run() { Ok(_) => {} - Err(err) => panic!("Error: {err:#}"), + Err(err) => { + error!("{err:#}"); + panic!("fatal error. see logs for details") + } } } diff --git a/Engine/Source/utils/Cargo.toml b/Engine/Source/utils/Cargo.toml new file mode 100644 index 0000000..02aac5c --- /dev/null +++ b/Engine/Source/utils/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "utils" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[dependencies] +thiserror.workspace = true +glam.workspace = true diff --git a/Engine/Source/utils/src/lib.rs b/Engine/Source/utils/src/lib.rs new file mode 100644 index 0000000..fce1e8d --- /dev/null +++ b/Engine/Source/utils/src/lib.rs @@ -0,0 +1,17 @@ +//! This is a utility crate. +//! It has basic utilities for use throughout the engine. +//! No actual engine systems live in this crate. It just has +//! many small features, functions and structures. + +mod version; +pub use version::*; + +/// The up direction used for all world and +/// renderer coordinate calcualtions. +/// +/// Y-up +pub const UP_DIRECTION: glam::Vec3 = glam::Vec3::Y; +/// The forward direction used for all world and +/// renderer coordinate calcualtions. +/// We use Z-forward because that is how Vulkan does it. +pub const FORWARD_DIRECTION: glam::Vec3 = glam::Vec3::Z; diff --git a/Engine/Source/utils/src/version.rs b/Engine/Source/utils/src/version.rs new file mode 100644 index 0000000..96431a7 --- /dev/null +++ b/Engine/Source/utils/src/version.rs @@ -0,0 +1,277 @@ +use std::{fmt, str::FromStr}; + +use thiserror::Error; + +/// A simple representation of a version. This is +/// used to track engine version, etc... +/// For now it is mainly used by Vulkan when populating +/// the application info struct. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Version(u32); + +impl Default for Version { + fn default() -> Self { + Self::ZERO + } +} + +impl Version { + pub const ZERO: Self = Self(0); + pub fn new(major: u32, minor: u32, patch: u32) -> Self { + assert!( + major < (1 << 10), + "major version must fit in 10 bits (0–1023)" + ); + assert!( + minor < (1 << 10), + "minor version must fit in 10 bits (0–1023)" + ); + assert!( + patch < (1 << 12), + "patch version must fit in 12 bits (0–4095)" + ); + Self(((major) << 22) | ((minor) << 12) | (patch)) + } + pub fn major(&self) -> u32 { + self.0 >> 22 + } + pub fn minor(&self) -> u32 { + (self.0 >> 12) & 0x3ff + } + pub fn patch(&self) -> u32 { + self.0 & 0xfff + } + /// Increments major, resets minor and patch to 0. + pub fn bump_major(self) -> Self { + Self::new(self.major() + 1, 0, 0) + } + /// Increments minor, resets patch to 0. + pub fn bump_minor(self) -> Self { + Self::new(self.major(), self.minor() + 1, 0) + } + /// Increments patch. + pub fn bump_patch(self) -> Self { + Self::new(self.major(), self.minor(), self.patch() + 1) + } + /// Returns true if `self` is semver-compatible with `required` + /// (same major, self.minor >= required.minor). + pub fn is_compatible_with(self, required: Self) -> bool { + self.major() == required.major() && self >= required + } + /// If the major is 0, then this is a prerelease version. + pub fn is_prerelease(self) -> bool { + self.major() == 0 + } +} + +impl From for Version { + fn from(v: u32) -> Self { + Self(v) + } +} + +impl From for u32 { + fn from(v: Version) -> Self { + v.0 + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// Comparing the raw packed `u32` is equivalent to comparing +/// (major, minor, patch) lexicographically because the fields are +/// stored in significance order. +impl Ord for Version { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.cmp(&other.0) + } +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}.{}", self.major(), self.minor(), self.patch()) + } +} + +impl fmt::Debug for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Version({self})") + } +} + +#[derive(Debug, PartialEq, Error)] +pub struct ParseVersionError(String); + +impl fmt::Display for ParseVersionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "invalid version string: '{}'", self.0) + } +} + +impl FromStr for Version { + type Err = ParseVersionError; + + fn from_str(s: &str) -> Result { + let err = || ParseVersionError(s.to_string()); + let mut parts = s.trim_start_matches('v').splitn(3, '.'); + let major = parts + .next() + .ok_or_else(err)? + .parse::() + .map_err(|_| err())?; + let minor = parts + .next() + .ok_or_else(err)? + .parse::() + .map_err(|_| err())?; + let patch = parts + .next() + .ok_or_else(err)? + .parse::() + .map_err(|_| err())?; + Ok(Self::new(major, minor, patch)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // -- Construction & accessors -- + + #[test] + fn test_fields_roundtrip() { + let v = Version::new(1, 2, 3); + assert_eq!(v.major(), 1); + assert_eq!(v.minor(), 2); + assert_eq!(v.patch(), 3); + } + + #[test] + fn test_zero_constant() { + assert_eq!(Version::ZERO.major(), 0); + assert_eq!(Version::ZERO.minor(), 0); + assert_eq!(Version::ZERO.patch(), 0); + } + + /// major/minor: 10 bits → max 1023 ; patch: 12 bits → max 4095 + #[test] + fn test_max_field_values() { + let v = Version::new(1023, 1023, 4095); + assert_eq!(v.major(), 1023); + assert_eq!(v.minor(), 1023); + assert_eq!(v.patch(), 4095); + } + + #[test] + #[should_panic] + fn test_major_overflow_panics() { + Version::new(1024, 0, 0); + } + + #[test] + #[should_panic] + fn test_patch_overflow_panics() { + Version::new(0, 0, 4096); + } + + // -- Bumping -- + + #[test] + fn test_bump_major_resets_minor_and_patch() { + let v = Version::new(1, 5, 3).bump_major(); + assert_eq!((v.major(), v.minor(), v.patch()), (2, 0, 0)); + } + + #[test] + fn test_bump_minor_resets_patch() { + let v = Version::new(1, 5, 3).bump_minor(); + assert_eq!((v.major(), v.minor(), v.patch()), (1, 6, 0)); + } + + #[test] + fn test_bump_patch() { + let v = Version::new(1, 5, 3).bump_patch(); + assert_eq!((v.major(), v.minor(), v.patch()), (1, 5, 4)); + } + + // -- Ordering -- + + #[test] + fn test_ordering() { + assert!(Version::new(2, 0, 0) > Version::new(1, 9, 9)); + assert!(Version::new(1, 1, 0) > Version::new(1, 0, 99)); + assert!(Version::new(1, 0, 1) > Version::new(1, 0, 0)); + assert_eq!(Version::new(1, 2, 3), Version::new(1, 2, 3)); + } + + // -- Compatibility -- + + #[test] + fn test_compatible_same_major_higher_minor() { + assert!(Version::new(1, 3, 0).is_compatible_with(Version::new(1, 2, 0))); + } + + #[test] + fn test_incompatible_different_major() { + assert!(!Version::new(2, 0, 0).is_compatible_with(Version::new(1, 0, 0))); + } + + #[test] + fn test_incompatible_older_version() { + assert!(!Version::new(1, 1, 0).is_compatible_with(Version::new(1, 2, 0))); + } + + #[test] + fn test_prerelease() { + assert!(Version::new(0, 9, 0).is_prerelease()); + assert!(!Version::new(1, 0, 0).is_prerelease()); + } + + // -- Formatting & parsing -- + + #[test] + fn test_display() { + assert_eq!(Version::new(1, 2, 3).to_string(), "1.2.3"); + } + + #[test] + fn test_parse_plain() { + let v: Version = "1.2.3".parse().unwrap(); + assert_eq!((v.major(), v.minor(), v.patch()), (1, 2, 3)); + } + + #[test] + fn test_parse_with_v_prefix() { + let v: Version = "v2.0.0".parse().unwrap(); + assert_eq!(v.major(), 2); + } + + #[test] + fn test_parse_invalid() { + assert!("1.2".parse::().is_err()); + assert!("abc".parse::().is_err()); + assert!("1.x.3".parse::().is_err()); + } + + #[test] + fn test_parse_display_roundtrip() { + let original = Version::new(3, 14, 42); + let roundtripped: Version = original.to_string().parse().unwrap(); + assert_eq!(original, roundtripped); + } + + // -- Raw conversion -- + + #[test] + fn test_from_into_u32() { + let v = Version::new(1, 2, 3); + let raw: u32 = v.into(); + let back = Version::from(raw); + assert_eq!(v, back); + } +} diff --git a/Engine/Source/world/Cargo.toml b/Engine/Source/world/Cargo.toml index 75940c1..04b1e9e 100644 --- a/Engine/Source/world/Cargo.toml +++ b/Engine/Source/world/Cargo.toml @@ -8,3 +8,5 @@ version.workspace = true [dependencies] glam.workspace = true + +utils.workspace = true diff --git a/Engine/Source/world/src/components.rs b/Engine/Source/world/src/components.rs index 7b08f66..1652057 100644 --- a/Engine/Source/world/src/components.rs +++ b/Engine/Source/world/src/components.rs @@ -1,11 +1,86 @@ -/// Holds the visual description of an entity. +use glam::{Mat4, Vec3}; + #[derive(Debug, Clone)] -// TODO: actually renderable -pub struct Renderable; +pub struct Renderable { + /// The name of the model to be rendered for this entity + pub model: String, +} #[derive(Debug, Clone)] pub struct Transform { - pub position: glam::Vec3, - pub rotation: glam::Vec3, - pub scale: glam::Vec3, + pub location: Vec3, + pub rotation: Vec3, + pub scale: Vec3, +} + +impl Transform { + pub fn matrix(&self) -> glam::Mat4 { + let translation = Mat4::from_translation(self.location); + let scale = Mat4::from_scale(self.scale); + let rot_x = Mat4::from_rotation_y(self.rotation.x.to_radians()); + let rot_y = Mat4::from_rotation_x(self.rotation.y.to_radians()); + let rot_z = Mat4::from_rotation_z(self.rotation.z.to_radians()); + + translation * scale * rot_x * rot_y * rot_z + } + pub fn rotation_quat(&self) -> glam::Quat { + glam::Quat::from_euler( + glam::EulerRot::YXZ, + self.rotation.x.to_radians(), + self.rotation.y.to_radians(), + self.rotation.z.to_radians(), + ) + } + pub fn forward(&self) -> glam::Vec3 { + self.rotation_quat() * utils::FORWARD_DIRECTION + } + /// Gets the view matrix for this transform's position & look-at from rotation. + pub fn view(&self) -> glam::Mat4 { + glam::Mat4::look_at_lh( + self.location, + self.location + self.forward(), + utils::UP_DIRECTION, + ) + } +} + +impl From for glam::Mat4 { + fn from(transform: Transform) -> Self { + transform.matrix() + } +} + +#[derive(Debug)] +pub struct Camera { + /// In radians + pub fov: f32, + pub near_clip: f32, + pub far_clip: f32, + pub width: f32, + pub height: f32, +} + +impl Camera { + pub fn projection(&self) -> glam::Mat4 { + let mut proj = glam::Mat4::perspective_rh( + self.fov, + self.width / self.height, + self.near_clip, + self.far_clip, + ); + proj.y_axis.y *= -1.; + proj + } +} + +impl Default for Camera { + fn default() -> Self { + Self { + fov: f32::to_radians(45.), + near_clip: 1., + far_clip: 100., + width: 100., + height: 100., + } + } } diff --git a/Engine/Source/world/src/lib.rs b/Engine/Source/world/src/lib.rs index db9f1d5..f32dea0 100644 --- a/Engine/Source/world/src/lib.rs +++ b/Engine/Source/world/src/lib.rs @@ -12,6 +12,8 @@ use components::*; /// An Entity is nothing more than a unique numeric identifier. pub type Entity = u32; +/// An identifier used to find the world. +pub type WorldId = u32; /// Component trait. Should not be implemented manually. /// Should be implemented by the [define_components] macro. @@ -28,7 +30,7 @@ macro_rules! define_components { ( $( $C:ident ),* $(,)? ) => { #[allow(non_snake_case)] - #[derive(Default)] + #[derive(Debug, Default)] pub struct Components { $( $C: HashMap, )* } @@ -52,12 +54,13 @@ macro_rules! define_components { }; } -define_components!(Transform, Renderable); +define_components!(Transform, Renderable, Camera); /// Stores all the entities and their components. Handles state /// of all the entities in the world. -#[derive(Default)] +#[derive(Debug)] pub struct World { + id: WorldId, next_id: Entity, alive: Vec, components: Components, @@ -65,8 +68,16 @@ pub struct World { impl World { /// Creates a new empty world. - pub fn new() -> Self { - Self::default() + pub fn new(id: WorldId) -> Self { + Self { + id, + next_id: 0, + alive: Vec::new(), + components: Components::default(), + } + } + pub fn id(&self) -> WorldId { + self.id } /// Spawn a new entity and return its ID.