diff --git a/Cargo.lock b/Cargo.lock index 001bfcc..84d93d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,16 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" +[[package]] +name = "accesskit" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d3b8f9bae46a948369bc4a03e815d4ed6d616bd00de4051133a5019dc31c5a" +dependencies = [ + "enumn", + "serde", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -46,12 +56,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", + "serde", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + [[package]] name = "android-activity" version = "0.6.0" @@ -136,23 +153,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] -name = "arboard" -version = "3.4.1" +name = "arbitrary" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" + +[[package]] +name = "arboard" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1df21f715862ede32a0c525ce2ca4d52626bb0007f8c18b87a384503ac33e70" dependencies = [ "clipboard-win", - "core-graphics", "image", "log", - "objc2 0.5.2", - "objc2-app-kit", - "objc2-foundation 0.2.2", + "objc2 0.6.0", + "objc2-app-kit 0.3.0", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.0", "parking_lot", - "windows-sys 0.48.0", + "percent-encoding", + "windows-sys 0.59.0", "x11rb", ] +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -203,6 +239,29 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" +dependencies = [ + "arrayvec", +] + [[package]] name = "backtrace" version = "0.3.71" @@ -218,6 +277,18 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -230,6 +301,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + [[package]] name = "block" version = "0.1.6" @@ -245,6 +322,12 @@ dependencies = [ "objc2 0.5.2", ] +[[package]] +name = "built" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" + [[package]] name = "bumpalo" version = "3.17.0" @@ -262,15 +345,21 @@ dependencies = [ [[package]] name = "bytemuck_derive" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ff22c2722516255d1823ce3cc4bc0b154dbc9364be5c905d6baa6eccbbc8774" +checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "byteorder-lite" version = "0.1.0" @@ -326,6 +415,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -340,9 +439,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" -version = "4.5.34" +version = "4.5.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" +checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" dependencies = [ "clap_builder", "clap_derive", @@ -350,9 +449,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.34" +version = "4.5.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" +checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" dependencies = [ "anstream", "anstyle", @@ -453,6 +552,12 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.3" @@ -537,12 +642,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + [[package]] name = "cursor-icon" version = "1.1.0" @@ -661,6 +791,7 @@ checksum = "bc4feb366740ded31a004a0e4452fbf84e80ef432ecf8314c485210229672fd1" dependencies = [ "bytemuck", "emath", + "serde", ] [[package]] @@ -669,6 +800,7 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3" dependencies = [ + "accesskit", "ahash", "bitflags 2.9.0", "emath", @@ -676,6 +808,7 @@ dependencies = [ "log", "nohash-hasher", "profiling", + "serde", ] [[package]] @@ -710,6 +843,25 @@ dependencies = [ "winit", ] +[[package]] +name = "egui_tiles" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67756b63b283a65bd0534b0c2a5fb1a12a5768bb6383d422147cc93193d09cfc" +dependencies = [ + "ahash", + "egui", + "itertools 0.13.0", + "log", + "serde", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "emath" version = "0.31.1" @@ -717,6 +869,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" dependencies = [ "bytemuck", + "serde", ] [[package]] @@ -729,11 +882,13 @@ dependencies = [ "color-eyre", "egui", "egui-winit", + "egui_tiles", "gfx_hal", "glam", "raw-window-handle", "renderer", "resource_manager", + "scene", "shared", "thiserror 2.0.12", "tracing", @@ -741,6 +896,17 @@ dependencies = [ "winit", ] +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "epaint" version = "0.31.1" @@ -757,6 +923,7 @@ dependencies = [ "nohash-hasher", "parking_lot", "profiling", + "serde", ] [[package]] @@ -787,6 +954,21 @@ version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide 0.8.7", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "eyre" version = "0.6.12" @@ -808,12 +990,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", - "miniz_oxide 0.8.5", + "miniz_oxide 0.8.7", ] [[package]] @@ -876,7 +1058,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -891,6 +1085,16 @@ dependencies = [ "winit", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.28.1" @@ -907,6 +1111,45 @@ dependencies = [ "num-traits", ] +[[package]] +name = "gltf" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ce1918195723ce6ac74e80542c5a96a40c2b26162c1957a5cd70799b8cacf7" +dependencies = [ + "base64", + "byteorder", + "gltf-json", + "image", + "lazy_static", + "serde_json", + "urlencoding", +] + +[[package]] +name = "gltf-derive" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14070e711538afba5d6c807edb74bcb84e5dbb9211a3bf5dea0dfab5b24f4c51" +dependencies = [ + "inflections", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gltf-json" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6176f9d60a7eab0a877e8e96548605dedbde9190a7ae1e80bbcc1c9af03ab14" +dependencies = [ + "gltf-derive", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "gpu-allocator" version = "0.27.0" @@ -920,6 +1163,16 @@ dependencies = [ "windows", ] +[[package]] +name = "half" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -1100,11 +1353,37 @@ checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" dependencies = [ "bytemuck", "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", "num-traits", "png", + "qoi", + "ravif", + "rayon", + "rgb", "tiff", + "zune-core", + "zune-jpeg", ] +[[package]] +name = "image-webp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" + [[package]] name = "indenter" version = "0.3.3" @@ -1121,6 +1400,23 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inflections" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a" + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1128,10 +1424,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] -name = "itoa" -version = "1.0.14" +name = "itertools" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jni" @@ -1157,10 +1471,11 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ + "getrandom 0.3.2", "libc", ] @@ -1186,12 +1501,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libc" version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +[[package]] +name = "libfuzzer-sys" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.8.6" @@ -1256,6 +1587,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1265,6 +1605,16 @@ dependencies = [ "libc", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1289,6 +1639,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.4" @@ -1300,9 +1656,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" dependencies = [ "adler2", "simd-adler32", @@ -1338,12 +1694,34 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nohash-hasher" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1354,6 +1732,47 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1435,6 +1854,18 @@ dependencies = [ "objc2-quartz-core", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.0", + "objc2-core-graphics", + "objc2-foundation 0.3.0", +] + [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -1471,6 +1902,28 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.0", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.0", + "objc2-core-foundation", + "objc2-io-surface", +] + [[package]] name = "objc2-core-image" version = "0.2.2" @@ -1522,6 +1975,18 @@ checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" dependencies = [ "bitflags 2.9.0", "objc2 0.6.0", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.0", + "objc2-core-foundation", ] [[package]] @@ -1532,7 +1997,7 @@ checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ "block2", "objc2 0.5.2", - "objc2-app-kit", + "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", ] @@ -1627,9 +2092,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2806eaa3524762875e21c3dcd057bc4b7bfa01ce4da8d46be1cd43649e1cc6b" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "orbclient" @@ -1684,6 +2149,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1732,7 +2203,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide 0.8.5", + "miniz_oxide 0.8.7", ] [[package]] @@ -1750,6 +2221,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.24", +] + [[package]] name = "presser" version = "0.3.1" @@ -1779,12 +2259,40 @@ name = "profiling" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.37.3" +version = "0.37.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf763ab1c7a3aa408be466efc86efe35ed1bd3dd74173ed39d6b0d0a6f0ba148" +checksum = "a4ce8c88de324ff838700f36fb6ab86c96df0e3c4ab6ef3a9b2044465cce1369" dependencies = [ "memchr", ] @@ -1798,6 +2306,92 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.12.1", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "system-deps", + "thiserror 1.0.69", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -1816,6 +2410,26 @@ dependencies = [ "raw-window-handle", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1847,6 +2461,7 @@ dependencies = [ "glam", "gpu-allocator", "resource_manager", + "scene", "shaderc", "shared", "thiserror 2.0.12", @@ -1861,11 +2476,19 @@ version = "0.1.0" dependencies = [ "ash", "gfx_hal", + "gltf", "gpu-allocator", + "image", "thiserror 2.0.12", "tracing", ] +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" + [[package]] name = "roxmltree" version = "0.20.0" @@ -1899,9 +2522,9 @@ checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "ryu" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -1912,6 +2535,19 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scene" +version = "0.1.0" +dependencies = [ + "ash", + "glam", + "gltf", + "resource_manager", + "shared", + "thiserror 2.0.12", + "tracing", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -1959,9 +2595,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.139" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -1969,6 +2605,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "shaderc" version = "0.9.1" @@ -2024,6 +2669,15 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "slab" version = "0.4.9" @@ -2124,6 +2778,25 @@ dependencies = [ "syn", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "thiserror" version = "1.0.69" @@ -2220,11 +2893,26 @@ dependencies = [ "zerovec", ] +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -2233,6 +2921,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] @@ -2346,6 +3036,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf16_iter" version = "1.0.5" @@ -2364,12 +3060,29 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.5" @@ -2392,6 +3105,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -2719,15 +3441,6 @@ dependencies = [ "windows-targets 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -2948,7 +3661,7 @@ dependencies = [ "memmap2", "ndk", "objc2 0.5.2", - "objc2-app-kit", + "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", "objc2-ui-kit", "orbclient", @@ -2985,6 +3698,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -3084,7 +3806,16 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive 0.8.24", ] [[package]] @@ -3098,6 +3829,17 @@ dependencies = [ "syn", ] +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.6" @@ -3140,3 +3882,27 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 539c227..0cdd355 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,9 @@ members = [ "crates/engine", "crates/gfx_hal", "crates/renderer", - "crates/resource_manager", "crates/shared", + "crates/resource_manager", + "crates/scene", + "crates/shared", ] [workspace.dependencies] @@ -24,10 +26,12 @@ egui-ash-renderer = { version = "0.8.0", features = [ "dynamic-rendering", ] } egui = "0.31" +egui_tiles = "0.12" bytemuck = { version = "1.21.0", features = ["derive"] } -tracing = "0.1" +tracing = { features = ["release_max_level_warn"], version = "0.1" } tracing-subscriber = { version = "0.3", features = ["json"] } thiserror = "2.0.12" +gltf = "1.4.1" # # Enable incremental by default in release mode. @@ -56,4 +60,7 @@ thiserror = "2.0.12" # # rustflags = ["-Zshare-generics=off"] # codegen-units = 1 +opt-level = 1 + +[profile.dev.package."*"] opt-level = 3 diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 2fd8816..c111b04 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] egui.workspace = true +egui_tiles.workspace = true ash.workspace = true ash-window.workspace = true color-eyre.workspace = true @@ -19,6 +20,7 @@ gfx_hal = { path = "../gfx_hal" } renderer = { path = "../renderer" } resource_manager = { path = "../resource_manager" } shared = { path = "../shared" } +scene = { path = "../scene" } clap = { version = "4.5.34", features = ["derive"] } egui-winit = "0.31.1" diff --git a/crates/engine/src/main.rs b/crates/engine/src/main.rs index e011ef6..bcd9aa2 100644 --- a/crates/engine/src/main.rs +++ b/crates/engine/src/main.rs @@ -17,14 +17,16 @@ use gfx_hal::{ use glam::Vec3; use raw_window_handle::HasDisplayHandle; use renderer::{Renderer, RendererError}; -use resource_manager::{Geometry, ResourceManager, ResourceManagerError}; -use shared::{CameraInfo, Vertex}; +use resource_manager::{ResourceManager, ResourceManagerError}; +use scene::Scene; +use shared::CameraInfo; use tracing::{debug, error, info, warn}; use tracing_subscriber::{filter, layer::SubscriberExt, util::SubscriberInitExt, Layer}; use winit::{ application::ApplicationHandler, - event::WindowEvent, + event::{ElementState, KeyEvent, MouseButton, WindowEvent}, event_loop::{ActiveEventLoop, EventLoop}, + keyboard::{KeyCode, PhysicalKey}, window::Window, }; @@ -47,6 +49,8 @@ enum AppError { NoSuitableDevice, #[error("Failed to create CString: {0}")] NulError(#[from] std::ffi::NulError), + #[error("Scene Error: {0}")] + SceneError(#[from] scene::SceneError), } struct Application { @@ -66,6 +70,23 @@ struct Application { egui_winit: State, egui_app: EditorUI, + // --- Camera State --- + camera_info: CameraInfo, + camera_speed: f32, + camera_sensitivity: f32, + + // --- Input State --- + is_forward_pressed: bool, + is_backward_pressed: bool, + is_left_pressed: bool, + is_right_pressed: bool, + is_up_pressed: bool, // Optional: For flying up + is_down_pressed: bool, // Optional: For flying down + is_rmb_pressed: bool, // Right mouse button + last_mouse_pos: Option<(f64, f64)>, + mouse_delta: (f64, f64), + capture_mouse: bool, // Flag to indicate if mouse should control camera + // Windowing window: Arc, // Use Arc for potential multi-threading later @@ -76,17 +97,15 @@ struct Application { } #[derive(Default)] -struct EditorUI { - camera_info: CameraInfo, -} +struct EditorUI {} impl EditorUI { fn title() -> String { "engine".to_string() } - fn build_ui(&mut self, ctx: &egui::Context, current_fps: f64) { - egui::Window::new(Self::title()).show(ctx, |ui| { + fn build_ui(&mut self, ctx: &egui::Context, current_fps: f64, camera_info: &mut CameraInfo) { + egui::SidePanel::new(egui::panel::Side::Left, Self::title()).show(ctx, |ui| { ui.label(format!("FPS - {:.2}", current_fps)); ui.separator(); @@ -96,12 +115,78 @@ impl EditorUI { .striped(true) .show(ui, |ui| { ui.label("FOV"); - ui.add(Slider::new(&mut self.camera_info.camera_fov, 10.0..=120.0)); + // Modify the passed-in camera_info + ui.add(Slider::new(&mut camera_info.camera_fov, 10.0..=120.0)); + ui.end_row(); // Good practice in grids + + // You could add more camera controls here if needed + // e.g., sliders for position, target (though direct manipulation is better) + ui.label("Camera Pos"); + ui.label(format!( + "({:.1}, {:.1}, {:.1})", + camera_info.camera_pos.x, + camera_info.camera_pos.y, + camera_info.camera_pos.z + )); + ui.end_row(); + + ui.label("Camera Target"); + ui.label(format!( + "({:.1}, {:.1}, {:.1})", + camera_info.camera_target.x, + camera_info.camera_target.y, + camera_info.camera_target.z + )); + ui.end_row(); }); + + ui.separator(); + ui.label("Controls:"); + ui.label("RMB + Drag: Look"); + ui.label("WASD: Move"); + ui.label("Space: Up"); + ui.label("Shift: Down"); + ui.label("Hold RMB to activate controls."); }); + + // let mut tree = create_tree(); + // + // egui::panel::SidePanel::new(egui::panel::Side::Left, Id::new("main_panel")).show( + // ctx, + // |ui| { + // let mut behavior = TreeBehavior {}; + // tree.ui(&mut behavior, ui); + // }, + // ); } } +fn create_tree() -> egui_tiles::Tree { + let mut next_view_nr = 0; + let mut gen_pane = || { + let pane = EditorUI {}; + next_view_nr += 1; + pane + }; + + let mut tiles = egui_tiles::Tiles::default(); + + let mut tabs = vec![]; + tabs.push({ + let children = (0..7).map(|_| tiles.insert_pane(gen_pane())).collect(); + tiles.insert_horizontal_tile(children) + }); + tabs.push({ + let cells = (0..11).map(|_| tiles.insert_pane(gen_pane())).collect(); + tiles.insert_grid_tile(cells) + }); + tabs.push(tiles.insert_pane(gen_pane())); + + let root = tiles.insert_tab_tile(tabs); + + egui_tiles::Tree::new("my_tree", root, tiles) +} + #[derive(Default)] struct ApplicationWrapper { app: Option, @@ -264,56 +349,10 @@ impl Application { let resource_manager = Arc::new(ResourceManager::new(instance.clone(), device.clone())?); info!("Resource Manager initialized."); - let vertices = vec![ - // Define 8 vertices for a cube with positions and colors - // Make sure winding order is correct (e.g., counter-clockwise for front faces) - Vertex { - pos: Vec3::new(-0.5, -0.5, 0.5).into(), - color: Vec3::new(1.0, 0.0, 0.0).into(), - }, // Front bottom left 0 - Vertex { - pos: Vec3::new(0.5, -0.5, 0.5).into(), - color: Vec3::new(0.0, 1.0, 0.0).into(), - }, // Front bottom right 1 - Vertex { - pos: Vec3::new(0.5, 0.5, 0.5).into(), - color: Vec3::new(0.0, 0.0, 1.0).into(), - }, // Front top right 2 - Vertex { - pos: Vec3::new(-0.5, 0.5, 0.5).into(), - color: Vec3::new(1.0, 1.0, 0.0).into(), - }, // Front top left 3 - // ... add back face vertices (4-7) ... - Vertex { - pos: Vec3::new(-0.5, -0.5, -0.5).into(), - color: Vec3::new(1.0, 0.0, 1.0).into(), - }, // Back bottom left 4 - Vertex { - pos: Vec3::new(0.5, -0.5, -0.5).into(), - color: Vec3::new(0.0, 1.0, 1.0).into(), - }, // Back bottom right 5 - Vertex { - pos: Vec3::new(0.5, 0.5, -0.5).into(), - color: Vec3::new(0.5, 0.5, 0.5).into(), - }, // Back top right 6 - Vertex { - pos: Vec3::new(-0.5, 0.5, -0.5).into(), - color: Vec3::new(1.0, 1.0, 1.0).into(), - }, // Back top left 7 - ]; - - let indices = vec![ - // Define 12 triangles (36 indices) for the cube faces - // Front face - 0, 1, 2, 2, 3, 0, // Right face - 1, 5, 6, 6, 2, 1, // Back face - 5, 4, 7, 7, 6, 5, // Left face - 4, 0, 3, 3, 7, 4, // Top face - 3, 2, 6, 6, 7, 3, // Bottom face - 4, 5, 1, 1, 0, 4, - ]; - - let cube_geometry = Geometry::new(resource_manager.clone(), &vertices, &indices)?; + let scene = Scene::from_gltf( + "./sponza/NewSponza_Main_glTF_003.gltf", + resource_manager.clone(), + )?; // --- 5. Renderer --- let initial_size = window.inner_size(); @@ -323,7 +362,7 @@ impl Application { graphics_queue.clone(), surface.clone(), resource_manager.clone(), - vec![cube_geometry], + scene, initial_size.width, initial_size.height, )?; @@ -341,6 +380,8 @@ impl Application { info!("Renderer initialized."); + let camera_info = CameraInfo::default(); // Get default camera settings + Ok(Self { _instance: instance, _physical_device: physical_device, @@ -353,6 +394,23 @@ impl Application { egui_winit, egui_ctx, egui_app, + + // --- Camera --- + camera_info, // Store the camera state here + camera_speed: 5.0, // Adjust as needed + camera_sensitivity: 0.002, // Adjust as needed + + // --- Input --- + is_forward_pressed: false, + is_backward_pressed: false, + is_left_pressed: false, + is_right_pressed: false, + is_up_pressed: false, + is_down_pressed: false, + is_rmb_pressed: false, + last_mouse_pos: None, + mouse_delta: (0.0, 0.0), + capture_mouse: false, // Start with mouse free frame_count: 0, current_fps: 0., last_fps_update_time: Instant::now(), @@ -361,7 +419,11 @@ impl Application { } fn handle_event(&mut self, event: &WindowEvent, active_event_loop: &ActiveEventLoop) { - let _ = self.egui_winit.on_window_event(&self.window, event); + // Let egui process the event first + let egui_consumed_event = self.egui_winit.on_window_event(&self.window, event); + + // Only process input for camera if egui didn't consume it AND we are capturing + let process_camera_input = !egui_consumed_event.consumed && self.capture_mouse; match event { WindowEvent::CloseRequested => { @@ -385,29 +447,127 @@ impl Application { .resize(new_inner_size.width, new_inner_size.height); } // Handle other inputs if not consumed by egui - WindowEvent::KeyboardInput { .. } - | WindowEvent::CursorMoved { .. } - | WindowEvent::MouseInput { .. } => {} + WindowEvent::MouseInput { state, button, .. } => { + if *button == MouseButton::Right { + let is_pressed = *state == ElementState::Pressed; + self.is_rmb_pressed = is_pressed; + + // Decide whether to capture/release mouse based on RMB + // Only capture if pressed *outside* an egui interactive area + if is_pressed && !self.egui_ctx.is_pointer_over_area() { + self.capture_mouse = true; + self.window + .set_cursor_grab(winit::window::CursorGrabMode::Confined) + .or_else(|_| { + self.window + .set_cursor_grab(winit::window::CursorGrabMode::Locked) + }) + .unwrap_or(()); + self.window.set_cursor_visible(false); + self.last_mouse_pos = None; // Reset last pos on capture start + } else if !is_pressed { + self.capture_mouse = false; + self.window + .set_cursor_grab(winit::window::CursorGrabMode::None) + .unwrap_or(()); + self.window.set_cursor_visible(true); + self.mouse_delta = (0.0, 0.0); // Stop camera movement + } + } + // Let egui handle its mouse clicks regardless of capture state + // (handled by on_window_event) + } + + WindowEvent::CursorMoved { position, .. } => { + let current_pos = (position.x, position.y); + if self.capture_mouse { + // Only calculate delta if capturing + if let Some(last_pos) = self.last_mouse_pos { + self.mouse_delta.0 += current_pos.0 - last_pos.0; + self.mouse_delta.1 += current_pos.1 - last_pos.1; + } + // Store position relative to window center might be more robust + // with set_cursor_position, but this works with grab/confine too. + self.last_mouse_pos = Some(current_pos); + } else { + // Still update egui's pointer position even if not capturing + // (handled by on_window_event) + self.last_mouse_pos = None; // Reset if not capturing + } + } + + // Use PhysicalKey for layout-independent keys + WindowEvent::KeyboardInput { + event: + KeyEvent { + physical_key, + state, + .. + }, + .. + } => { + // Let egui handle keyboard input first if it wants it + if egui_consumed_event.consumed { + return; + } + + let is_pressed = *state == ElementState::Pressed; + match physical_key { + PhysicalKey::Code(KeyCode::KeyW) | PhysicalKey::Code(KeyCode::ArrowUp) => { + self.is_forward_pressed = is_pressed; + } + PhysicalKey::Code(KeyCode::KeyS) | PhysicalKey::Code(KeyCode::ArrowDown) => { + self.is_backward_pressed = is_pressed; + } + PhysicalKey::Code(KeyCode::KeyA) | PhysicalKey::Code(KeyCode::ArrowLeft) => { + self.is_left_pressed = is_pressed; + } + PhysicalKey::Code(KeyCode::KeyD) | PhysicalKey::Code(KeyCode::ArrowRight) => { + self.is_right_pressed = is_pressed; + } + PhysicalKey::Code(KeyCode::Space) => { + self.is_up_pressed = is_pressed; + } + PhysicalKey::Code(KeyCode::ShiftLeft) + | PhysicalKey::Code(KeyCode::ShiftRight) => { + self.is_down_pressed = is_pressed; + } + // Optional: Escape to release mouse capture + PhysicalKey::Code(KeyCode::Escape) if is_pressed && self.capture_mouse => { + self.capture_mouse = false; + self.is_rmb_pressed = false; // Ensure RMB state is also reset + self.window + .set_cursor_grab(winit::window::CursorGrabMode::None) + .unwrap_or(()); + self.window.set_cursor_visible(true); + self.mouse_delta = (0.0, 0.0); + } + _ => {} + } + } WindowEvent::RedrawRequested => { let now = Instant::now(); - let _delta_time = now.duration_since(self.last_frame_time); + let delta_time = now.duration_since(self.last_frame_time).as_secs_f32(); self.last_frame_time = now; - let elapsed_sice_last_update = now.duration_since(self.last_fps_update_time); + // --- FPS Calculation --- + let elapsed_since_last_update = now.duration_since(self.last_fps_update_time); self.frame_count += 1; - - if elapsed_sice_last_update >= Duration::from_secs(1) { - let fps = self.frame_count as f64 / elapsed_sice_last_update.as_secs_f64(); - self.current_fps = fps; - - let new_title = format!("{} - {} - {:.0} FPS", ENGINE_NAME, APP_NAME, fps); + if elapsed_since_last_update >= Duration::from_secs(1) { + self.current_fps = + self.frame_count as f64 / elapsed_since_last_update.as_secs_f64(); + let new_title = format!( + "{} - {} - {:.0} FPS", + ENGINE_NAME, APP_NAME, self.current_fps + ); self.window.set_title(&new_title); - self.frame_count = 0; self.last_fps_update_time = now; } + self.update_camera(delta_time); // Call the new update function + let raw_input = self.egui_winit.take_egui_input(&self.window); let egui::FullOutput { @@ -417,7 +577,8 @@ impl Application { pixels_per_point, .. } = self.egui_ctx.run(raw_input, |ctx| { - self.egui_app.build_ui(ctx, self.current_fps); + self.egui_app + .build_ui(ctx, self.current_fps, &mut self.camera_info); }); self.renderer.update_textures(textures_delta).unwrap(); @@ -431,7 +592,7 @@ impl Application { match self.renderer.render_frame( pixels_per_point, &clipped_primitives, - self.egui_app.camera_info, + self.camera_info, ) { Ok(_) => { self.window.request_redraw(); @@ -454,6 +615,106 @@ impl Application { _ => {} } } + + // --- New Camera Update Function --- + fn update_camera(&mut self, dt: f32) { + if !self.capture_mouse + && self.mouse_delta == (0.0, 0.0) + && !self.is_forward_pressed + && !self.is_backward_pressed + && !self.is_left_pressed + && !self.is_right_pressed + && !self.is_up_pressed + && !self.is_down_pressed + { + return; // No input, no update needed + } + + let mut cam_pos = self.camera_info.camera_pos; + let mut cam_target = self.camera_info.camera_target; + let cam_up = self.camera_info.camera_up; // Usually Vec3::Y + + // --- Mouse Look (Rotation) --- + if self.capture_mouse && self.mouse_delta != (0.0, 0.0) { + let (delta_x, delta_y) = self.mouse_delta; + self.mouse_delta = (0.0, 0.0); // Consume the delta + + let sensitivity = self.camera_sensitivity; + let yaw_delta = delta_x as f32 * sensitivity; + let pitch_delta = delta_y as f32 * sensitivity; + + let forward_dir = (cam_target - cam_pos).normalize(); + let right_dir = forward_dir.cross(cam_up).normalize(); + // Recalculate up to prevent roll if needed, though cross product handles it here + let current_up = right_dir.cross(forward_dir).normalize(); + + // --- Pitch (Up/Down) --- + // Calculate new forward direction based on pitch rotation around right axis + let pitch_quat = glam::Quat::from_axis_angle(right_dir, -pitch_delta); // Negative for standard mouse look + let mut new_forward = pitch_quat * forward_dir; + + // Clamp pitch to avoid flipping over (e.g., +/- 89 degrees) + let max_pitch_angle = 89.0f32.to_radians(); + let current_pitch = new_forward.angle_between(cam_up) - 90.0f32.to_radians(); + if current_pitch.abs() > max_pitch_angle { + // Revert pitch if it exceeds limits + new_forward = forward_dir; // Keep previous forward if clamp needed + } + + // --- Yaw (Left/Right) --- + // Rotate the (potentially pitch-adjusted) forward direction and right vector around the global up axis (Y) + let yaw_quat = glam::Quat::from_axis_angle(Vec3::Y, -yaw_delta); // Negative for standard mouse look + new_forward = yaw_quat * new_forward; + + // Update target based on the new forward direction + cam_target = cam_pos + new_forward; + + // Update the camera's internal up vector based on yaw rotation as well + // This prevents weird tilting when looking straight up/down if up wasn't Vec3::Y + // self.camera_info.camera_up = yaw_quat * current_up; // Optional: only if up can change + } + + // --- Keyboard Movement --- + let forward_dir = (cam_target - cam_pos).normalize(); + // Use Vec3::Y for world-relative right/up movement, or calculate from forward/up + let right_dir = forward_dir.cross(Vec3::Y).normalize(); + // let up_dir = right_dir.cross(forward_dir).normalize(); // Camera's local up + let world_up_dir = Vec3::Y; // Use world up for space/shift + + let effective_speed = self.camera_speed * dt; + let mut move_delta = Vec3::ZERO; + + if self.is_forward_pressed { + move_delta += forward_dir; + } + if self.is_backward_pressed { + move_delta -= forward_dir; + } + if self.is_left_pressed { + move_delta -= right_dir; + } + if self.is_right_pressed { + move_delta += right_dir; + } + if self.is_up_pressed { + move_delta += world_up_dir; // Move along world Y + } + if self.is_down_pressed { + move_delta -= world_up_dir; // Move along world Y + } + + // Normalize move_delta if non-zero to ensure consistent speed diagonally + if move_delta != Vec3::ZERO { + let move_vec = move_delta.normalize() * effective_speed; + cam_pos += move_vec; + cam_target += move_vec; // Move target along with position + } + + // --- Apply Changes --- + self.camera_info.camera_pos = cam_pos; + self.camera_info.camera_target = cam_target; + // self.camera_info.camera_up remains Vec3::Y usually + } } // --- Helper Functions --- diff --git a/crates/renderer/Cargo.toml b/crates/renderer/Cargo.toml index 9da1bc2..c82ec34 100644 --- a/crates/renderer/Cargo.toml +++ b/crates/renderer/Cargo.toml @@ -17,6 +17,7 @@ winit.workspace = true gfx_hal = { path = "../gfx_hal" } resource_manager = { path = "../resource_manager" } shared = { path = "../shared" } +scene = { path = "../scene" } [build-dependencies] shaderc = "0.9.1" diff --git a/crates/renderer/src/lib.rs b/crates/renderer/src/lib.rs index 5e32a9c..fd69ff9 100644 --- a/crates/renderer/src/lib.rs +++ b/crates/renderer/src/lib.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashMap, ffi::c_void, mem, sync::{Arc, Mutex}, @@ -17,12 +18,15 @@ use gpu_allocator::{ vulkan::{Allocation, AllocationCreateDesc, Allocator}, MemoryLocation, }; -use resource_manager::{Geometry, ImageHandle, ResourceManager, ResourceManagerError}; +use resource_manager::{ + ImageHandle, Material, ResourceManager, ResourceManagerError, SamplerHandle, Texture, +}; use shared::{CameraInfo, UniformBufferObject}; use thiserror::Error; use tracing::{debug, error, info, warn}; const MAX_FRAMES_IN_FLIGHT: usize = 2; +const MAX_MATERIALS: usize = 150; #[derive(Debug, Error)] pub enum RendererError { @@ -60,6 +64,9 @@ pub enum RendererError { AllocatorUnavailable, // Added based on egui requirement #[error("Allocator Error: {0}")] AllocatorError(#[from] gpu_allocator::AllocationError), + + #[error("Other Error: {0}")] + Other(String), } impl From> for RendererError { @@ -101,11 +108,13 @@ pub struct Renderer { swapchain_format: vk::SurfaceFormatKHR, swapchain_extent: vk::Extent2D, - scene: Vec, + scene: scene::Scene, descriptor_set_layout: vk::DescriptorSetLayout, descriptor_pool: vk::DescriptorPool, + material_descriptor_set_layout: vk::DescriptorSetLayout, + egui_renderer: EguiRenderer, depth_image_handle: ImageHandle, @@ -115,6 +124,11 @@ pub struct Renderer { model_pipeline_layout: vk::PipelineLayout, model_pipeline: vk::Pipeline, + material_descriptor_sets: HashMap, + + default_white_texture: Option>, + default_sampler: SamplerHandle, + frames_data: Vec, current_frame: usize, @@ -134,7 +148,7 @@ impl Renderer { graphics_queue: Arc, surface: Arc, resource_manager: Arc, - scene: Vec, + scene: scene::Scene, initial_width: u32, initial_height: u32, ) -> Result { @@ -154,14 +168,18 @@ impl Renderer { let (depth_image_handle, depth_image_view) = Self::create_depth_resources(&device, &resource_manager, extent, depth_format)?; - let (descriptor_set_layout, descriptor_pool) = - Self::create_descriptor_sets_resources(&device)?; + let descriptor_set_layout = Self::create_descriptor_set_layout(&device)?; + let material_descriptor_set_layout = Self::create_material_descriptor_set_layout(&device)?; + + let descriptor_set_layouts = [descriptor_set_layout, material_descriptor_set_layout]; + + let descriptor_pool = Self::create_descriptor_pool(&device)?; let (model_pipeline_layout, model_pipeline) = Self::create_model_pipeline( &device, format.format, depth_format, - descriptor_set_layout, + &descriptor_set_layouts, )?; let start_time = Instant::now(); @@ -170,9 +188,8 @@ impl Renderer { &device, &resource_manager, descriptor_pool, - descriptor_set_layout, + &descriptor_set_layouts, swapchain.extent(), - start_time, )?; info!("Renderer initialized successfully."); @@ -191,6 +208,13 @@ impl Renderer { }, )?; + let default_sampler = resource_manager.get_or_create_sampler(&Default::default())?; + + let default_white_texture = Some(Self::create_default_texture( + device.clone(), + resource_manager.clone(), + )); + Ok(Self { device, graphics_queue, @@ -204,11 +228,19 @@ impl Renderer { swapchain_extent: extent, descriptor_set_layout, descriptor_pool, + + material_descriptor_set_layout, depth_image_handle, depth_image_view, depth_format, model_pipeline_layout, model_pipeline, + + material_descriptor_sets: HashMap::new(), + + default_white_texture, + default_sampler, + frames_data, scene, current_frame: 0, @@ -220,6 +252,134 @@ impl Renderer { }) } + /// Gets or creates/updates a descriptor set for a given material. + fn get_or_create_material_set( + &mut self, + material: &Arc, // Use Arc directly if hashable, or use a unique ID + ) -> Result { + // Return generic error + + // Use a unique identifier for the material instance if Arc isn't directly hashable + // or if pointer comparison isn't reliable across runs/reloads. + // For simplicity here, we use the Arc's pointer address as a key. + // WARNING: This is only safe if the Arc instances are stable! + // A better key might be derived from material.name or a generated ID. + let material_key = Arc::as_ptr(material) as usize; + + if let Some(set) = self.material_descriptor_sets.get(&material_key) { + return Ok(*set); + } + + // --- Allocate Descriptor Set --- + let layouts = [self.material_descriptor_set_layout]; + let alloc_info = vk::DescriptorSetAllocateInfo::default() + .descriptor_pool(self.descriptor_pool) + .set_layouts(&layouts); + + let descriptor_set = unsafe { self.device.raw().allocate_descriptor_sets(&alloc_info)? }[0]; + + // --- Update Descriptor Set --- + let (image_handle, view_handle, sampler_handle) = match &material.base_color_texture { + Some(texture) => { + // Get the default view handle associated with the image + let img_info = self.resource_manager.get_image_info(texture.handle)?; + let view_h = img_info.default_view_handle.ok_or(RendererError::Other( + "Image missing default view handle".to_string(), + ))?; + // Use the sampler specified by the material, or the default + let sampler_h = material.base_color_sampler.unwrap_or(self.default_sampler); + (texture.handle, view_h, sampler_h) + } + None => { + // Use default white texture + let default_tex = + self.default_white_texture + .as_ref() + .ok_or(RendererError::Other( + "Default texture not created".to_string(), + ))?; + let img_info = self.resource_manager.get_image_info(default_tex.handle)?; + let view_h = img_info.default_view_handle.ok_or(RendererError::Other( + "Default image missing default view handle".to_string(), + ))?; + (default_tex.handle, view_h, self.default_sampler) + } + }; + + // Get the actual Vulkan handles + let image_view_info = self.resource_manager.get_image_view_info(view_handle)?; + let sampler_info = self.resource_manager.get_sampler_info(sampler_handle)?; + + let image_descriptor_info = vk::DescriptorImageInfo::default() + .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) // Expected layout for sampling + .image_view(image_view_info.view) // The vk::ImageView + .sampler(sampler_info.sampler); // The vk::Sampler + + let writes = [ + // Write for binding 0 (baseColorSampler) + vk::WriteDescriptorSet::default() + .dst_set(descriptor_set) + .dst_binding(0) + .dst_array_element(0) + .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) + .image_info(std::slice::from_ref(&image_descriptor_info)), + // Add writes for other bindings (normal map, etc.) here + ]; + + unsafe { + self.device.raw().update_descriptor_sets(&writes, &[]); // Update the set + } + + // Store in cache + self.material_descriptor_sets + .insert(material_key, descriptor_set); + Ok(descriptor_set) + } + + fn create_default_texture( + device: Arc, // Need device Arc for RM + resource_manager: Arc, + ) -> Arc { + let width = 1; + let height = 1; + let data = [255u8, 255, 255, 255]; // White RGBA + let format = vk::Format::R8G8B8A8_UNORM; // Or SRGB if preferred + + let create_info = vk::ImageCreateInfo::default() + .image_type(vk::ImageType::TYPE_2D) + .format(format) + .extent(vk::Extent3D { + width, + height, + depth: 1, + }) + .mip_levels(1) + .array_layers(1) + .samples(vk::SampleCountFlags::TYPE_1) + .tiling(vk::ImageTiling::OPTIMAL) + .usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST) + .initial_layout(vk::ImageLayout::UNDEFINED); + + let handle = resource_manager + .create_image_init( + &create_info, + gpu_allocator::MemoryLocation::GpuOnly, + vk::ImageAspectFlags::COLOR, + &data, + ) + .expect("Failed to create default white texture"); + + Arc::new(Texture { + handle, + format: vk::Format::R8G8B8A8_UNORM, + extent: vk::Extent3D { + width: 1, + height: 1, + depth: 1, + }, + }) + } + pub fn resize(&mut self, width: u32, height: u32) { if width > 0 && height > 0 { self.window_resized = true; @@ -425,9 +585,7 @@ impl Renderer { .cmd_set_scissor(command_buffer, 0, &[scissor]); } - // --- Draw Triangle --- unsafe { - // Need unsafe for Vulkan commands self.device.raw().cmd_bind_pipeline( command_buffer, vk::PipelineBindPoint::GRAPHICS, @@ -444,10 +602,48 @@ impl Renderer { ); } - for g in &self.scene { - g.draw(self.device.raw(), command_buffer)?; + let meshes = self.scene.meshes.clone(); + + for mesh in meshes { + let material_set = self.get_or_create_material_set(&mesh.material)?; + + unsafe { + self.device.raw().cmd_bind_descriptor_sets( + command_buffer, + vk::PipelineBindPoint::GRAPHICS, + self.model_pipeline_layout, + 1, + &[material_set], + &[], + ); + } + + let model_matrix_bytes = unsafe { + std::slice::from_raw_parts( + mesh.transform.as_ref().as_ptr() as *const u8, + std::mem::size_of::(), + ) + }; + + unsafe { + self.device.raw().cmd_push_constants( + command_buffer, + self.model_pipeline_layout, + vk::ShaderStageFlags::VERTEX, + 0, + model_matrix_bytes, + ); + } + + mesh.geometry.draw(self.device.raw(), command_buffer)?; } + let frame_data = &mut self.frames_data[self.current_frame]; + let swapchain_ref = self + .swapchain + .as_ref() + .ok_or(RendererError::SwapchainAcquisitionFailed)?; + tracing::trace!("Rendering EGUI"); self.egui_renderer.cmd_draw( command_buffer, @@ -716,7 +912,7 @@ impl Renderer { device: &Arc, color_format: vk::Format, depth_format: vk::Format, - descriptor_set_layout: vk::DescriptorSetLayout, + descriptor_set_layouts: &[vk::DescriptorSetLayout], ) -> Result<(vk::PipelineLayout, vk::Pipeline), RendererError> { // Load compiled SPIR-V (replace with actual loading) let vert_shader_code = include_bytes!(concat!(env!("OUT_DIR"), "/shaders/vert.glsl.spv")); // Placeholder path @@ -787,9 +983,15 @@ impl Renderer { let dynamic_state = vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states); + let push_constant_range = vk::PushConstantRange::default() + .stage_flags(vk::ShaderStageFlags::VERTEX) + .offset(0) + .size(mem::size_of::() as u32); + // --- Pipeline Layout --- let layout_info = vk::PipelineLayoutCreateInfo::default() - .set_layouts(std::slice::from_ref(&descriptor_set_layout)); // No descriptors/push constants + .set_layouts(descriptor_set_layouts) + .push_constant_ranges(std::slice::from_ref(&push_constant_range)); let pipeline_layout = unsafe { device .raw() @@ -891,9 +1093,8 @@ impl Renderer { device: &Arc, resource_manager: &Arc, descriptor_pool: vk::DescriptorPool, - descriptor_set_layout: vk::DescriptorSetLayout, + descriptor_set_layouts: &[vk::DescriptorSetLayout], swapchain_extent: vk::Extent2D, - instant: Instant, ) -> Result, RendererError> { let mut frames_data = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT); for _ in 0..MAX_FRAMES_IN_FLIGHT { @@ -930,15 +1131,14 @@ impl Renderer { tracing::info!("Allocated frame_data command_buffer: {:?}", command_buffer); let descriptor_set = - Self::create_descriptor_set(device, descriptor_set_layout, descriptor_pool)?; + Self::create_descriptor_set(device, descriptor_set_layouts, descriptor_pool)?; let (uniform_buffer, uniform_buffer_allocation, uniform_buffer_mapped_ptr) = Self::create_uniform_buffer(device, resource_manager)?; Self::update_descriptor_set(device.clone(), descriptor_set, uniform_buffer); - let uniform_buffer_object = - calculate_ubo(CameraInfo::default(), swapchain_extent, instant); + let uniform_buffer_object = calculate_ubo(CameraInfo::default(), swapchain_extent); frames_data.push(FrameData { textures_to_free: None, @@ -1043,9 +1243,37 @@ impl Renderer { )) } - fn create_descriptor_sets_resources( + fn create_material_descriptor_set_layout( device: &Arc, - ) -> Result<(vk::DescriptorSetLayout, vk::DescriptorPool), RendererError> { + ) -> Result { + let bindings = [ + // Binding 0: Combined Image Sampler (baseColorSampler) + vk::DescriptorSetLayoutBinding::default() + .binding(0) + .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) + .descriptor_count(1) + .stage_flags(vk::ShaderStageFlags::FRAGMENT), // Used in fragment shader + // Add more bindings here if needed (e.g., for normal map, metallic/roughness map) + // Binding 1: Uniform Buffer (Optional: for material factors) + // vk::DescriptorSetLayoutBinding::default() + // .binding(1) + // .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER) + // .descriptor_count(1) + // .stage_flags(vk::ShaderStageFlags::FRAGMENT), + ]; + + let layout_info = vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings); + + Ok(unsafe { + device + .raw() + .create_descriptor_set_layout(&layout_info, None)? + }) + } + + fn create_descriptor_set_layout( + device: &Arc, + ) -> Result { let ubo_layout_binding = vk::DescriptorSetLayoutBinding::default() .binding(0) .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER) @@ -1061,29 +1289,38 @@ impl Renderer { .create_descriptor_set_layout(&layout_info, None)? }; - let pool_size = vk::DescriptorPoolSize { - ty: vk::DescriptorType::UNIFORM_BUFFER, - descriptor_count: MAX_FRAMES_IN_FLIGHT as u32, - }; + Ok(descriptor_set_layout) + } + + fn create_descriptor_pool(device: &Arc) -> Result { + let pool_sizes = [ + vk::DescriptorPoolSize { + ty: vk::DescriptorType::UNIFORM_BUFFER, + descriptor_count: MAX_FRAMES_IN_FLIGHT as u32, + }, + vk::DescriptorPoolSize { + ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER, + descriptor_count: MAX_MATERIALS as u32, + }, + ]; let pool_info = vk::DescriptorPoolCreateInfo::default() - .pool_sizes(std::slice::from_ref(&pool_size)) - .max_sets(MAX_FRAMES_IN_FLIGHT as u32); + .pool_sizes(&pool_sizes) + .max_sets(MAX_FRAMES_IN_FLIGHT as u32 + MAX_MATERIALS as u32); let descriptor_pool = unsafe { device.raw().create_descriptor_pool(&pool_info, None)? }; - Ok((descriptor_set_layout, descriptor_pool)) + Ok(descriptor_pool) } fn create_descriptor_set( device: &Arc, - descriptor_set_layout: vk::DescriptorSetLayout, + descriptor_set_layouts: &[vk::DescriptorSetLayout], descriptor_pool: vk::DescriptorPool, ) -> Result { - let layouts = vec![descriptor_set_layout; 1]; let alloc_info = vk::DescriptorSetAllocateInfo::default() .descriptor_pool(descriptor_pool) - .set_layouts(&layouts); + .set_layouts(descriptor_set_layouts); let descriptor_set = unsafe { device.raw().allocate_descriptor_sets(&alloc_info)? }[0]; @@ -1167,7 +1404,7 @@ impl Renderer { fn update_uniform_buffer(&mut self, camera_info: CameraInfo) -> Result<(), RendererError> { let frame_data = &mut self.frames_data[self.current_frame]; - let ubo = calculate_ubo(camera_info, self.swapchain_extent, self.start_time); + let ubo = calculate_ubo(camera_info, self.swapchain_extent); if frame_data.uniform_buffer_object != ubo { let ptr = frame_data.uniform_buffer_mapped_ptr; @@ -1181,15 +1418,7 @@ impl Renderer { } } -fn calculate_ubo( - camera_info: CameraInfo, - swapchain_extent: vk::Extent2D, - start: Instant, -) -> UniformBufferObject { - let time = start.elapsed(); - - let model = Mat4::from_rotation_y(time.as_secs_f32()); - +fn calculate_ubo(camera_info: CameraInfo, swapchain_extent: vk::Extent2D) -> UniformBufferObject { let view = Mat4::look_at_rh(camera_info.camera_pos, camera_info.camera_target, Vec3::Y); let mut proj = Mat4::perspective_rh( @@ -1201,7 +1430,7 @@ fn calculate_ubo( proj.y_axis.y *= -1.0; - UniformBufferObject { model, view, proj } + UniformBufferObject { view, proj } } // --- Drop Implementation --- diff --git a/crates/resource_manager/Cargo.toml b/crates/resource_manager/Cargo.toml index f71fc5d..bde4cc1 100644 --- a/crates/resource_manager/Cargo.toml +++ b/crates/resource_manager/Cargo.toml @@ -8,5 +8,7 @@ ash.workspace = true gpu-allocator.workspace = true thiserror.workspace = true tracing.workspace = true +gltf.workspace = true gfx_hal = { path = "../gfx_hal" } +image = { version = "0.25.6", features = ["rayon"] } diff --git a/crates/resource_manager/src/error.rs b/crates/resource_manager/src/error.rs index 669427d..96906d6 100644 --- a/crates/resource_manager/src/error.rs +++ b/crates/resource_manager/src/error.rs @@ -34,6 +34,12 @@ pub enum ResourceManagerError { #[error("Error occurred in GfxHal: {0}")] GfxHalError(#[from] gfx_hal::error::GfxHalError), + #[error("I/O Error occurred: {0}")] + Io(#[from] std::io::Error), + + #[error("Image Error occurred: {0}")] + ImageError(#[from] image::ImageError), + #[error("An unexpected error occurred: {0}")] Other(String), } diff --git a/crates/resource_manager/src/geo.rs b/crates/resource_manager/src/geo.rs index 3af2e71..c78c467 100644 --- a/crates/resource_manager/src/geo.rs +++ b/crates/resource_manager/src/geo.rs @@ -13,6 +13,7 @@ unsafe fn as_byte_slice(data: &[T]) -> &[u8] { /// Represents geometry data (verticies and indicies) stored in GPU buffers managed by /// ResourceManager. Handles automatic cleanup via a `Drop` implementation. +#[derive(Clone)] pub struct Geometry { resource_manager: Arc, pub vertex_buffer: BufferHandle, diff --git a/crates/resource_manager/src/lib.rs b/crates/resource_manager/src/lib.rs index 518fc12..4a3ec7a 100644 --- a/crates/resource_manager/src/lib.rs +++ b/crates/resource_manager/src/lib.rs @@ -1,9 +1,12 @@ mod error; mod geo; +mod texture; use std::{ collections::HashMap, + fs, hash::Hash, + path::{Path, PathBuf}, sync::{ atomic::{AtomicU64, Ordering}, Arc, Mutex, @@ -16,6 +19,7 @@ use tracing::{debug, error, trace, warn}; pub use error::{ResourceManagerError, Result}; pub use geo::Geometry; +pub use texture::{Material, SamplerDesc, Texture}; use gpu_allocator::{ vulkan::{Allocation, AllocationCreateDesc, Allocator, AllocatorCreateDesc}, @@ -28,6 +32,12 @@ pub struct BufferHandle(u64); #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ImageHandle(u64); +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] // New Handle +pub struct ImageViewHandle(u64); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct SamplerHandle(u64); + #[derive(Debug, Clone)] pub struct BufferInfo { pub handle: BufferHandle, @@ -40,14 +50,21 @@ pub struct BufferInfo { #[derive(Debug, Clone)] pub struct ImageInfo { pub handle: ImageHandle, - /// Non-owning handle. pub image: vk::Image, - /// Non-owning handle. - pub view: vk::ImageView, + // pub view: vk::ImageView, // Removed raw view + pub default_view_handle: Option, // Added handle to default view pub format: vk::Format, pub extent: vk::Extent3D, pub usage: vk::ImageUsageFlags, pub layout: vk::ImageLayout, + pub mapped_ptr: Option<*mut u8>, +} + +#[derive(Debug, Clone)] +pub struct SamplerInfo { + pub handle: SamplerHandle, + pub sampler: vk::Sampler, + pub desc: SamplerDesc, // Include desc if useful } struct InternalBufferInfo { @@ -82,11 +99,12 @@ impl Drop for InternalBufferInfo { } struct InternalImageInfo { - device: Arc, // Keep device alive for Drop - allocator: Arc>, // Needed for Drop + device: Arc, + allocator: Arc>, image: vk::Image, - view: vk::ImageView, - allocation: Option, // Option because it's taken in Drop + // view: vk::ImageView, // Removed raw view + default_view_handle: Option, // Added handle + allocation: Option, format: vk::Format, extent: vk::Extent3D, usage: vk::ImageUsageFlags, @@ -97,10 +115,7 @@ struct InternalImageInfo { impl Drop for InternalImageInfo { fn drop(&mut self) { trace!("Dropping InternalImageInfo for handle {:?}", self.handle); - // Destroy view first - unsafe { - self.device.raw().destroy_image_view(self.view, None); - } + // Then free memory if let Some(allocation) = self.allocation.take() { let mut allocator = self.allocator.lock().expect("to acquire mutex lock"); @@ -124,6 +139,51 @@ impl Drop for InternalImageInfo { } } +struct InternalSamplerInfo { + device: Arc, + sampler: vk::Sampler, + handle: SamplerHandle, + desc: SamplerDesc, +} + +struct InternalImageViewInfo { + device: Arc, // Keep device alive for Drop + view: vk::ImageView, + handle: ImageViewHandle, + // Optional: Store the ImageHandle this view belongs to for debugging/validation + // image_handle: ImageHandle, +} + +#[derive(Debug, Clone)] +pub struct ImageViewInfo { + pub handle: ImageViewHandle, + pub view: vk::ImageView, + // Could add format, subresource range etc. if needed frequently +} + +impl Drop for InternalImageViewInfo { + fn drop(&mut self) { + trace!( + "Dropping InternalImageViewInfo for handle {:?}", + self.handle + ); + unsafe { + self.device.raw().destroy_image_view(self.view, None); + } + trace!("Destroyed vk::ImageView for handle {:?}", self.handle); + } +} + +impl Drop for InternalSamplerInfo { + fn drop(&mut self) { + trace!("Dropping InternalSamplerInfo for handle {:?}", self.handle); + unsafe { + self.device.raw().destroy_sampler(self.sampler, None); + } + trace!("Destroyed vk::Sampler for handle {:?}", self.handle); + } +} + struct TransferSetup { command_pool: vk::CommandPool, queue: Arc, @@ -136,6 +196,10 @@ pub struct ResourceManager { allocator: Arc>, buffers: Arc>>, images: Arc>>, + image_views: Arc>>, + samplers: Arc>>, + sampler_cache_by_desc: Arc>>, + texture_cache_uri: Arc>>>, next_id: AtomicU64, transfer_setup: Arc>, } @@ -182,6 +246,10 @@ impl ResourceManager { allocator: Arc::new(Mutex::new(allocator)), buffers: Arc::new(Mutex::new(HashMap::new())), images: Arc::new(Mutex::new(HashMap::new())), + image_views: Arc::new(Mutex::new(HashMap::new())), // Initialize view map + samplers: Arc::new(Mutex::new(HashMap::new())), + sampler_cache_by_desc: Arc::new(Mutex::new(HashMap::new())), + texture_cache_uri: Arc::new(Mutex::new(HashMap::new())), next_id: AtomicU64::new(1), transfer_setup: Arc::new(Mutex::new(new_setup)), }) @@ -306,11 +374,11 @@ impl ResourceManager { Ok(handle) } - /// Creates a buffer, allocates memory, and uploads initial data using a staging buffer. + // Make sure create_buffer_init is correct and doesn't call itself pub fn create_buffer_init( &self, - usage: vk::BufferUsageFlags, // Usage for the *final* buffer - location: MemoryLocation, // Memory location for the *final* buffer + usage: vk::BufferUsageFlags, + location: MemoryLocation, data: &[u8], ) -> Result { let size = data.len() as vk::DeviceSize; @@ -324,14 +392,16 @@ impl ResourceManager { size, usage, location ); - // 1. Create Staging Buffer - let staging_usage = vk::BufferUsageFlags::TRANSFER_SRC; - let staging_location = MemoryLocation::CpuToGpu; // Mapped memory for upload - let staging_handle = self.create_buffer(size, staging_usage, staging_location)?; + // 1. Create Staging Buffer (CPU accessible) + let staging_handle = self.create_buffer( + // Call create_buffer, NOT create_buffer_init + size, + vk::BufferUsageFlags::TRANSFER_SRC, + MemoryLocation::CpuToGpu, + )?; // 2. Map & Copy data to staging buffer { - // Scope for buffer info and mapping pointer let staging_info = self.get_buffer_info(staging_handle)?; let mapping = staging_info .mapped_ptr @@ -339,42 +409,45 @@ impl ResourceManager { unsafe { std::ptr::copy_nonoverlapping(data.as_ptr(), mapping, data.len()); } - // If memory is not HOST_COHERENT, need to flush here: - // let mem_range = vk::MappedMemoryRange::builder().memory(...).offset(...).size(size); - // unsafe { self.device.raw().flush_mapped_memory_ranges(&[mem_range])? }; - trace!("Data copied to staging buffer."); - } // staging_info goes out of scope + // Optional: Flush + trace!("Data copied to staging buffer handle {:?}", staging_handle); + } // 3. Create Destination Buffer - let final_usage = usage | vk::BufferUsageFlags::TRANSFER_DST; // Add transfer dest usage - let dest_handle = self.create_buffer(size, final_usage, location)?; + let final_usage = usage | vk::BufferUsageFlags::TRANSFER_DST; + let dest_handle = self.create_buffer(size, final_usage, location)?; // Call create_buffer - // 4. Record and submit transfer command - let transfer_setup = self.transfer_setup.lock()?; - let dest_info = self.get_buffer_info(dest_handle)?; // Get info for vk::Buffer handle - let staging_info_for_copy = self.get_buffer_info(staging_handle)?; // Get info again + // 4. Perform Copy via Command Buffer + { + let transfer_setup_locked = self.transfer_setup.lock()?; + let buffers_locked = self.buffers.lock()?; - trace!("Submitting buffer copy command..."); - unsafe { - self.submit_commands_and_wait(&transfer_setup, |cmd| { - let region = vk::BufferCopy::default() - .src_offset(0) - .dst_offset(0) - .size(size); - self.device.raw().cmd_copy_buffer( - cmd, - staging_info_for_copy.buffer, // Use raw handle from info struct - dest_info.buffer, // Use raw handle from info struct - &[region], - ); - Ok(()) // Return Ok inside the closure - })?; + let dest_internal = buffers_locked + .get(&dest_handle.0) + .ok_or(ResourceManagerError::HandleNotFound(dest_handle.0))?; + let staging_internal = buffers_locked + .get(&staging_handle.0) + .ok_or(ResourceManagerError::HandleNotFound(staging_handle.0))?; + + trace!("Submitting buffer copy command..."); + unsafe { + Self::submit_commands_and_wait(self, &transfer_setup_locked, |cmd| { + let region = vk::BufferCopy::default().size(size); + self.device.raw().cmd_copy_buffer( + cmd, + staging_internal.buffer, + dest_internal.buffer, + &[region], + ); + Ok(()) + })?; + } + trace!("Buffer copy command finished."); } - trace!("Buffer copy command finished."); // 5. Cleanup staging buffer - self.destroy_buffer(staging_handle)?; // This frees memory and destroys buffer - debug!("Staging buffer destroyed."); + self.destroy_buffer(staging_handle)?; + debug!("Staging buffer destroyed: handle={:?}", staging_handle); Ok(dest_handle) } @@ -397,9 +470,7 @@ impl ResourceManager { ); let image = unsafe { self.device.raw().create_image(create_info, None)? }; - let requirements = unsafe { self.device.raw().get_image_memory_requirements(image) }; - let allocation = self.allocator.lock()?.allocate(&AllocationCreateDesc { name: &format!( "image_fmt_{:?}_usage_{:?}", @@ -410,7 +481,6 @@ impl ResourceManager { linear: create_info.tiling == vk::ImageTiling::LINEAR, allocation_scheme: gpu_allocator::vulkan::AllocationScheme::GpuAllocatorManaged, })?; - unsafe { self.device .raw() @@ -418,22 +488,17 @@ impl ResourceManager { } trace!("Image memory bound."); - // Create a default image view - // TODO: Make view creation more flexible (allow different subresource ranges, types) - let view_info = vk::ImageViewCreateInfo::default() - .image(image) - .view_type(vk::ImageViewType::TYPE_2D) // Assuming 2D, adjust based on create_info - .format(create_info.format) - .subresource_range(vk::ImageSubresourceRange { - aspect_mask: aspect_flags, - base_mip_level: 0, - level_count: create_info.mip_levels, - base_array_layer: 0, - layer_count: create_info.array_layers, - }); - let view = unsafe { self.device.raw().create_image_view(&view_info, None)? }; - trace!("Default image view created."); + // 2. Create the *default* Image View using the new method + let default_view_create_info = Self::build_default_view_info(create_info, aspect_flags); + // Use internal helper to avoid locking images map again if called from create_image_view + let default_view_handle = + self.create_image_view_internal(image, &default_view_create_info)?; + trace!( + "Default image view created: handle={:?}", + default_view_handle + ); + // 3. Store InternalImageInfo let id = self.next_id.fetch_add(1, Ordering::Relaxed); let handle = ImageHandle(id); @@ -441,12 +506,12 @@ impl ResourceManager { device: self.device.clone(), allocator: self.allocator.clone(), image, - view, + default_view_handle: Some(default_view_handle), // Store handle allocation: Some(allocation), format: create_info.format, extent: create_info.extent, usage: create_info.usage, - layout: create_info.initial_layout, // Store initial layout + layout: create_info.initial_layout, handle, }; @@ -455,7 +520,658 @@ impl ResourceManager { Ok(handle) } - // TODO: Implement create_image_init (similar to create_buffer_init but uses vkCmdCopyBufferToImage and layout transitions) + fn build_default_view_info<'a>( + image_create_info: &vk::ImageCreateInfo, + aspect_flags: vk::ImageAspectFlags, + ) -> vk::ImageViewCreateInfo<'a> { + let view_type = match image_create_info.image_type { + vk::ImageType::TYPE_1D => { + if image_create_info.array_layers > 1 { + vk::ImageViewType::TYPE_1D_ARRAY + } else { + vk::ImageViewType::TYPE_1D + } + } + vk::ImageType::TYPE_3D => vk::ImageViewType::TYPE_3D, + _ => { + // TYPE_2D + if image_create_info + .flags + .contains(vk::ImageCreateFlags::CUBE_COMPATIBLE) + { + if image_create_info.array_layers > 6 { + vk::ImageViewType::CUBE_ARRAY + } else { + vk::ImageViewType::CUBE + } // Assumes 6 layers + } else if image_create_info.array_layers > 1 { + vk::ImageViewType::TYPE_2D_ARRAY + } else { + vk::ImageViewType::TYPE_2D + } + } + }; + + vk::ImageViewCreateInfo::default() + // .image(image) // Image is set by create_image_view_internal + .view_type(view_type) + .format(image_create_info.format) + .subresource_range( + vk::ImageSubresourceRange::default() + .aspect_mask(aspect_flags) + .base_mip_level(0) + .level_count(image_create_info.mip_levels) + .base_array_layer(0) + .layer_count(image_create_info.array_layers), + ) + // .components(...) // Default components usually fine + } + + /// Creates a new Vulkan ImageView for an existing Image. + /// The view's lifetime is managed by the ResourceManager. + pub fn create_image_view( + &self, + image_handle: ImageHandle, + view_create_info: &vk::ImageViewCreateInfo, // User provides desired view settings + ) -> Result { + trace!("Request to create image view for image {:?}", image_handle); + // 1. Get the vk::Image handle from the InternalImageInfo + let image_vk_handle = { + // Scope for images lock + let images_map = self.images.lock()?; + let internal_image_info = images_map + .get(&image_handle.0) + .ok_or(ResourceManagerError::HandleNotFound(image_handle.0))?; + internal_image_info.image // Copy the vk::Image handle + }; // Release images lock + + // 2. Call internal helper to create the view and manage it + self.create_image_view_internal(image_vk_handle, view_create_info) + } + + /// Internal helper to create and register an image view. + /// Takes the raw vk::Image to avoid re-locking the images map. + fn create_image_view_internal( + &self, + image: vk::Image, // The actual Vulkan image handle + view_create_info: &vk::ImageViewCreateInfo, + ) -> Result { + // Ensure the create info points to the correct image + let final_view_info = (*view_create_info).image(image); + + let view = unsafe { + self.device + .raw() + .create_image_view(&final_view_info, None)? + }; + trace!("vk::ImageView created."); + + let id = self.next_id.fetch_add(1, Ordering::Relaxed); + let handle = ImageViewHandle(id); + + let internal_view_info = InternalImageViewInfo { + device: self.device.clone(), + view, + handle, + // image_handle: image_handle, // Optional: Store originating image handle + }; + + // Add to the image_views map + self.image_views.lock()?.insert(id, internal_view_info); + debug!("Image view registered: handle={:?}", handle); + + Ok(handle) + } + + /// Creates an image, uploads data from a buffer, and transitions layout. + pub fn create_image_init( + &self, + create_info: &vk::ImageCreateInfo, // Must have usage TRANSFER_DST + location: MemoryLocation, // Usually GpuOnly for textures + aspect_flags: vk::ImageAspectFlags, + data: &[u8], + ) -> Result { + if data.is_empty() { + return Err(ResourceManagerError::Other( + "Cannot create image with empty data".to_string(), + )); + } + if !create_info + .usage + .contains(vk::ImageUsageFlags::TRANSFER_DST) + { + return Err(ResourceManagerError::Other( + "Image create info must include TRANSFER_DST usage for init".to_string(), + )); + } + // It's okay if initialLayout is not UNDEFINED, we override it for the creation step + // but the user might have specified it for other reasons. We just ensure the + // internal creation uses UNDEFINED. + // if create_info.initial_layout != vk::ImageLayout::UNDEFINED { + // warn!( + // "create_image_init expects initial_layout UNDEFINED, overriding." + // ); + // } + + let data_size = data.len() as vk::DeviceSize; + debug!( + "Creating image with init data: size={}, format={:?}, extent={:?}", + data_size, create_info.format, create_info.extent + ); + + // --- Corrected Flow --- + + // 1. Create Staging Buffer (CPU accessible for copy) + // Use create_buffer directly, then map and copy. + let staging_handle = self.create_buffer( + data_size, + vk::BufferUsageFlags::TRANSFER_SRC, + MemoryLocation::CpuToGpu, // Mapped memory for upload + )?; + // Map & Copy data to staging buffer + { + // Scope for buffer info and mapping pointer + let staging_info = self.get_buffer_info(staging_handle)?; // Lock buffers map + let mapping = staging_info + .mapped_ptr + .ok_or(ResourceManagerError::MappingFailed)?; + unsafe { + std::ptr::copy_nonoverlapping(data.as_ptr(), mapping, data.len()); + } + // Optional: Flush if memory is not HOST_COHERENT + // Check allocation properties if needed and flush + trace!("Data copied to staging buffer handle {:?}", staging_handle); + } // staging_info goes out of scope, unlocks buffers map + + // 2. Create Destination Image (ensure layout is UNDEFINED initially) + let mut final_create_info = *create_info; + final_create_info.initial_layout = vk::ImageLayout::UNDEFINED; // MUST start as undefined for transition + let image_handle = self.create_image(&final_create_info, location, aspect_flags)?; + + // 3. Perform Layout Transition (Undefined -> TransferDst) & Copy + { + let transfer_setup_locked = self.transfer_setup.lock()?; + let buffers_locked = self.buffers.lock()?; + let mut images_locked = self.images.lock()?; // Mut needed to update layout + + let image_internal = images_locked + .get_mut(&image_handle.0) // Get mut ref + .ok_or(ResourceManagerError::HandleNotFound(image_handle.0))?; + // Get the *correct* staging buffer info + let staging_internal = buffers_locked + .get(&staging_handle.0) // Use the handle created in step 1 + .ok_or(ResourceManagerError::HandleNotFound(staging_handle.0))?; + + let subresource_layers = vk::ImageSubresourceLayers::default() + .aspect_mask(aspect_flags) + .mip_level(0) + .base_array_layer(0) + .layer_count(create_info.array_layers); + + let copy_region = vk::BufferImageCopy::default() + .buffer_offset(0) + .buffer_row_length(0) // 0 means tightly packed + .buffer_image_height(0) // 0 means tightly packed + .image_subresource(subresource_layers) + .image_offset(vk::Offset3D::default()) + .image_extent(create_info.extent); + + trace!("Submitting image transition and copy command..."); + unsafe { + Self::submit_commands_and_wait(self, &transfer_setup_locked, |cmd| { + // Barrier 1: Undefined -> TransferDstOptimal + let (src_access, dst_access, src_stage, dst_stage) = Self::get_barrier_params( + vk::ImageLayout::UNDEFINED, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + ); + let barrier1 = vk::ImageMemoryBarrier::default() + .old_layout(vk::ImageLayout::UNDEFINED) + .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(image_internal.image) + .subresource_range( + vk::ImageSubresourceRange::default() + .aspect_mask(aspect_flags) + .base_mip_level(0) + .level_count(create_info.mip_levels) + .base_array_layer(0) + .layer_count(create_info.array_layers), + ) + .src_access_mask(src_access) + .dst_access_mask(dst_access); + + self.device.raw().cmd_pipeline_barrier( + cmd, + src_stage, + dst_stage, + vk::DependencyFlags::empty(), + &[], + &[], + &[barrier1], + ); + + // Copy Command (using correct staging buffer) + self.device.raw().cmd_copy_buffer_to_image( + cmd, + staging_internal.buffer, // Use buffer from staging_handle + image_internal.image, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, // Layout during copy + &[copy_region], + ); + + // Barrier 2: TransferDstOptimal -> ShaderReadOnlyOptimal + let final_layout = vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL; + let (src_access, dst_access, src_stage, dst_stage) = Self::get_barrier_params( + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + final_layout, + ); + let barrier2 = vk::ImageMemoryBarrier::default() + .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL) + .new_layout(final_layout) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(image_internal.image) + .subresource_range( + vk::ImageSubresourceRange::default() + .aspect_mask(aspect_flags) + .base_mip_level(0) + .level_count(create_info.mip_levels) + .base_array_layer(0) + .layer_count(create_info.array_layers), + ) + .src_access_mask(src_access) + .dst_access_mask(dst_access); + + self.device.raw().cmd_pipeline_barrier( + cmd, + src_stage, + dst_stage, + vk::DependencyFlags::empty(), + &[], + &[], + &[barrier2], + ); + + Ok(()) // Return Ok from the closure + })?; // End submit_commands_and_wait + } // Locks released (transfer_setup, buffers, images) + + drop(images_locked); + + let mut images_locked_update = self.images.lock()?; // Use different name to avoid confusion + if let Some(info) = images_locked_update.get_mut(&image_handle.0) { + info.layout = vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL; // Update to final layout + trace!( + "Image {:?} layout updated internally to {:?}", + image_handle, + info.layout + ); + } else { + warn!( + "Image {:?} disappeared after creation during init!", + image_handle + ); + } + } // Scope for locks ends here + + // 4. Cleanup staging buffer (the one created in step 1) + self.destroy_buffer(staging_handle)?; // Destroy the correct handle + debug!( + "Staging buffer destroyed for image init: handle={:?}", + staging_handle + ); // Log handle + + tracing::info!( + "Image created and initialized successfully: handle={:?}", + image_handle + ); + Ok(image_handle) + } + + /// Gets non-owning information about a sampler. + pub fn get_sampler_info(&self, handle: SamplerHandle) -> Result { + let samplers_map = self + .samplers + .lock() + .map_err(|_| ResourceManagerError::Other("Sampler map mutex poisoned".to_string()))?; + samplers_map + .get(&handle.0) + .map(|internal| SamplerInfo { + handle: internal.handle, + sampler: internal.sampler, + desc: internal.desc.clone(), + }) + .ok_or(ResourceManagerError::HandleNotFound(handle.0)) + } + + /// Gets or creates a Vulkan sampler based on the description. + /// Uses caching to avoid creating duplicate samplers. + pub fn get_or_create_sampler(&self, desc: &SamplerDesc) -> Result { + let mut cache = self.sampler_cache_by_desc.lock()?; + if let Some(handle) = cache.get(desc) { + if self.samplers.lock()?.contains_key(&handle.0) { + trace!("Using cached sampler for desc: {:?}", desc); + return Ok(*handle); + } else { + warn!( + "Sampler handle {:?} found in cache but not main map. Removing from cache.", + handle + ); + cache.remove(desc); + } + } + + drop(cache); + + trace!("Creating a new sampler for desc: {:?}", desc); + let sampler_info = vk::SamplerCreateInfo::default() + .mag_filter(desc.mag_filter) + .min_filter(desc.min_filter) + .mipmap_mode(desc.mipmap_mode) + .address_mode_u(desc.address_mode_u) + .address_mode_v(desc.address_mode_v) + .address_mode_w(desc.address_mode_w) + .mip_lod_bias(0.0) + .anisotropy_enable(false) // TODO: Expose anisotropy in SamplerDesc? + .max_anisotropy(1.0) + .compare_enable(false) // TODO: Expose compare op? + .compare_op(vk::CompareOp::ALWAYS) + .min_lod(0.0) + .max_lod(vk::LOD_CLAMP_NONE) // TODO: Allow setting max LOD (e.g., for mipmapping) + .border_color(vk::BorderColor::INT_OPAQUE_BLACK) // TODO: Expose border color? + .unnormalized_coordinates(false); + + let vk_sampler = unsafe { self.device.raw().create_sampler(&sampler_info, None)? }; + + let id = self.next_id.fetch_add(1, Ordering::Relaxed); + let handle = SamplerHandle(id); + + let internal_info = InternalSamplerInfo { + device: self.device.clone(), + sampler: vk_sampler, + handle, + desc: desc.clone(), + }; + + self.samplers.lock()?.insert(id, internal_info); + self.sampler_cache_by_desc + .lock()? + .insert(desc.clone(), handle); + + debug!("Sampler created successfully: handle={:?}", handle); + Ok(handle) + } + + pub fn destroy_sampler(&self, handle: SamplerHandle) -> Result<()> { + debug!("Requesting destroy for sampler handle {:?}", handle); + let mut samplers_map = self.samplers.lock()?; + if let Some(internal_info) = samplers_map.remove(&handle.0) { + self.sampler_cache_by_desc + .lock()? + .remove(&internal_info.desc); + debug!("Sampler handle {:?} removed for destruction.", handle); + Ok(()) + } else { + warn!( + "Attempted to destroy non-existent sampler handle {:?}", + handle + ); + Err(ResourceManagerError::HandleNotFound(handle.0)) + } + } + + /// Loads a texture from a file path or embedded data using glTF info. + /// Handles image decoding, resource creation, upload and caching. + pub fn load_texture( + &self, + gltf_image: &gltf::Image, + gltf_source: &gltf::image::Source, + base_path: &Path, + buffers: &[gltf::buffer::Data], + usage: vk::ImageUsageFlags, + ) -> Result> { + let cache_key_path: Option = match gltf_source { + gltf::image::Source::View { view, mime_type } => todo!(), + gltf::image::Source::Uri { uri, mime_type } => { + let image_path = base_path.join(uri); + match fs::canonicalize(&image_path) { + Ok(canon_path) => Some(canon_path), + Err(e) => { + warn!( + "Failed to canonicalize image path {:?}: {} Skipping cache lookup", + image_path, e + ); + None + } + } + } + }; + + if let Some(ref path_key) = cache_key_path { + let uri_cache = self.texture_cache_uri.lock()?; + if let Some(cached_texture) = uri_cache.get(path_key) { + if self.images.lock()?.contains_key(&cached_texture.handle.0) { + trace!("Using cached texture (URI): {:?}", path_key); + return Ok(cached_texture.clone()); + } else { + warn!( + "Texture hadnle {:?} found in URI cache but not in main map. Will reload.", + cached_texture.handle + ); + } + } + } + + let (image_data, _format_hint) = match gltf_source { + gltf::image::Source::Uri { uri, mime_type } => { + let image_path = base_path.join(uri); + debug!("Loading texture from URI: {:?}", image_path); + let bytes = fs::read(&image_path).map_err(|e| { + error!("Failed to read image file {:?}: {}", image_path, e); + ResourceManagerError::Io(e) + })?; + (bytes, mime_type.map(|s| s.to_string())) + } + gltf::image::Source::View { view, mime_type } => { + debug!( + "Loading texture from buffer view: index={}, offset={}, length={}", + view.buffer().index(), + view.offset(), + view.length() + ); + let buffer_data = &buffers[view.buffer().index()]; + let start = view.offset(); + let end = start + view.length(); + if end > buffer_data.len() { + return Err(ResourceManagerError::Other(format!( + "Buffer view out of bounds for image: view index {}, buffer index {}", + view.index(), + view.buffer().index() + ))); + } + let bytes: Vec = buffer_data[start..end].to_vec(); // Clone data + (bytes, Some(mime_type.to_string())) // mime_type is required for View + } + }; + + let img = image::load_from_memory(&image_data)?; + + let rgba_image = img.to_rgba8(); + let (width, height) = rgba_image.dimensions(); + let raw_pixels = rgba_image.into_raw(); + + let vk_format = vk::Format::R8G8B8A8_SRGB; + + let extent = vk::Extent3D { + width, + height, + depth: 1, + }; + let image_create_info = vk::ImageCreateInfo::default() + .image_type(vk::ImageType::TYPE_2D) + .format(vk_format) + .extent(extent) + .mip_levels(1) // TODO: Add mipmap generation + .array_layers(1) + .samples(vk::SampleCountFlags::TYPE_1) + .tiling(vk::ImageTiling::OPTIMAL) // Textures should be optimal + .usage(usage | vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED) // Ensure needed usages + .sharing_mode(vk::SharingMode::EXCLUSIVE) + .initial_layout(vk::ImageLayout::UNDEFINED); // create_image_init handles transition + + let image_handle = self.create_image_init( + &image_create_info, + MemoryLocation::GpuOnly, // Textures usually live on GPU + vk::ImageAspectFlags::COLOR, + &raw_pixels, + )?; + + let app_texture = Arc::new(Texture { + handle: image_handle, + format: vk_format, + extent, + }); + + if let Some(path_key) = cache_key_path { + self.texture_cache_uri + .lock()? + .insert(path_key, app_texture.clone()); + trace!("Texture added to URI cache."); + } + + Ok(app_texture) + } + + /// Transitions the layout of an image using a command buffer. + /// Updates the internal layout state. + pub fn transition_image_layout( + &self, + image_handle: ImageHandle, + new_layout: vk::ImageLayout, + ) -> Result<()> { + let transfer_setup_locked = self.transfer_setup.lock()?; + let mut images_locked = self.images.lock()?; + + let internal_info = images_locked + .get_mut(&image_handle.0) + .ok_or(ResourceManagerError::HandleNotFound(image_handle.0))?; + + let old_layout = internal_info.layout; + if old_layout == new_layout { + trace!( + "Image {:?} already in layout {:?}. Skipping transition.", + image_handle, + new_layout + ); + return Ok(()); + } + + trace!( + "Transitioning image {:?} layout {:?} -> {:?}", + image_handle, + old_layout, + new_layout + ); + + let aspect_mask = if new_layout == vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL + || old_layout == vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL + { + vk::ImageAspectFlags::DEPTH + } else { + vk::ImageAspectFlags::COLOR + }; + + let subresource_range = vk::ImageSubresourceRange::default() + .aspect_mask(aspect_mask) + .base_mip_level(0) + .level_count(1) + .base_array_layer(0) + .layer_count(1); + + unsafe { + Self::submit_commands_and_wait(self, &transfer_setup_locked, |cmd| { + let (src_access_mask, dst_access_mask, src_stage, dst_stage) = + Self::get_barrier_params(old_layout, 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) + .image(internal_info.image) + .subresource_range(subresource_range) + .src_access_mask(src_access_mask) + .dst_access_mask(dst_access_mask); + + self.device.raw().cmd_pipeline_barrier( + cmd, + src_stage, + dst_stage, + vk::DependencyFlags::empty(), + &[], + &[], + &[barrier], + ); + Ok(()) + })?; + } + + internal_info.layout = new_layout; + debug!( + "Image {:?} layout transitioned to {:?} 🏳️‍⚧️", + image_handle, new_layout + ); + Ok(()) + } + + fn get_barrier_params( + old_layout: vk::ImageLayout, + new_layout: vk::ImageLayout, + ) -> ( + vk::AccessFlags, + vk::AccessFlags, + vk::PipelineStageFlags, + vk::PipelineStageFlags, + ) { + let src_access_mask; + let dst_access_mask; + let src_stage; + let dst_stage; + + match (old_layout, new_layout) { + (vk::ImageLayout::UNDEFINED, vk::ImageLayout::TRANSFER_DST_OPTIMAL) => { + src_access_mask = vk::AccessFlags::empty(); + dst_access_mask = vk::AccessFlags::TRANSFER_WRITE; + src_stage = vk::PipelineStageFlags::TOP_OF_PIPE; + dst_stage = vk::PipelineStageFlags::TRANSFER; + } + (vk::ImageLayout::TRANSFER_DST_OPTIMAL, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) => { + src_access_mask = vk::AccessFlags::TRANSFER_WRITE; + dst_access_mask = vk::AccessFlags::SHADER_READ; + src_stage = vk::PipelineStageFlags::TRANSFER; + dst_stage = vk::PipelineStageFlags::FRAGMENT_SHADER; + } + (vk::ImageLayout::UNDEFINED, vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) => { + src_access_mask = vk::AccessFlags::empty(); + dst_access_mask = vk::AccessFlags::SHADER_READ; + src_stage = vk::PipelineStageFlags::TOP_OF_PIPE; + dst_stage = vk::PipelineStageFlags::FRAGMENT_SHADER; + } + _ => { + warn!( + "Unsupported layout trasnition: {:?} -> {:?}", + old_layout, new_layout + ); + src_access_mask = vk::AccessFlags::MEMORY_WRITE; + dst_access_mask = vk::AccessFlags::MEMORY_READ; + src_stage = vk::PipelineStageFlags::ALL_COMMANDS; + dst_stage = vk::PipelineStageFlags::ALL_COMMANDS; + } + } + + (src_access_mask, dst_access_mask, src_stage, dst_stage) + } /// Destroys a buffer and frees its memory. pub fn destroy_buffer(&self, handle: BufferHandle) -> Result<()> { @@ -513,23 +1229,47 @@ impl ResourceManager { .ok_or(ResourceManagerError::HandleNotFound(handle.0)) } - /// Gets non-owning information about an image. pub fn get_image_info(&self, handle: ImageHandle) -> Result { - let images_map = self.images.lock()?; + let images_map = self + .images + .lock() + .map_err(|_| ResourceManagerError::Other("Image map mutex poisoned".to_string()))?; images_map .get(&handle.0) - .map(|internal| ImageInfo { - handle: internal.handle, - image: internal.image, - view: internal.view, - format: internal.format, - extent: internal.extent, - usage: internal.usage, - layout: internal.layout, // Note: Layout tracking is basic here + .map(|internal| { + let mapped_ptr = internal + .allocation + .as_ref() + .and_then(|a| a.mapped_ptr().map(|p| p.as_ptr() as *mut u8)); + + ImageInfo { + handle: internal.handle, + image: internal.image, + default_view_handle: internal.default_view_handle, // Return handle + format: internal.format, + extent: internal.extent, + usage: internal.usage, + layout: internal.layout, + mapped_ptr, + } }) .ok_or(ResourceManagerError::HandleNotFound(handle.0)) } + /// Gets non-owning information about a specific image view. + pub fn get_image_view_info(&self, handle: ImageViewHandle) -> Result { + let views_map = self.image_views.lock().map_err(|_| { + ResourceManagerError::Other("Image view map mutex poisoned".to_string()) + })?; + views_map + .get(&handle.0) + .map(|internal| ImageViewInfo { + handle: internal.handle, + view: internal.view, + }) + .ok_or(ResourceManagerError::HandleNotFound(handle.0)) // Use handle.0 + } + /// Explicitly waits for the device to be idle. Useful before shutdown. pub fn wait_device_idle(&self) -> Result<(), ResourceManagerError> { self.device @@ -541,41 +1281,66 @@ impl ResourceManager { impl Drop for ResourceManager { fn drop(&mut self) { debug!("Destroying ResourceManager..."); - // Ensure all GPU operations are finished before freeing memory/destroying resources - if let Err(e) = self.device.wait_idle() { + // Wait for idle BEFORE locking/clearing maps + if let Err(e) = unsafe { self.device.raw().device_wait_idle() } { error!( "Failed to wait for device idle during ResourceManager drop: {}", e ); - // Proceeding with cleanup, but resources might still be in use! + // Proceeding, but cleanup might be unsafe } - // Clear resource maps. This triggers the Drop impl for each Internal*Info, - // which frees allocations and destroys Vulkan objects. - let mut buffers_map = self.buffers.lock().expect("mutex to not be poisoned"); - debug!("Clearing {} buffer entries...", buffers_map.len()); - buffers_map.clear(); - let mut images_map = self.images.lock().expect("mutex to not be poisoned"); - debug!("Clearing {} image entries...", images_map.len()); - images_map.clear(); - - let setup = self - .transfer_setup - .lock() - .expect("mutex to not be poisoned"); - - debug!("Destroying TransferSetup resources..."); - unsafe { - self.device - .raw() - .destroy_command_pool(setup.command_pool, None); + // Clear resource maps. This triggers the Drop impl for each Internal*Info. + if let Ok(mut buffers_map) = self.buffers.lock() { + debug!("Clearing {} buffer entries...", buffers_map.len()); + buffers_map.clear(); + } else { + error!("Buffer map mutex poisoned during drop."); } - debug!("TransferSetup resources destroyed."); - // The Allocator is wrapped in an Arc>, so its Drop will be handled - // when the last Arc reference (including those held by Internal*Info) is dropped. - // gpu-allocator's Allocator Drop implementation should be empty, as memory - // is freed via allocator.free(). + if let Ok(mut images_map) = self.images.lock() { + debug!("Clearing {} image entries...", images_map.len()); + images_map.clear(); + } else { + error!("Image map mutex poisoned during drop."); + } + + if let Ok(mut samplers_map) = self.samplers.lock() { + debug!("Clearing {} sampler entries...", samplers_map.len()); + samplers_map.clear(); + } else { + error!("Sampler map mutex poisoned during drop."); + } + + // Clear caches (Arc drops will happen naturally) + if let Ok(mut sampler_cache) = self.sampler_cache_by_desc.lock() { + sampler_cache.clear(); + } else { + error!("Sampler cache mutex poisoned during drop."); + } + if let Ok(mut texture_cache) = self.texture_cache_uri.lock() { + texture_cache.clear(); + } else { + error!("Texture URI cache mutex poisoned during drop."); + } + // Clear other caches... + + // Destroy transfer setup resources + if let Ok(setup) = self.transfer_setup.lock() { + debug!("Destroying TransferSetup resources..."); + unsafe { + self.device + .raw() + .destroy_command_pool(setup.command_pool, None); + } + debug!("TransferSetup resources destroyed."); + } else { + error!("TransferSetup mutex poisoned during drop."); + } + + // Allocator Drop: gpu-allocator's Allocator doesn't do anything in drop. + // Memory is freed via allocator.free() called by Internal*Info drops. + // The Arc> will be dropped when the last reference goes away. debug!("ResourceManager destroyed."); } diff --git a/crates/resource_manager/src/texture.rs b/crates/resource_manager/src/texture.rs new file mode 100644 index 0000000..857e0f5 --- /dev/null +++ b/crates/resource_manager/src/texture.rs @@ -0,0 +1,59 @@ +use std::sync::Arc; + +use ash::vk; + +use crate::{ImageHandle, SamplerHandle}; + +#[derive(Debug, Clone)] +pub struct Texture { + pub handle: ImageHandle, + + pub format: vk::Format, + pub extent: vk::Extent3D, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct SamplerDesc { + pub mag_filter: vk::Filter, + pub min_filter: vk::Filter, + pub mipmap_mode: vk::SamplerMipmapMode, + pub address_mode_u: vk::SamplerAddressMode, + pub address_mode_v: vk::SamplerAddressMode, + pub address_mode_w: vk::SamplerAddressMode, +} + +impl Default for SamplerDesc { + fn default() -> Self { + Self { + 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, + } + } +} + +#[derive(Debug, Clone)] +pub struct Material { + pub name: String, + pub base_color_texture: Option>, + pub base_color_sampler: Option, + pub base_color_factor: [f32; 4], + pub metallic_factor: f32, + pub roughness_factor: f32, + // TODO: Add other PBR properties: + // pub metallic_roughness_texture: Option>, + // pub metallic_roughness_sampler: Option, + // pub normal_texture: Option>, + // pub normal_sampler: Option, + // pub occlusion_texture: Option>, + // pub occlusion_sampler: Option, + // pub emissive_texture: Option>, + // pub emissive_sampler: Option, + // pub emissive_factor: [f32; 3], + // pub alpha_mode: gltf::material::AlphaMode, + // pub alpha_cutoff: f32, + // pub double_sided: bool, +} diff --git a/crates/scene/Cargo.toml b/crates/scene/Cargo.toml new file mode 100644 index 0000000..6a67b50 --- /dev/null +++ b/crates/scene/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "scene" +version = "0.1.0" +edition = "2021" + +[dependencies] +ash.workspace = true +thiserror.workspace = true +tracing.workspace = true +glam.workspace = true +gltf.workspace = true + +shared = { path = "../shared" } +resource_manager = { path = "../resource_manager" } diff --git a/crates/scene/src/error.rs b/crates/scene/src/error.rs new file mode 100644 index 0000000..b052c9c --- /dev/null +++ b/crates/scene/src/error.rs @@ -0,0 +1,16 @@ +use thiserror::Error; + +/// Any errors that can be returned from this crate. +#[derive(Error, Debug)] +pub enum SceneError { + #[error("Error from ResourceManager: {0}")] + ResourceManagerError(#[from] resource_manager::ResourceManagerError), + + #[error("Error from GLTF: {0}")] + GltfError(#[from] gltf::Error), + + #[error("InconsistentData: {0}")] + InconsistentData(String), +} + +pub type Result = std::result::Result; diff --git a/crates/scene/src/lib.rs b/crates/scene/src/lib.rs new file mode 100644 index 0000000..fd672ac --- /dev/null +++ b/crates/scene/src/lib.rs @@ -0,0 +1,411 @@ +mod error; + +use ash::vk; +pub use error::{Result, SceneError}; +use glam::Mat4; +use shared::Vertex; + +use std::{collections::HashMap, path::Path, sync::Arc}; + +use resource_manager::{Geometry, Material, ResourceManager, SamplerDesc, SamplerHandle, Texture}; + +/// Represents a drawable entity in the scene, storing geometry with its transform. +#[derive(Clone)] +pub struct Mesh { + pub name: String, + pub material: Arc, + pub geometry: Arc, + pub transform: Mat4, +} + +/// Stores all objects to be rendered by the renderer. +pub struct Scene { + pub name: String, + pub meshes: Vec, +} + +fn sampler_desc_from_gltf(g_sampler: &gltf::texture::Sampler) -> SamplerDesc { + let wrap_s = g_sampler.wrap_s(); + let wrap_t = g_sampler.wrap_t(); + + SamplerDesc { + mag_filter: g_sampler + .mag_filter() + .map_or(vk::Filter::LINEAR, |mf| match mf { + gltf::texture::MagFilter::Nearest => vk::Filter::NEAREST, + gltf::texture::MagFilter::Linear => vk::Filter::LINEAR, + }), + min_filter: g_sampler + .min_filter() + .map_or(vk::Filter::LINEAR, |mf| match mf { + gltf::texture::MinFilter::Nearest + | gltf::texture::MinFilter::NearestMipmapNearest + | gltf::texture::MinFilter::NearestMipmapLinear => vk::Filter::NEAREST, + gltf::texture::MinFilter::Linear + | gltf::texture::MinFilter::LinearMipmapNearest + | gltf::texture::MinFilter::LinearMipmapLinear => vk::Filter::LINEAR, + }), + mipmap_mode: g_sampler + .min_filter() + .map_or(vk::SamplerMipmapMode::LINEAR, |mf| match mf { + gltf::texture::MinFilter::NearestMipmapNearest + | gltf::texture::MinFilter::LinearMipmapNearest => vk::SamplerMipmapMode::NEAREST, + gltf::texture::MinFilter::NearestMipmapLinear + | gltf::texture::MinFilter::LinearMipmapLinear => vk::SamplerMipmapMode::LINEAR, + _ => vk::SamplerMipmapMode::LINEAR, // Default if no mipmapping + }), + address_mode_u: vk_address_mode(wrap_s), + address_mode_v: vk_address_mode(wrap_t), + address_mode_w: vk::SamplerAddressMode::REPEAT, // glTF doesn't define wrapR + } +} + +fn vk_address_mode(g_mode: gltf::texture::WrappingMode) -> vk::SamplerAddressMode { + match g_mode { + gltf::texture::WrappingMode::ClampToEdge => vk::SamplerAddressMode::CLAMP_TO_EDGE, + gltf::texture::WrappingMode::MirroredRepeat => vk::SamplerAddressMode::MIRRORED_REPEAT, + gltf::texture::WrappingMode::Repeat => vk::SamplerAddressMode::REPEAT, + } +} + +impl Scene { + /// Takes a glTF file and returns a `Scene`. + pub fn from_gltf(path: T, resource_manager: Arc) -> Result + where + T: AsRef, + { + let path_ref = path.as_ref(); + let base_path = path_ref.parent().unwrap_or_else(|| Path::new("")); + tracing::info!("Loading glTF from: {:?}", path_ref); + tracing::info!("Base path for resources: {:?}", base_path); + + // Import images as well + let (doc, buffers, images) = gltf::import(path_ref)?; + tracing::info!( + "glTF Stats: {} scenes, {} nodes, {} meshes, {} materials, {} textures, {} images", + doc.scenes().len(), + doc.nodes().len(), + doc.meshes().len(), + doc.materials().len(), + doc.textures().len(), + doc.images().len() + ); + + let mut meshes = Vec::new(); + // Cache Geometry: Key = (mesh_index, primitive_index) + let mut geometry_cache: HashMap<(usize, usize), Arc> = HashMap::new(); + // Cache Materials: Key = glTF material index (usize::MAX for default) + let mut material_cache: HashMap> = HashMap::new(); + // Cache default sampler handle to avoid repeated lookups + let default_sampler_handle = + resource_manager.get_or_create_sampler(&SamplerDesc::default())?; + + let scene_to_load = doc + .default_scene() + .unwrap_or_else(|| doc.scenes().next().expect("No scenes found in glTF")); + + let scene_name = scene_to_load + .name() + .unwrap_or("") + .to_string(); + tracing::info!( + "Processing scene '{}' ({})", + scene_name, + scene_to_load.index() + ); + + // Create a context struct to pass around common data + let mut load_ctx = LoadContext { + doc: &doc, + buffers: &buffers, + images: &images, + base_path, + resource_manager, + geometry_cache: &mut geometry_cache, + material_cache: &mut material_cache, + default_sampler_handle, + meshes: &mut meshes, + }; + + for node in scene_to_load.nodes() { + Self::process_node(&node, &Mat4::IDENTITY, &mut load_ctx)?; + } + + tracing::info!("Successfully loaded {} render meshes.", meshes.len()); + + Ok(Self { + name: scene_name, + meshes, + }) + } + + /// Recursively processes a glTF node. + fn process_node( + node: &gltf::Node, + parent_transform: &Mat4, + ctx: &mut LoadContext, // Pass context mutably for caches + ) -> Result<()> { + let local_transform = Mat4::from_cols_array_2d(&node.transform().matrix()); + let world_transform = *parent_transform * local_transform; + let node_name = node.name().unwrap_or(""); + + if let Some(mesh) = node.mesh() { + let mesh_index = mesh.index(); + let mesh_name = mesh.name().unwrap_or(""); + tracing::debug!( + "Node '{}' ({}) has Mesh '{}' ({})", + node_name, + node.index(), + mesh_name, + mesh_index + ); + + // Process mesh primitives + for (primitive_index, primitive) in mesh.primitives().enumerate() { + // Generate a name for the Mesh object + let primitive_name = format!("{}_prim{}", mesh_name, primitive_index); + + Self::process_primitive( + &primitive, + mesh_index, + primitive_index, + &primitive_name, // Pass name + world_transform, + ctx, // Pass context + )?; + } + } else { + tracing::trace!("Node '{}' ({}) has no mesh.", node_name, node.index()); + } + + // Recursively process child nodes + for child_node in node.children() { + Self::process_node( + &child_node, + &world_transform, // Pass current world transform + ctx, // Pass context + )?; + } + + Ok(()) + } + + /// Processes a single glTF primitive, creating Geometry, Material, and Mesh. + fn process_primitive( + primitive: &gltf::Primitive, + mesh_index: usize, + primitive_index: usize, + mesh_name: &str, // Name for the final Mesh object + world_transform: Mat4, + ctx: &mut LoadContext, // Use context + ) -> Result<()> { + let geometry_cache_key = (mesh_index, primitive_index); + + // --- Get or Create Geometry --- + let geometry = if let Some(cached_geo) = ctx.geometry_cache.get(&geometry_cache_key) { + tracing::trace!("Using cached Geometry for key {:?}", geometry_cache_key); + cached_geo.clone() + } else { + tracing::trace!("Creating new Geometry for key {:?}", geometry_cache_key); + let reader = primitive.reader(|buffer| Some(&ctx.buffers[buffer.index()])); + + let Some(pos_iter) = reader.read_positions() else { + tracing::warn!( + "Primitive {:?} missing positions. Skipping.", + geometry_cache_key + ); + return Ok(()); // Skip this primitive + }; + let positions: Vec<[f32; 3]> = pos_iter.collect(); + let vertex_count = positions.len(); + + if vertex_count == 0 { + tracing::warn!( + "Primitive {:?} has no vertices. Skipping.", + geometry_cache_key + ); + return Ok(()); + } + + let normals: Vec<[f32; 3]> = reader + .read_normals() + .map(|iter| iter.collect()) + .unwrap_or_else(|| { + tracing::debug!( + "Primitive {:?} missing normals, using default.", + geometry_cache_key + ); + vec![[0.0, 1.0, 0.0]; vertex_count] + }); + + // Read Texture Coordinates (Set 0) - needed for vertex struct regardless of material + let tex_coords: Vec<[f32; 2]> = reader + .read_tex_coords(0) // Read UV set 0 + .map(|iter| iter.into_f32().collect()) + .unwrap_or_else(|| { + tracing::trace!( + "Primitive {:?} missing tex_coords (set 0), using default.", + geometry_cache_key + ); + vec![[0.0, 0.0]; vertex_count] + }); + + if normals.len() != vertex_count || tex_coords.len() != vertex_count { + return Err(SceneError::InconsistentData(format!( + "Attribute count mismatch for Primitive {:?} (Pos: {}, Norm: {}, TexCoord0: {}).", + geometry_cache_key, vertex_count, normals.len(), tex_coords.len() + ))); + } + + let vertices: Vec = positions + .into_iter() + .zip(normals) + .zip(tex_coords) + .map(|((pos, normal), tex_coord)| Vertex { + pos, + normal, + tex_coord, + }) + .collect(); + + let indices: Vec = reader + .read_indices() + .map(|read_indices| read_indices.into_u32().collect()) + .unwrap_or_else(|| (0..vertex_count as u32).collect()); + + if indices.is_empty() && vertex_count > 0 { + tracing::warn!( + "Primitive {:?} has vertices but no indices. Skipping.", + geometry_cache_key + ); + return Ok(()); + } + + let new_geo = Arc::new(Geometry::new( + ctx.resource_manager.clone(), + &vertices, + &indices, + )?); + ctx.geometry_cache + .insert(geometry_cache_key, new_geo.clone()); + new_geo + }; + + // --- Get or Create Material --- + let g_material = primitive.material(); + // Use index usize::MAX as key for default material if index() is None + let material_cache_key = g_material.index().unwrap_or(usize::MAX); + let material_name = g_material.name().unwrap_or(""); + + let material = if let Some(cached_mat) = ctx.material_cache.get(&material_cache_key) { + tracing::trace!( + "Using cached Material index {} ('{}')", + material_cache_key, + material_name + ); + cached_mat.clone() + } else { + tracing::trace!( + "Creating new Material for index {} ('{}')", + material_cache_key, + material_name + ); + let pbr = g_material.pbr_metallic_roughness(); + + let base_color_factor = pbr.base_color_factor(); + let metallic_factor = pbr.metallic_factor(); + let roughness_factor = pbr.roughness_factor(); + + let mut loaded_base_color_texture: Option> = None; + let mut loaded_base_color_sampler: Option = None; + + // --- Load Base Color Texture (if it exists) --- + if let Some(color_info) = pbr.base_color_texture() { + let tex_coord_set = color_info.tex_coord(); + if tex_coord_set != 0 { + tracing::warn!( + "Material '{}' requests tex_coord set {}, but only set 0 is currently loaded. Texture ignored.", + material_name, tex_coord_set + ); + // Fall through, texture won't be loaded + } else { + let g_texture = color_info.texture(); + let g_sampler = g_texture.sampler(); + let g_image = g_texture.source(); // This is the gltf::Image + + tracing::debug!( + "Material '{}' uses Texture index {}, Sampler index {:?}, Image index {}", + material_name, + g_texture.index(), + g_sampler.index(), + g_image.index() + ); + + // Get or create sampler + let sampler_desc = sampler_desc_from_gltf(&g_sampler); + let sampler_handle = + ctx.resource_manager.get_or_create_sampler(&sampler_desc)?; + loaded_base_color_sampler = Some(sampler_handle); + + // Load texture image data via ResourceManager + // Pass the correct gltf::Image using its index + let texture = ctx.resource_manager.load_texture( + &ctx.doc + .images() + .nth(g_image.index()) + .expect("Image index out of bounds"), // Get gltf::Image + &g_image.source(), // Get gltf::image::Source + ctx.base_path, + ctx.buffers, + vk::ImageUsageFlags::SAMPLED, // Standard usage for textures + )?; + loaded_base_color_texture = Some(texture); // Store Arc + } + } + + // Assign default sampler if none was loaded via texture info + if loaded_base_color_sampler.is_none() { + loaded_base_color_sampler = Some(ctx.default_sampler_handle); + tracing::trace!("Material '{}' using default sampler.", material_name); + } + + // Create the application Material struct + let new_mat = Arc::new(Material { + name: material_name.to_string(), + base_color_texture: loaded_base_color_texture, + base_color_sampler: loaded_base_color_sampler, + base_color_factor, + metallic_factor, + roughness_factor, + // Initialize other material properties here... + }); + + ctx.material_cache + .insert(material_cache_key, new_mat.clone()); + new_mat + }; + + // Create the final Mesh object + ctx.meshes.push(Mesh { + name: mesh_name.to_string(), + geometry, + material, // Assign the Arc + transform: world_transform, + }); + + Ok(()) + } +} + +// Context struct to avoid passing too many arguments +struct LoadContext<'a> { + doc: &'a gltf::Document, + buffers: &'a [gltf::buffer::Data], + images: &'a [gltf::image::Data], // Keep image data accessible if needed by RM + base_path: &'a Path, + resource_manager: Arc, + geometry_cache: &'a mut HashMap<(usize, usize), Arc>, + material_cache: &'a mut HashMap>, + default_sampler_handle: SamplerHandle, // Store the default sampler + meshes: &'a mut Vec, // Store results directly +} diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index b83634a..120f579 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -4,11 +4,14 @@ use glam::{Mat4, Vec3}; use core::f32; use std::mem::size_of; +mod material; + #[repr(C)] #[derive(Clone, Debug, Copy)] pub struct Vertex { pub pos: [f32; 3], - pub color: [f32; 3], + pub normal: [f32; 3], + pub tex_coord: [f32; 2], } impl Vertex { @@ -19,7 +22,7 @@ impl Vertex { .input_rate(vk::VertexInputRate::VERTEX) } - pub fn get_attribute_descriptions() -> [vk::VertexInputAttributeDescription; 2] { + pub fn get_attribute_descriptions() -> [vk::VertexInputAttributeDescription; 3] { [ vk::VertexInputAttributeDescription::default() .location(0) @@ -30,7 +33,12 @@ impl Vertex { .location(1) .binding(0) .format(vk::Format::R32G32B32_SFLOAT) - .offset(memoffset::offset_of!(Vertex, color) as u32), + .offset(memoffset::offset_of!(Vertex, normal) as u32), + vk::VertexInputAttributeDescription::default() + .location(2) + .binding(0) + .format(vk::Format::R32G32_SFLOAT) + .offset(memoffset::offset_of!(Vertex, tex_coord) as u32), ] } } @@ -38,7 +46,6 @@ impl Vertex { #[repr(C)] #[derive(Clone, Debug, Copy, PartialEq)] pub struct UniformBufferObject { - pub model: Mat4, pub view: Mat4, pub proj: Mat4, } @@ -47,6 +54,7 @@ pub struct UniformBufferObject { pub struct CameraInfo { pub camera_pos: Vec3, pub camera_target: Vec3, + pub camera_up: Vec3, pub camera_fov: f32, } @@ -55,6 +63,7 @@ impl Default for CameraInfo { Self { camera_pos: Vec3::new(10.0, 10.0, 10.0), camera_target: Vec3::new(0.0, 0.0, 0.0), + camera_up: Vec3::Y, camera_fov: 45.0, } } diff --git a/crates/shared/src/material.rs b/crates/shared/src/material.rs new file mode 100644 index 0000000..e69de29 diff --git a/flake.lock b/flake.lock index a74e166..f777db0 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1733312601, - "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "lastModified": 1743550720, + "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "rev": "c621e8422220273271f52058f618c94e405bb0f5", "type": "github" }, "original": { @@ -51,23 +51,26 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1733096140, - "narHash": "sha256-1qRH7uAUsyQI7R1Uwl4T+XvdNv778H0Nb5njNrqvylY=", - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz" + "lastModified": 1743296961, + "narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa", + "type": "github" }, "original": { - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz" + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" } }, "nixpkgs_2": { "locked": { - "lastModified": 1728538411, - "narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=", + "lastModified": 1736320768, + "narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221", + "rev": "4bc9c909d9ac828a039f288cf872d16d38185db8", "type": "github" }, "original": { @@ -79,11 +82,11 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1733097829, - "narHash": "sha256-9hbb1rqGelllb4kVUCZ307G2k3/UhmA8PPGBoyuWaSw=", + "lastModified": 1735554305, + "narHash": "sha256-zExSA1i/b+1NMRhGGLtNfFGXgLtgo+dcuzHzaWA6w3Q=", "owner": "nixos", "repo": "nixpkgs", - "rev": "2c15aa59df0017ca140d9ba302412298ab4bf22a", + "rev": "0e82ab234249d8eee3e8c91437802b32c74bb3fd", "type": "github" }, "original": { @@ -108,11 +111,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1735266518, - "narHash": "sha256-2XkWYGgT+911gOLjgBj+8W8ZJk6P0qHJNz8RfKgT/5o=", + "lastModified": 1743820323, + "narHash": "sha256-UXxJogXhPhBFaX4uxmMudcD/x3sEGFtoSc4busTcftY=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "e0b3654b716098b47f3643c65fbb75ef49c033e1", + "rev": "b4734ce867252f92cdc7d25f8cc3b7cef153e703", "type": "github" }, "original": { @@ -141,11 +144,11 @@ "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1735135567, - "narHash": "sha256-8T3K5amndEavxnludPyfj3Z1IkcFdRpR23q+T0BVeZE=", + "lastModified": 1743748085, + "narHash": "sha256-uhjnlaVTWo5iD3LXics1rp9gaKgDRQj6660+gbUU3cE=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "9e09d30a644c57257715902efbb3adc56c79cf28", + "rev": "815e4121d6a5d504c0f96e5be2dd7f871e4fd99d", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 71450ae..3991877 100644 --- a/flake.nix +++ b/flake.nix @@ -35,11 +35,12 @@ commonArgs, ... }: { - _module.args = { + _module.args = rec { pkgs = import nixpkgs { inherit system; overlays = [inputs.rust-overlay.overlays.default]; }; + craneLib = (inputs.crane.mkLib pkgs).overrideToolchain ( pkgs: pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml ); diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 551cee7..30f36be 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,6 +1,11 @@ [toolchain] channel = "nightly-2024-11-22" -components = ["rust-src", "rustc-dev", "llvm-tools"] +components = [ + "rust-src", + "rustc-dev", + "llvm-tools", + "rustc-codegen-cranelift-preview", +] # commit_hash = b19329a37cedf2027517ae22c87cf201f93d776e # Whenever changing the nightly channel, update the commit hash above, and make diff --git a/shaders/frag.glsl.frag b/shaders/frag.glsl.frag index 5943263..3e46886 100644 --- a/shaders/frag.glsl.frag +++ b/shaders/frag.glsl.frag @@ -1,16 +1,34 @@ + #version 450 // Input from vertex shader -layout(location = 0) in vec3 fragColor; -// layout(location = 1) in vec2 fragTexCoord; // If using textures +layout(location = 0) in vec3 fragNormal; // Receive normal +layout(location = 1) in vec2 fragTexCoord; // Receive texture coordinates // Output color layout(location = 0) out vec4 outColor; -// layout(binding = 1) uniform sampler2D texSampler; // If using textures +// Descriptor set for material properties (Set 1) +layout(set = 1, binding = 0) uniform sampler2D baseColorSampler; + +// Optional: Pass material factors via another UBO or Push Constants if needed +// layout(set = 1, binding = 1) uniform MaterialFactors { +// vec4 baseColorFactor; +// } materialFactors; void main() { - // Use interpolated color - outColor = vec4(fragColor, 1.0); - // outColor = texture(texSampler, fragTexCoord); // If using textures + // Sample the texture + vec4 texColor = texture(baseColorSampler, fragTexCoord); + + // Use the texture color + // You might multiply by baseColorFactor here if you pass it + // outColor = texColor * materialFactors.baseColorFactor; + outColor = texColor; + + // Basic fallback if texture alpha is zero (or use baseColorFactor) + if (outColor.a == 0.0) { + outColor = vec4(0.8, 0.8, 0.8, 1.0); // Default grey + } + + // You could add basic lighting using fragNormal here later } diff --git a/shaders/vert.glsl.vert b/shaders/vert.glsl.vert index 73bc36c..a2053d6 100644 --- a/shaders/vert.glsl.vert +++ b/shaders/vert.glsl.vert @@ -1,26 +1,38 @@ #version 450 -// Matches Vertex struct attribute descriptions +// INPUTS from Vertex Buffer (matching Vertex struct) layout(location = 0) in vec3 inPosition; -layout(location = 1) in vec3 inColor; -// layout(location = 2) in vec2 inTexCoord; // If you add texture coords +layout(location = 1) in vec3 inNormal; +layout(location = 2) in vec2 inTexCoord; // <<< MUST be vec2 -// Matches UniformBufferObject struct and descriptor set layout binding -layout(binding = 0) uniform UniformBufferObject { - mat4 model; +// UNIFORMS (Set 0) +layout(set = 0, binding = 0) uniform UniformBufferObject { mat4 view; mat4 proj; } ubo; -// Output to fragment shader -layout(location = 0) out vec3 fragColor; -// layout(location = 1) out vec2 fragTexCoord; // If you add texture coords +// PUSH CONSTANTS +layout(push_constant) uniform PushConstants { + mat4 model; +} pushConstants; + +// OUTPUTS to Fragment Shader +layout(location = 0) out vec3 fragNormal; // Location 0 for Normal +layout(location = 1) out vec2 fragTexCoord; // Location 1 for TexCoord void main() { - // Transform position: Model -> World -> View -> Clip space - gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0); + vec4 worldPos = pushConstants.model * vec4(inPosition, 1.0); - // Pass color (and other attributes) through - fragColor = inColor; - // fragTexCoord = inTexCoord; + // Calculate final position + gl_Position = ubo.proj * ubo.view * worldPos; + + // --- Pass attributes to Fragment Shader --- + + // Pass world-space normal (adjust calculation if needed) + // Ensure fragNormal is assigned a vec3 + fragNormal = normalize(mat3(transpose(inverse(pushConstants.model))) * inNormal); + + // Pass texture coordinates (ensure inTexCoord is vec2) + // Ensure fragTexCoord is assigned a vec2 + fragTexCoord = inTexCoord; }