diff options
88 files changed, 9111 insertions, 4828 deletions
@@ -1,27 +1,51 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aho-corasick" -version = "1.1.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bindgen" @@ -29,7 +53,7 @@ version = "0.68.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.9.0", "cexpr", "clang-sys", "lazy_static", @@ -38,7 +62,27 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", + "shlex", + "syn", +] + +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags 2.9.0", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.1", "shlex", "syn", ] @@ -51,15 +95,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "bytemuck" -version = "1.14.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" [[package]] name = "byteorder" @@ -68,6 +112,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] name = "cexpr" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -83,10 +133,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] name = "clang-sys" -version = "1.6.1" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", @@ -94,6 +171,31 @@ dependencies = [ ] [[package]] +name = "clap" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] name = "color_quant" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -101,24 +203,68 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] name = "ecs" version = "0.1.0" dependencies = [ + "criterion", "ecs-macros", + "hashbrown", "linkme", + "parking_lot", "paste", "seq-macro", "thiserror", "tracing", "util-macros", + "vizoxide", ] [[package]] @@ -132,10 +278,16 @@ dependencies = [ ] [[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] name = "engine" version = "0.1.0" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.9.0", "ecs", "gl", "glfw", @@ -144,34 +296,41 @@ dependencies = [ "seq-macro", "thiserror", "tracing", + "util-macros", ] [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "fdeflate" -version = "0.3.1" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] [[package]] name = "flate2" -version = "1.0.28" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] name = "game-newest" version = "0.1.0" dependencies = [ @@ -204,8 +363,8 @@ dependencies = [ name = "glfw" version = "0.1.0" dependencies = [ - "bindgen", - "bitflags 2.4.0", + "bindgen 0.68.1", + "bitflags 2.9.0", "libc", "thiserror", "util-macros", @@ -213,46 +372,101 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "graphviz-sys" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97dbac5b269c7160b78812a9873f548668aa34b62f28f7a64328a6cd94feb47d" +dependencies = [ + "bindgen 0.71.1", +] + +[[package]] +name = "half" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" +dependencies = [ + "cfg-if", + "crunchy", +] [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hermit-abi" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" [[package]] name = "image" -version = "0.24.7" +version = "0.24.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" dependencies = [ "bytemuck", "byteorder", "color_quant", "jpeg-decoder", - "num-rational", "num-traits", "png", ] [[package]] name = "indexmap" -version = "2.2.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", "hashbrown", ] [[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] name = "jpeg-decoder" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "khronos_api" @@ -262,9 +476,9 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lazycell" @@ -274,34 +488,34 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.148" +version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libloading" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "winapi", + "windows-targets", ] [[package]] name = "linkme" -version = "0.3.29" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70fe496a7af8c406f877635cbf3cd6a9fac9d6f443f58691cd8afe6ce0971af4" +checksum = "22d227772b5999ddc0690e733f734f95ca05387e329c4084fe65678c51198ffe" dependencies = [ "linkme-impl", ] [[package]] name = "linkme-impl" -version = "0.3.29" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b01f197a15988fb5b2ec0a5a9800c97e70771499c456ad757d63b3c5e9b96e75" +checksum = "71a98813fa0073a317ed6a8055dcd4722a49d9b862af828ee68449adb799b6be" dependencies = [ "proc-macro2", "quote", @@ -309,16 +523,35 @@ dependencies = [ ] [[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] name = "log" -version = "0.4.20" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "minimal-lexical" @@ -328,11 +561,11 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" dependencies = [ - "adler", + "adler2", "simd-adler32", ] @@ -357,40 +590,25 @@ dependencies = [ ] [[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "num-integer", - "num-traits", ] [[package]] -name = "num-traits" -version = "0.2.17" +name = "once_cell" +version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" -dependencies = [ - "autocfg", -] +checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" [[package]] -name = "once_cell" -version = "1.18.0" +name = "oorandom" +version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "overload" @@ -399,10 +617,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "peeking_take_while" @@ -412,15 +653,15 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "png" -version = "0.17.10" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -430,51 +671,85 @@ dependencies = [ ] [[package]] +name = "prettyplease" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] name = "regex" -version = "1.9.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] name = "regex-automata" -version = "0.3.9" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] [[package]] name = "regex-syntax" -version = "0.7.5" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustc-hash" @@ -483,25 +758,52 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] name = "seq-macro" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -509,10 +811,22 @@ dependencies = [ ] [[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -528,9 +842,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simd-adler32" @@ -540,15 +854,15 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "smallvec" -version = "1.11.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" [[package]] name = "syn" -version = "2.0.51" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab617d94515e94ae53b8406c628598680aa0c9587474ecbe58188f7b345d66c" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -557,18 +871,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.49" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.49" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", @@ -577,19 +891,29 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", ] [[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] name = "toml" -version = "0.8.12" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" dependencies = [ "serde", "serde_spanned", @@ -599,18 +923,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.9" +version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", "serde", @@ -621,9 +945,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.39" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2ef2af84856a50c1d430afce2fdded0a4ec7eda868db86409b4543df0797f9" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -632,9 +956,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", @@ -643,44 +967,35 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" -dependencies = [ - "lazy_static", - "log", - "tracing-core", ] [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", - "tracing-log", ] [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "util-macros" @@ -692,10 +1007,24 @@ dependencies = [ ] [[package]] -name = "valuable" -version = "0.1.0" +name = "vizoxide" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec190047a5e02a6423f147cf40890be1d37bbc68b9eba441689acbcd255f942" +dependencies = [ + "base64", + "graphviz-sys", +] + +[[package]] +name = "walkdir" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] [[package]] name = "winapi" @@ -714,22 +1043,104 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] name = "winnow" -version = "0.6.6" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" dependencies = [ "memchr", ] [[package]] name = "xml-rs" -version = "0.8.19" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" +checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" @@ -7,6 +7,10 @@ edition = "2021" members = ["glfw", "engine", "ecs", "ecs-macros", "util-macros"] [dependencies] -engine = { path = "./engine", features = ["debug"] } -tracing = "0.1.39" -tracing-subscriber = "0.3.17" +engine = { path = "./engine" } +tracing = { version = "0.1.39", features = ["max_level_debug"] } + +[dependencies.tracing-subscriber] +version = "0.3.17" +default-features = false +features = ["std", "ansi", "fmt", "smallvec", "env-filter"] @@ -1,3 +1,27 @@ +- [ ] Multiple rendering passes + - [ ] Effects using stencil buffer +- [ ] Remove possible edge cases in ECS component storage + - [ ] A Query<()> yields all components. Should this be the behaviour? +- [ ] Improve ECS component storage performance + - [x] Give archetypes edges for faster component addition & removal + - [ ] Store components of the same kind in the same memory allocation (not boxed) +- [ ] Investigate what happends when a entity has all of it's components removed. +- [ ] Audio +- [ ] Transparent/translucent models +- [ ] Animations +- [ ] Physics +- [ ] Rotation (using quaternions) +- [ ] Add support for entity tags in ECS framework +- [ ] Make OpenGL resource (texture, VBO, VAO) structs not implement Drop. This will + probably make some parts of the code cleaner +- [ ] Add implementation of GJK algorithm for collision detection +- [ ] Add a way to not have to check every collidable object for collision. Something + like binary space partitioning would work +- [ ] Figure out way to handle collision of small fast moving objects +- [ ] Improve vertex code. +- [ ] Add support for optionally rendering objects without a projection +- [ ] Add shadow mapping +- [x] Use Blinn-phong lighting instead of phong lighting - [x] Support for multiple textures - [x] Non-hardcoded projection settings - [x] Model importing @@ -9,17 +33,11 @@ - [x] Add support for entity relationships in ECS framework - [x] Add component events (removal & addition) in ECS framework - [x] Make renderer not create new buffers each frame -- [ ] Multiple rendering passes - - [ ] Effects using stencil buffer - [x] Remove position field from Camera component -- [ ] Remove possible edge cases in ECS component storage +- [x] Add support for entities with no components - [x] Fix OpenGL warning "Vertex shader in program 3 is being recompiled based on GL state". It started at commit 526edc6f4cb5f29d17e2fe384e316236c033fccd. Fixed by c4686c2992417545e7a05a6a40ee9f1a8bbf3b96 -- [ ] Audio -- [ ] Transparent/translucent models -- [ ] Animations -- [ ] Physics -- [ ] Rotation (using quaternions) -- [ ] Add support for entity tags in ECS framework - +- [x] New texture IDs are created for no reason. Crashes when on texture ID 31 +- [x] engine::file_format:wavefront::obj::Obj::to_mesh seems to produce meshes with + pointless vertex indices since it flat maps all face vertices diff --git a/ecs-macros/src/lib.rs b/ecs-macros/src/lib.rs index 8bef0b6..7d00736 100644 --- a/ecs-macros/src/lib.rs +++ b/ecs-macros/src/lib.rs @@ -1,3 +1,4 @@ +#![deny(clippy::all, clippy::pedantic)] use std::path::PathBuf as FsPathBuf; use proc_macro::TokenStream; @@ -6,7 +7,6 @@ use syn::spanned::Spanned; use syn::{ parse, Attribute, - GenericParam, Generics, Ident, Item, @@ -14,7 +14,6 @@ use syn::{ ItemStruct, ItemUnion, Path, - Type, }; use toml::value::{Table as TomlTable, Value as TomlValue}; @@ -42,60 +41,35 @@ macro_rules! syn_path_segment { }; } -#[proc_macro_derive(Component, attributes(component))] +/// Generates a `Component` implementation. +/// +/// # Panics +/// Will panic if: +/// - Not attributed to a type item +/// - The attributed-to type item is generic +/// - If parsing the user crate's `Cargo.toml` file fails. +#[proc_macro_derive(Component)] pub fn component_derive(input: TokenStream) -> TokenStream { let item: TypeItem = parse::<Item>(input).unwrap().try_into().unwrap(); - let ComponentAttribute { ref_type, ref_mut_type } = item - .attribute::<ComponentAttribute>("component") - .unwrap_or_default(); - let item_ident = item.ident(); let (impl_generics, type_generics, where_clause) = item.generics().split_for_impl(); let ecs_path = find_engine_ecs_crate_path().unwrap_or_else(|| syn_path!(ecs)); - let (id_or_ids, get_id) = if !item.generics().params.is_empty() { - let id_lut_ident = - format_ident!("{}_ID_LUT", item_ident.to_string().to_uppercase()); + assert!( + item.generics().params.is_empty(), + "Generic types are not supported as components" + ); - let id_lut = quote! { - static #id_lut_ident: LazyLock<Mutex<HashMap<TypeId, Uid>>> = - LazyLock::new(|| Mutex::new(HashMap::new())); - }; + let id_var_ident = format_ident!("{}_ID", item_ident.to_string().to_uppercase()); - let generics = item.generics().params.iter().map(|param| match param { - GenericParam::Type(type_param) => type_param.ident.clone(), - GenericParam::Lifetime(_) => panic!("Lifetime generics are not supported"), - GenericParam::Const(_) => panic!("Const generics are not supported"), + let id_var = quote! { + static #id_var_ident: LazyLock<Uid> = LazyLock::new(|| { + Uid::new_unique(UidKind::Component) }); - - let get_id = quote! { - *#id_lut_ident - .try_lock() - .unwrap() - .entry(TypeId::of::<(#(#generics,)*)>()) - .or_insert_with(|| Uid::new_unique(UidKind::Component)) - }; - - (id_lut, get_id) - } else { - let id_lazylock_ident = - format_ident!("{}_ID", item_ident.to_string().to_uppercase()); - - let id_lazylock = quote! { - static #id_lazylock_ident: LazyLock<Uid> = LazyLock::new(|| { - Uid::new_unique(UidKind::Component) - }); - }; - - let get_id = quote! { - *#id_lazylock_ident - }; - - (id_lazylock, get_id) }; let mod_ident = format_ident!( @@ -107,45 +81,26 @@ pub fn component_derive(input: TokenStream) -> TokenStream mod #mod_ident { use ::std::any::{Any, TypeId}; use ::std::sync::{LazyLock, Mutex}; - use ::std::collections::HashMap; use #ecs_path::component::Component; - use #ecs_path::system::ComponentRefMut; - use #ecs_path::system::ComponentRef; use #ecs_path::uid::{Uid, Kind as UidKind}; use #ecs_path::system::Input as SystemInput; - use #ecs_path::type_name::TypeName; use super::*; - #id_or_ids + #id_var impl #impl_generics Component for #item_ident #type_generics #where_clause { - type Component = Self; - - type RefMut<'component> = #ref_mut_type; - type Ref<'component> = #ref_type; - fn id() -> Uid { - #get_id - } - - fn self_id(&self) -> Uid - { - Self::id() + *#id_var_ident } - fn as_any_mut(&mut self) -> &mut dyn Any + fn name(&self) -> &'static str { - self - } - - fn as_any(&self) -> &dyn Any - { - self + std::any::type_name::<Self>() } } @@ -153,20 +108,16 @@ pub fn component_derive(input: TokenStream) -> TokenStream #where_clause { } - - impl #impl_generics TypeName for #item_ident #type_generics - #where_clause - { - fn type_name(&self) -> &'static str - { - std::any::type_name::<Self>() - } - } } } .into() } +/// Generates a `Sole` implementation. +/// +/// # Panics +/// Will panic if not attributed to a type item or if parsing the user crate's +/// `Cargo.toml` file fails. #[proc_macro_derive(Sole, attributes(sole))] pub fn sole_derive(input: TokenStream) -> TokenStream { @@ -201,15 +152,6 @@ pub fn sole_derive(input: TokenStream) -> TokenStream self } } - - impl #impl_generics #ecs_path::type_name::TypeName for #item_ident #type_generics - #where_clause - { - fn type_name(&self) -> &'static str - { - std::any::type_name::<Self>() - } - } } .into() } @@ -248,9 +190,7 @@ impl TypeItem let mut attr: Option<&Attribute> = None; for item_attr in item_attrs { - if attr.is_some() { - panic!("Expected only one {} attribute", attr_ident); - } + assert!(attr.is_none(), "Expected only one {attr_ident} attribute"); if item_attr.path().get_ident()? == attr_ident { attr = Some(item_attr); @@ -374,61 +314,3 @@ fn find_engine_ecs_crate_path() -> Option<Path> None }) } - -#[derive(Debug)] -struct ComponentAttribute -{ - ref_type: proc_macro2::TokenStream, - ref_mut_type: proc_macro2::TokenStream, -} - -impl FromAttribute for ComponentAttribute -{ - fn from_attribute(attribute: &Attribute) -> Result<Self, syn::Error> - { - let mut ref_type: Option<Type> = None; - let mut ref_mut_type: Option<Type> = None; - - attribute.parse_nested_meta(|meta| { - let Some(flag) = meta.path.get_ident() else { - return Err(meta.error("Not a single identifier")); - }; - - if flag == "ref_type" { - let value = meta.value()?; - - ref_type = Some(value.parse::<Type>()?); - - return Ok(()); - } else if flag == "ref_mut_type" { - let value = meta.value()?; - - ref_mut_type = Some(value.parse::<Type>()?); - - return Ok(()); - } - - Err(meta.error("Unrecognized token")) - })?; - - Ok(Self { - ref_type: ref_type - .map(|ref_type| ref_type.into_token_stream()) - .unwrap_or_else(|| Self::default().ref_type), - ref_mut_type: ref_mut_type - .map(|ref_mut_type| ref_mut_type.into_token_stream()) - .unwrap_or_else(|| Self::default().ref_mut_type), - }) - } -} - -impl Default for ComponentAttribute -{ - fn default() -> Self - { - Self { - ref_type: quote! { ComponentRef<'component, Self> }, - ref_mut_type: quote! { ComponentRefMut<'component, Self> }, - } - } -} diff --git a/ecs/Cargo.toml b/ecs/Cargo.toml index 342f087..cf35a74 100644 --- a/ecs/Cargo.toml +++ b/ecs/Cargo.toml @@ -4,14 +4,25 @@ version = "0.1.0" edition = "2021" [features] -debug = ["dep:tracing"] +vizoxide = ["dep:vizoxide"] [dependencies] seq-macro = "0.3.5" paste = "1.0.14" thiserror = "1.0.49" -tracing = { version = "0.1.39", optional = true } +tracing = "0.1.39" linkme = "0.3.29" +hashbrown = "0.15.2" +parking_lot = "0.12.3" ecs-macros = { path = "../ecs-macros" } util-macros = { path = "../util-macros" } +vizoxide = { version = "1.0.5", optional = true } +[dev-dependencies.criterion] +version = "0.5.1" +default-features = false +features = ["cargo_bench_support"] + +[[bench]] +name = "query" +harness = false diff --git a/ecs/benches/query.rs b/ecs/benches/query.rs new file mode 100644 index 0000000..f14bb06 --- /dev/null +++ b/ecs/benches/query.rs @@ -0,0 +1,141 @@ +use std::hint::black_box; +use std::path::PathBuf; + +use criterion::{criterion_group, criterion_main, Criterion}; +use ecs::{Component, World}; + +#[derive(Component)] +struct Foo +{ + _text: String, +} + +#[derive(Component)] +struct Bar +{ + _path: PathBuf, + _num: u64, +} + +#[derive(Component)] +struct Position +{ + _x: f32, + _y: f32, + _z: f32, +} + +#[derive(Component)] +struct PosA +{ + _x: f32, + _y: f32, + _z: f32, +} + +#[derive(Component)] +struct PosB +{ + _x: f32, + _y: f32, + _z: f32, +} + +#[derive(Component)] +struct PosC +{ + _x: f32, + _y: f32, + _z: f32, +} + +#[derive(Component)] +struct MoreText +{ + _more_text: String, +} + +#[derive(Component)] +struct EvenMoreText +{ + _even_more_text: String, +} + +fn spawn_1000_entities(world: &mut World) +{ + for _ in 0..300 { + world.create_entity(( + Bar { + _path: "/dev/zero".into(), + _num: 65789, + }, + Position { _x: 13.98, _y: 27.0, _z: 0.2 }, + Foo { _text: "Hello there".to_string() }, + PosA { + _x: 1183.98, + _y: 272628.0, + _z: 3306.2, + }, + PosB { + _x: 171183.98, + _y: 28.0, + _z: 336.2901, + }, + PosC { _x: 8273.98, _y: 28.0, _z: 336.2901 }, + MoreText { + _more_text: "Lorem ipsum".to_string(), + }, + EvenMoreText { + _even_more_text: "Wow so much text".to_string(), + }, + )); + } + + for _ in 0..700 { + world.create_entity(( + Bar { + _path: "/dev/null".into(), + _num: 65789, + }, + Position { _x: 88.11, _y: 9.0, _z: 36.11 }, + Foo { _text: "Hey".to_string() }, + PosA { + _x: 118310.98, + _y: 272628.0, + _z: 3306.2, + }, + PosB { _x: 11323.98, _y: 28.0, _z: 336.2901 }, + PosC { + _x: 8273.98, + _y: 21818.0, + _z: 336.2901, + }, + MoreText { + _more_text: "Lorem ipsum".to_string(), + }, + EvenMoreText { + _even_more_text: "Wow much text".to_string(), + }, + )); + } +} + +fn benchbark(criterion: &mut Criterion) +{ + criterion.bench_function("Iterate 1000 entities", |bencher| { + let mut world = World::new(); + + spawn_1000_entities(&mut world); + + let query = world.query::<(&Bar, &Position, &Foo), ()>(); + + bencher.iter(|| { + for comps in query.iter() { + black_box(comps); + } + }) + }); +} + +criterion_group!(benches, benchbark); +criterion_main!(benches); diff --git a/ecs/examples/event_loop.rs b/ecs/examples/event_loop.rs index 8d8d84f..61d7ba4 100644 --- a/ecs/examples/event_loop.rs +++ b/ecs/examples/event_loop.rs @@ -1,6 +1,7 @@ use ecs::actions::Actions; -use ecs::event::Event; -use ecs::{Component, Query, World}; +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::{Phase, UPDATE as UPDATE_PHASE}; +use ecs::{static_entity, Component, Query, World}; #[derive(Component)] struct Wool @@ -20,7 +21,7 @@ struct Name name: &'static str, } -fn sheer(query: Query<(Wool, Name)>) +fn sheer(query: Query<(&mut Wool, &Name)>) { for (mut wool, name) in &query { if wool.remaining == 0 { @@ -36,7 +37,7 @@ fn sheer(query: Query<(Wool, Name)>) } } -fn feed(query: Query<(Health, Name)>) +fn feed(query: Query<(&mut Health, &Name)>) { for (mut health, name) in &query { health.health += 1; @@ -45,7 +46,7 @@ fn feed(query: Query<(Health, Name)>) } } -fn age(query: Query<(Health, Name)>, mut actions: Actions) +fn age(query: Query<(&mut Health, &Name)>, mut actions: Actions) { for (mut health, name) in &query { if health.health <= 2 { @@ -64,28 +65,19 @@ fn age(query: Query<(Health, Name)>, mut actions: Actions) } } -#[derive(Debug)] -struct EventA; +static_entity!(SHEER_PHASE, (Phase, Pair::new::<ChildOf>(*UPDATE_PHASE))); -impl Event for EventA {} +static_entity!(FEED_PHASE, (Phase, Pair::new::<ChildOf>(*SHEER_PHASE))); -#[derive(Debug)] -struct EventB; - -impl Event for EventB {} - -#[derive(Debug)] -struct EventC; - -impl Event for EventC {} +static_entity!(AGE_PHASE, (Phase, Pair::new::<ChildOf>(*FEED_PHASE))); fn main() { let mut world = World::new(); - world.register_system(EventA, sheer); - world.register_system(EventB, feed); - world.register_system(EventC, age); + world.register_system(*SHEER_PHASE, sheer); + world.register_system(*FEED_PHASE, feed); + world.register_system(*AGE_PHASE, age); world.create_entity(( Wool { remaining: 30 }, @@ -93,5 +85,5 @@ fn main() Name { name: "Bessy" }, )); - world.event_loop::<(EventA, EventB, EventC)>(); + world.start_loop(); } diff --git a/ecs/examples/extension.rs b/ecs/examples/extension.rs index 2022b05..f6282e1 100644 --- a/ecs/examples/extension.rs +++ b/ecs/examples/extension.rs @@ -1,6 +1,6 @@ use ecs::actions::Actions; -use ecs::event::Event; use ecs::extension::{Collector as ExtensionCollector, Extension}; +use ecs::phase::UPDATE as UPDATE_PHASE; use ecs::{Component, Query, World}; #[derive(Debug, Component)] @@ -19,14 +19,9 @@ enum EvilnessLevel Medium, } -#[derive(Debug)] -struct Update; - -impl Event for Update {} - fn spawn_enemies( - spawner_query: Query<(EnemySpawnSource, Position)>, - enemies_query: Query<(EvilnessLevel,)>, + spawner_query: Query<(&EnemySpawnSource, &Position)>, + enemies_query: Query<(&EvilnessLevel,)>, mut actions: Actions, ) { @@ -57,7 +52,7 @@ impl Extension for EnemySpawningExtension { fn collect(self, mut collector: ExtensionCollector<'_>) { - collector.add_system(Update, spawn_enemies); + collector.add_system(*UPDATE_PHASE, spawn_enemies); collector.add_entity((Position { x: 187, y: 30 }, EnemySpawnSource)); } @@ -70,8 +65,6 @@ fn main() world.add_extension(EnemySpawningExtension); for _ in 0..7 { - world.emit(Update); - - world.perform_queued_actions(); + world.step(); } } diff --git a/ecs/examples/multiple_queries.rs b/ecs/examples/multiple_queries.rs index 2736bce..e0c957f 100644 --- a/ecs/examples/multiple_queries.rs +++ b/ecs/examples/multiple_queries.rs @@ -1,6 +1,6 @@ use std::fmt::Display; -use ecs::event::start::Start as StartEvent; +use ecs::phase::START as START_PHASE; use ecs::{Component, Query, World}; #[derive(Component)] @@ -31,8 +31,8 @@ impl Display for EnemyName } fn do_attacks( - attacker_query: Query<(AttackStrength,)>, - enemy_query: Query<(Health, EnemyName)>, + attacker_query: Query<(&AttackStrength,)>, + enemy_query: Query<(&mut Health, &EnemyName)>, ) { for (attack_strength,) in &attacker_query { @@ -61,7 +61,7 @@ fn main() { let mut world = World::new(); - world.register_system(StartEvent, do_attacks); + world.register_system(*START_PHASE, do_attacks); world.create_entity(( Health { health: 100 }, @@ -81,5 +81,5 @@ fn main() world.create_entity((AttackStrength::Strong,)); world.create_entity((AttackStrength::Weak,)); - world.emit(StartEvent); + world.step(); } diff --git a/ecs/examples/optional_component.rs b/ecs/examples/optional_component.rs index e47bf2e..ebc9115 100644 --- a/ecs/examples/optional_component.rs +++ b/ecs/examples/optional_component.rs @@ -1,4 +1,4 @@ -use ecs::event::Event; +use ecs::phase::UPDATE as UPDATE_PHASE; use ecs::{Component, Query, World}; #[derive(Debug, Component)] @@ -21,7 +21,7 @@ pub struct CatName name: String, } -fn pet_cats(query: Query<(CatName, PettingCapacity, Option<Aggressivity>)>) +fn pet_cats(query: Query<(&CatName, &mut PettingCapacity, Option<&Aggressivity>)>) { for (cat_name, mut petting_capacity, aggressivity) in &query { let Some(aggressivity) = aggressivity else { @@ -48,16 +48,11 @@ fn pet_cats(query: Query<(CatName, PettingCapacity, Option<Aggressivity>)>) } } -#[derive(Debug)] -struct PettingTime; - -impl Event for PettingTime {} - fn main() { let mut world = World::new(); - world.register_system(PettingTime, pet_cats); + world.register_system(*UPDATE_PHASE, pet_cats); world.create_entity(( CatName { name: "Jasper".to_string() }, @@ -82,5 +77,5 @@ fn main() Aggressivity::Low, )); - world.emit(PettingTime); + world.step(); } diff --git a/ecs/examples/relationship.rs b/ecs/examples/relationship.rs index e8fb327..b607398 100644 --- a/ecs/examples/relationship.rs +++ b/ecs/examples/relationship.rs @@ -1,5 +1,6 @@ -use ecs::event::start::Start as StartEvent; -use ecs::relationship::Relationship; +use ecs::pair::Pair; +use ecs::phase::START as START_PHASE; +use ecs::uid::Wildcard; use ecs::{Component, Query, World}; #[derive(Component)] @@ -17,14 +18,19 @@ struct Health health: u32, } +#[derive(Component)] struct Holding; -fn print_player_stats(player_query: Query<(Player, Health, Relationship<Holding, Sword>)>) +fn print_player_stats(player_query: Query<(&Player, &Health, Pair<Holding, Wildcard>)>) { for (_, health, sword_relationship) in &player_query { println!("Player health: {}", health.health); - if let Some(sword) = sword_relationship.get(0) { + if let Some(sword_ent) = sword_relationship.get_target_entity() { + let sword = sword_ent + .get::<Sword>() + .expect("Sword entity is missing sword component"); + println!("Player sword attack strength: {}", sword.attack_strength); } } @@ -34,15 +40,15 @@ fn main() { let mut world = World::new(); - world.register_system(StartEvent, print_player_stats); + world.register_system(*START_PHASE, print_player_stats); let sword_uid = world.create_entity((Sword { attack_strength: 17 },)); world.create_entity(( Player, Health { health: 180 }, - Relationship::<Holding, Sword>::new(sword_uid), + Pair::new::<Holding>(sword_uid), )); - world.emit(StartEvent); + world.step(); } diff --git a/ecs/examples/simple.rs b/ecs/examples/simple.rs index 4057c84..0169062 100644 --- a/ecs/examples/simple.rs +++ b/ecs/examples/simple.rs @@ -1,4 +1,4 @@ -use ecs::event::start::Start as StartEvent; +use ecs::phase::START as START_PHASE; use ecs::{Component, Query, World}; #[derive(Component)] @@ -13,7 +13,7 @@ struct Greeting greeting: String, } -fn say_hello(query: Query<(SomeData, Greeting)>) +fn say_hello(query: Query<(&SomeData, &Greeting)>) { for (data, greeting) in &query { println!("{}: {}", greeting.greeting, data.num); @@ -24,7 +24,7 @@ fn main() { let mut world = World::new(); - world.register_system(StartEvent, say_hello); + world.register_system(*START_PHASE, say_hello); world.create_entity(( SomeData { num: 987_654 }, @@ -38,5 +38,5 @@ fn main() Greeting { greeting: "Good evening".to_string() }, )); - world.emit(StartEvent); + world.step(); } diff --git a/ecs/examples/with_local.rs b/ecs/examples/with_local.rs index 5890b90..4658fc0 100644 --- a/ecs/examples/with_local.rs +++ b/ecs/examples/with_local.rs @@ -1,5 +1,5 @@ use ecs::component::local::Local; -use ecs::event::Event; +use ecs::phase::UPDATE as UPDATE_PHASE; use ecs::system::{Into, System}; use ecs::{Component, Query, World}; @@ -21,7 +21,7 @@ struct SayHelloState cnt: usize, } -fn say_hello(query: Query<(SomeData,)>, mut state: Local<SayHelloState>) +fn say_hello(query: Query<(&SomeData,)>, mut state: Local<SayHelloState>) { for (data,) in &query { println!("Hello there. Count {}: {}", state.cnt, data.num); @@ -30,7 +30,7 @@ fn say_hello(query: Query<(SomeData,)>, mut state: Local<SayHelloState>) } } -fn say_whats_up(query: Query<(SomeData, Name)>, mut state: Local<SayHelloState>) +fn say_whats_up(query: Query<(&SomeData, &Name)>, mut state: Local<SayHelloState>) { for (data, name) in &query { println!( @@ -42,24 +42,19 @@ fn say_whats_up(query: Query<(SomeData, Name)>, mut state: Local<SayHelloState>) } } -#[derive(Debug)] -struct Update; - -impl Event for Update {} - fn main() { let mut world = World::new(); world.register_system( - Update, + *UPDATE_PHASE, say_hello .into_system() .initialize((SayHelloState { cnt: 0 },)), ); world.register_system( - Update, + *UPDATE_PHASE, say_whats_up .into_system() .initialize((SayHelloState { cnt: 0 },)), @@ -69,9 +64,6 @@ fn main() world.create_entity((SomeData { num: 345 },)); - world.emit(Update); - - println!("Haha"); - - world.emit(Update); + world.step(); + world.step(); } diff --git a/ecs/examples/with_sole.rs b/ecs/examples/with_sole.rs index a387bea..c3feaab 100644 --- a/ecs/examples/with_sole.rs +++ b/ecs/examples/with_sole.rs @@ -1,6 +1,7 @@ -use ecs::event::Event; +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::{Phase, UPDATE as UPDATE_PHASE}; use ecs::sole::Single; -use ecs::{Component, Query, Sole, World}; +use ecs::{static_entity, Component, Query, Sole, World}; #[derive(Component)] struct Ammo @@ -14,7 +15,7 @@ struct AmmoCounter counter: u32, } -fn count_ammo(query: Query<(Ammo,)>, mut ammo_counter: Single<AmmoCounter>) +fn count_ammo(query: Query<(&Ammo,)>, mut ammo_counter: Single<AmmoCounter>) { for (ammo,) in &query { println!("Found {} ammo", ammo.ammo_left); @@ -30,22 +31,17 @@ fn print_total_ammo_count(ammo_counter: Single<AmmoCounter>) assert_eq!(ammo_counter.counter, 19); } -#[derive(Debug)] -struct EventA; - -impl Event for EventA {} - -#[derive(Debug)] -struct EventB; - -impl Event for EventB {} +static_entity!( + PRINT_AMMO_COUNT_PHASE, + (Phase, Pair::new::<ChildOf>(*UPDATE_PHASE)) +); fn main() { let mut world = World::new(); - world.register_system(EventA, count_ammo); - world.register_system(EventB, print_total_ammo_count); + world.register_system(*UPDATE_PHASE, count_ammo); + world.register_system(*PRINT_AMMO_COUNT_PHASE, print_total_ammo_count); world.create_entity((Ammo { ammo_left: 4 },)); world.create_entity((Ammo { ammo_left: 7 },)); @@ -53,7 +49,5 @@ fn main() world.add_sole(AmmoCounter::default()).unwrap(); - world.emit(EventA); - - world.emit(EventB); + world.step(); } diff --git a/ecs/src/actions.rs b/ecs/src/actions.rs index f7b00d3..3dd8755 100644 --- a/ecs/src/actions.rs +++ b/ecs/src/actions.rs @@ -1,12 +1,8 @@ use std::marker::PhantomData; -use std::sync::{Arc, Weak}; - -use crate::component::{ - Component, - Metadata as ComponentMetadata, - Sequence as ComponentSequence, -}; -use crate::system::{NoInitParamFlag, Param as SystemParam, System}; +use std::rc::{Rc, Weak}; + +use crate::component::{Parts as ComponentParts, Sequence as ComponentSequence}; +use crate::system::{Param as SystemParam, System}; use crate::uid::{Kind as UidKind, Uid}; use crate::{ActionQueue, World}; @@ -20,36 +16,61 @@ pub struct Actions<'world> impl<'world> Actions<'world> { - /// Adds a spawning a new entity to the action queue. + /// Queues up a entity to spawn at the end of the current tick. pub fn spawn<Comps: ComponentSequence>(&mut self, components: Comps) { - self.action_queue.push(Action::Spawn(components.into_vec())); + self.action_queue + .push(Action::Spawn(components.into_parts_array().into())); } - /// Adds component(s) to a entity. + /// Queues up despawning a entity at the end of the **next** tick. + pub fn despawn(&mut self, entity_uid: Uid) + { + debug_assert_eq!(entity_uid.kind(), UidKind::Entity); + + self.action_queue.push(Action::Despawn(entity_uid)); + } + + /// Queues up adding component(s) to a entity at the end of the current tick. pub fn add_components<Comps>(&mut self, entity_uid: Uid, components: Comps) where Comps: ComponentSequence, { debug_assert_eq!(entity_uid.kind(), UidKind::Entity); - self.action_queue - .push(Action::AddComponents(entity_uid, components.into_vec())); + if Comps::COUNT == 0 { + return; + } + + self.action_queue.push(Action::AddComponents( + entity_uid, + components.into_parts_array().into(), + )); } - /// Removes component(s) from a entity. - pub fn remove_components<Comps>(&mut self, entity_uid: Uid) - where - Comps: ComponentSequence, + /// Queues up removing component(s) from a entity at the end of the **next** tick. + pub fn remove_components( + &mut self, + entity_uid: Uid, + component_ids: impl IntoIterator<Item = Uid>, + ) { debug_assert_eq!(entity_uid.kind(), UidKind::Entity); - self.action_queue - .push(Action::RemoveComponents(entity_uid, Comps::metadata())); + let mut component_ids = component_ids.into_iter().peekable(); + + if component_ids.peek().is_none() { + return; + } + + self.action_queue.push(Action::RemoveComponents( + entity_uid, + component_ids.collect(), + )); } - /// Adds stopping the loop in [`Engine::event_loop`] at the next opportune time to the - /// action queue. + /// Stops the [`World`]. The world will finish the current tick and that tick will be + /// the last. pub fn stop(&mut self) { self.action_queue.push(Action::Stop); @@ -66,18 +87,17 @@ impl<'world> Actions<'world> } } - fn new(action_queue: &'world Arc<ActionQueue>) -> Self + fn new(action_queue: &'world Rc<ActionQueue>) -> Self { Self { action_queue, - action_queue_weak: Arc::downgrade(action_queue), + action_queue_weak: Rc::downgrade(action_queue), } } } -unsafe impl<'world> SystemParam<'world> for Actions<'world> +impl<'world> SystemParam<'world> for Actions<'world> { - type Flags = NoInitParamFlag; type Input = (); fn initialize<SystemImpl>( @@ -122,11 +142,11 @@ impl WeakRef #[derive(Debug, Clone)] pub struct Ref<'weak_ref> { - action_queue: Arc<ActionQueue>, + action_queue: Rc<ActionQueue>, _pd: PhantomData<&'weak_ref ()>, } -impl<'weak_ref> Ref<'weak_ref> +impl Ref<'_> { #[must_use] pub fn to_actions(&self) -> Actions<'_> @@ -139,8 +159,9 @@ impl<'weak_ref> Ref<'weak_ref> #[derive(Debug)] pub(crate) enum Action { - Spawn(Vec<Box<dyn Component>>), - AddComponents(Uid, Vec<Box<dyn Component>>), - RemoveComponents(Uid, Vec<ComponentMetadata>), + Spawn(Vec<ComponentParts>), + Despawn(Uid), + AddComponents(Uid, Vec<ComponentParts>), + RemoveComponents(Uid, Vec<Uid>), Stop, } diff --git a/ecs/src/archetype.rs b/ecs/src/archetype.rs deleted file mode 100644 index d2ee36a..0000000 --- a/ecs/src/archetype.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::hash::{DefaultHasher, Hash, Hasher}; - -use crate::component::{ - IsOptional as ComponentIsOptional, - Metadata as ComponentMetadata, -}; -use crate::uid::Uid; - -/// Archetype ID. -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub struct Id -{ - hash: u64, -} - -impl Id -{ - pub fn from_components_metadata( - components_metadata: &impl AsRef<[ComponentMetadata]>, - ) -> Self - { - debug_assert!( - components_metadata.as_ref().len() > 0, - "Cannot create a archetype ID from a empty component metadata list" - ); - - debug_assert!( - components_metadata - .as_ref() - .is_sorted_by_key(|comp_metadata| comp_metadata.id), - "Cannot create archetype ID from a unsorted component metadata list" - ); - - Self::new( - components_metadata - .as_ref() - .iter() - .filter_map(|component_metadata| { - if component_metadata.is_optional == ComponentIsOptional::Yes { - return None; - } - - Some(component_metadata.id) - }), - ) - } - - /// Returns the ID of a archetype with the given components. - fn new(component_ids: impl IntoIterator<Item = Uid>) -> Self - { - let mut hasher = DefaultHasher::new(); - - for component_id in component_ids { - component_id.hash(&mut hasher); - } - - let hash = hasher.finish(); - - Self { hash } - } -} diff --git a/ecs/src/component.rs b/ecs/src/component.rs index a9894b7..5a8cd0b 100644 --- a/ecs/src/component.rs +++ b/ecs/src/component.rs @@ -1,79 +1,53 @@ use std::any::{type_name, Any}; use std::fmt::Debug; +use std::ops::{Deref, DerefMut}; +use ecs_macros::Component; +use hashbrown::HashSet; use seq_macro::seq; -use crate::lock::{ReadGuard, WriteGuard}; -use crate::system::{ComponentRef, ComponentRefMut, Input as SystemInput}; -use crate::type_name::TypeName; +use crate::lock::{ + Error as LockError, + MappedReadGuard, + MappedWriteGuard, + ReadGuard, + WriteGuard, +}; +use crate::system::Input as SystemInput; use crate::uid::Uid; -use crate::{EntityComponent, World}; +use crate::util::Array; +use crate::EntityComponentRef; pub mod local; pub(crate) mod storage; -pub trait Component: SystemInput + Any + TypeName +pub trait Component: SystemInput + Any { - /// The component type in question. Will usually be `Self` - type Component: Component - where - Self: Sized; - - type RefMut<'component> - where - Self: Sized; - - type Ref<'component> - where - Self: Sized; - /// Returns the ID of this component. fn id() -> Uid where Self: Sized; - /// The ID of the component `self`. Returns the same value as [`Component::id`]. - fn self_id(&self) -> Uid; - - #[doc(hidden)] - fn as_any_mut(&mut self) -> &mut dyn Any; - - #[doc(hidden)] - fn as_any(&self) -> &dyn Any; - - /// Whether the component `self` is optional. Returns the same value as - /// [`Component::is_optional`]. - fn self_is_optional(&self) -> IsOptional - { - IsOptional::No - } - - /// Returns whether this component is optional. - #[must_use] - fn is_optional() -> IsOptional - where - Self: Sized, - { - IsOptional::No - } + /// Returns the name of this component. + fn name(&self) -> &'static str; } impl dyn Component { pub fn downcast_mut<Real: 'static>(&mut self) -> Option<&mut Real> { - self.as_any_mut().downcast_mut() + (self as &mut dyn Any).downcast_mut() } pub fn downcast_ref<Real: 'static>(&self) -> Option<&Real> { - self.as_any().downcast_ref() + (self as &dyn Any).downcast_ref() } pub fn is<Other: 'static>(&self) -> bool { - self.as_any().is::<Other>() + (self as &dyn Any).is::<Other>() } } @@ -85,249 +59,292 @@ impl Debug for dyn Component } } -impl TypeName for Box<dyn Component> +/// A sequence of components. +pub trait Sequence { - fn type_name(&self) -> &'static str - { - self.as_ref().type_name() - } + /// The number of components in this component sequence. + const COUNT: usize; + + type PartsArray: Array<Parts>; + + fn into_parts_array(self) -> Self::PartsArray; } -impl<ComponentT> Component for Option<ComponentT> -where - ComponentT: Component, +#[derive(Debug)] +pub struct Handle<'a, ComponentData: 'static> { - type Component = ComponentT; - type Ref<'component> = Option<ComponentRef<'component, ComponentT>>; - type RefMut<'component> = Option<ComponentRefMut<'component, ComponentT>>; + inner: MappedReadGuard<'a, ComponentData>, +} - fn id() -> Uid +impl<'comp, ComponentData: 'static> Handle<'comp, ComponentData> +{ + /// Creates a new handle instance from a [`EntityComponentRef`]. + /// + /// # Errors + /// Will return `Err` if acquiring the component's lock fails. + pub fn from_entity_component_ref( + entity_component_ref: EntityComponentRef<'comp>, + ) -> Result<Self, HandleError> { - ComponentT::id() + Ok(Self::new( + entity_component_ref + .component() + .read_nonblock() + .map_err(AcquireLockError)?, + )) } - fn self_id(&self) -> Uid + pub(crate) fn new(inner: ReadGuard<'comp, Box<dyn Any>>) -> Self { - Self::id() + Self { + inner: inner.map(|component| { + component + .downcast_ref::<ComponentData>() + .unwrap_or_else(|| { + panic!( + "Failed to downcast component to type {}", + type_name::<ComponentData>() + ); + }) + }), + } } +} + +impl<ComponentData: 'static> Deref for Handle<'_, ComponentData> +{ + type Target = ComponentData; - fn as_any_mut(&mut self) -> &mut dyn Any + fn deref(&self) -> &Self::Target { - self + &self.inner } +} - fn as_any(&self) -> &dyn Any +#[derive(Debug)] +pub struct HandleMut<'a, ComponentData: 'static> +{ + inner: MappedWriteGuard<'a, ComponentData>, +} + +impl<'comp, ComponentData: 'static> HandleMut<'comp, ComponentData> +{ + /// Creates a new handle instance from a [`EntityComponentRef`]. + /// + /// # Errors + /// Will return `Err` if acquiring the component's lock fails. + pub fn from_entity_component_ref( + entity_component_ref: EntityComponentRef<'comp>, + ) -> Result<Self, HandleError> { - self + Ok(Self::new( + entity_component_ref + .component() + .write_nonblock() + .map_err(AcquireLockError)?, + )) } - fn self_is_optional(&self) -> IsOptional + pub(crate) fn new(inner: WriteGuard<'comp, Box<dyn Any>>) -> Self { - Self::is_optional() + Self { + inner: inner.map(|component| { + component + .downcast_mut::<ComponentData>() + .unwrap_or_else(|| { + panic!( + "Failed to downcast component to type {}", + type_name::<ComponentData>() + ); + }) + }), + } } +} - fn is_optional() -> IsOptional +impl<ComponentData: 'static> Deref for HandleMut<'_, ComponentData> +{ + type Target = ComponentData; + + fn deref(&self) -> &Self::Target { - IsOptional::Yes + &self.inner } } -impl<ComponentT> TypeName for Option<ComponentT> -where - ComponentT: Component, +impl<ComponentData: 'static> DerefMut for HandleMut<'_, ComponentData> { - fn type_name(&self) -> &'static str + fn deref_mut(&mut self) -> &mut Self::Target { - type_name::<Self>() + &mut self.inner } } -impl<ComponentT> SystemInput for Option<ComponentT> where ComponentT: Component {} +#[derive(Debug, thiserror::Error)] +pub enum HandleError +{ + #[error(transparent)] + AcquireLockFailed(#[from] AcquireLockError), +} -/// A sequence of components. -pub trait Sequence +#[derive(Debug, thiserror::Error)] +#[error("Failed to acquire component lock")] +pub struct AcquireLockError(#[source] LockError); + +macro_rules! inner { + ($c: tt) => { + seq!(I in 0..=$c { + impl<#(IntoCompParts~I: IntoParts,)*> Sequence for (#(IntoCompParts~I,)*) + { + const COUNT: usize = $c + 1; + + type PartsArray = [Parts; $c + 1]; + + fn into_parts_array(self) -> Self::PartsArray + { + [#({ + self.I.into_parts() + },)*] + } + } + }); + }; +} + +seq!(C in 0..=16 { + inner!(C); +}); + +impl Sequence for () { - type MutRefs<'component> - where - Self: 'component; + type PartsArray = [Parts; 0]; - type Refs<'component> - where - Self: 'component; - - fn into_vec(self) -> Vec<Box<dyn Component>>; - - fn metadata() -> Vec<Metadata>; - - fn from_components_mut<'component>( - components: impl Iterator<Item = &'component EntityComponent>, - world: &'component World, - lock_component: fn( - entity_component: &EntityComponent, - ) -> WriteGuard<'_, Box<dyn Component>>, - ) -> Self::MutRefs<'component>; - - fn from_components<'component>( - components: impl Iterator<Item = &'component EntityComponent>, - world: &'component World, - lock_component: fn( - entity_component: &EntityComponent, - ) -> ReadGuard<'_, Box<dyn Component>>, - ) -> Self::Refs<'component>; + const COUNT: usize = 0; + + fn into_parts_array(self) -> Self::PartsArray + { + [] + } +} + +pub trait IntoParts +{ + fn into_parts(self) -> Parts; +} + +impl<ComponentT> IntoParts for ComponentT +where + ComponentT: Component, +{ + fn into_parts(self) -> Parts + { + Parts::builder() + .name(type_name::<Self>()) + .build(Self::id(), self) + } } -/// [`Component`] metadata. -#[derive(Debug, Clone)] +/// The parts of a component. +#[derive(Debug)] #[non_exhaustive] -pub struct Metadata +pub struct Parts { - pub id: Uid, - pub is_optional: IsOptional, + id: Uid, + name: &'static str, + data: Box<dyn Any>, } -impl Metadata +impl Parts { - pub fn get<ComponentT: Component + ?Sized>(component: &ComponentT) -> Self + #[must_use] + pub fn id(&self) -> Uid { - Self { - id: component.self_id(), - is_optional: component.self_is_optional(), - } + self.id } - pub fn of<ComponentT: Component>() -> Self + #[must_use] + pub fn name(&self) -> &'static str { - Self { - id: ComponentT::id(), - is_optional: ComponentT::is_optional(), - } + self.name + } + + #[must_use] + pub fn builder() -> PartsBuilder + { + PartsBuilder::default() + } + + pub(crate) fn into_data(self) -> Box<dyn Any> + { + self.data } } -/// Whether or not a `Component` is optional. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum IsOptional +#[derive(Debug)] +pub struct PartsBuilder { - Yes, - No, + name: &'static str, } -impl From<bool> for IsOptional +impl PartsBuilder { - fn from(is_optional: bool) -> Self + #[must_use] + pub fn name(mut self, name: &'static str) -> Self { - if is_optional { - return IsOptional::Yes; - } + self.name = name; + self + } - IsOptional::No + #[must_use] + pub fn build<Data: 'static>(self, id: Uid, data: Data) -> Parts + { + Parts { + id, + name: self.name, + data: Box::new(data), + } } } -pub trait FromOptionalMut<'comp> +impl Default for PartsBuilder { - fn from_optional_mut_component( - optional_component: Option<WriteGuard<'comp, Box<dyn Component>>>, - world: &'comp World, - ) -> Self; + fn default() -> Self + { + Self { name: "(unspecified)" } + } } -pub trait FromOptional<'comp> +/// Pending component removals for a entity. +#[derive(Debug, Clone, Component)] +pub struct Removals { - fn from_optional_component( - optional_component: Option<ReadGuard<'comp, Box<dyn Component>>>, - world: &'comp World, - ) -> Self; + component_ids: HashSet<Uid>, } -macro_rules! inner { - ($c: tt) => { - seq!(I in 0..=$c { - impl<#(Comp~I: Component,)*> Sequence for (#(Comp~I,)*) - where - #(for<'comp> Comp~I::RefMut<'comp>: FromOptionalMut<'comp>,)* - #(for<'comp> Comp~I::Ref<'comp>: FromOptional<'comp>,)* - { - type MutRefs<'component> = (#(Comp~I::RefMut<'component>,)*) - where Self: 'component; - - type Refs<'component> = (#(Comp~I::Ref<'component>,)*) - where Self: 'component; - - fn into_vec(self) -> Vec<Box<dyn Component>> - { - Vec::from_iter([#(Box::new(self.I) as Box<dyn Component>,)*]) - } - - fn metadata() -> Vec<Metadata> - { - vec![ - #( - Metadata { - id: Comp~I::id(), - is_optional: Comp~I::is_optional() - }, - )* - ] - } - - fn from_components_mut<'component>( - components: impl Iterator<Item = &'component EntityComponent>, - world: &'component World, - lock_component: fn( - entity_component: &EntityComponent, - ) -> WriteGuard<'_, Box<dyn Component>>, - ) -> Self::MutRefs<'component> - { - #( - let mut comp_~I: Option<WriteGuard<Box<dyn Component>>> = None; - )* - - for comp in components { - #( - if comp.id == Comp~I::Component::id() { - comp_~I = Some(lock_component(comp)); - continue; - } - )* - } - - (#( - Comp~I::RefMut::from_optional_mut_component(comp_~I, world), - )*) - } +impl Removals +{ + pub fn contains<ComponentT: Component>(&self) -> bool + { + self.contains_id(ComponentT::id()) + } - fn from_components<'component>( - components: impl Iterator<Item = &'component EntityComponent>, - world: &'component World, - lock_component: fn( - entity_component: &EntityComponent, - ) -> ReadGuard<'_, Box<dyn Component>>, - ) -> Self::Refs<'component> - { + pub fn contains_id(&self, component_id: Uid) -> bool + { + self.component_ids.contains(&component_id) + } - #( - let mut comp_~I: Option<ReadGuard<Box<dyn Component>>> = None; - )* - - for comp in components { - #( - if comp.id == Comp~I::Component::id() { - comp_~I = Some(lock_component(comp)); - continue; - } - )* - } - - (#( - Comp~I::Ref::from_optional_component(comp_~I, world), - )*) - } - } - }); - }; + pub(crate) fn add_ids(&mut self, ids: impl IntoIterator<Item = Uid>) + { + self.component_ids.extend(ids) + } } -seq!(C in 0..=64 { - inner!(C); -}); +impl FromIterator<Uid> for Removals +{ + fn from_iter<T: IntoIterator<Item = Uid>>(iter: T) -> Self + { + Self { + component_ids: iter.into_iter().collect(), + } + } +} diff --git a/ecs/src/component/local.rs b/ecs/src/component/local.rs index 20627bf..0f6f641 100644 --- a/ecs/src/component/local.rs +++ b/ecs/src/component/local.rs @@ -1,21 +1,20 @@ use std::ops::{Deref, DerefMut}; -use crate::component::Component; -use crate::system::{ComponentRefMut, Param as SystemParam, System}; +use crate::component::{Component, HandleMut as ComponentHandleMut}; +use crate::system::{Param as SystemParam, System}; use crate::World; /// Holds a component which is local to a single system. #[derive(Debug)] pub struct Local<'world, LocalComponent: Component> { - local_component: ComponentRefMut<'world, LocalComponent>, + local_component: ComponentHandleMut<'world, LocalComponent>, } -unsafe impl<'world, LocalComponent> SystemParam<'world> for Local<'world, LocalComponent> +impl<'world, LocalComponent> SystemParam<'world> for Local<'world, LocalComponent> where LocalComponent: Component, { - type Flags = (); type Input = LocalComponent; fn initialize<SystemImpl>( @@ -39,7 +38,7 @@ where } } -impl<'world, LocalComponent> Deref for Local<'world, LocalComponent> +impl<LocalComponent> Deref for Local<'_, LocalComponent> where LocalComponent: Component, { @@ -51,7 +50,7 @@ where } } -impl<'world, LocalComponent> DerefMut for Local<'world, LocalComponent> +impl<LocalComponent> DerefMut for Local<'_, LocalComponent> where LocalComponent: Component, { diff --git a/ecs/src/component/storage.rs b/ecs/src/component/storage.rs index ffd682e..b27b552 100644 --- a/ecs/src/component/storage.rs +++ b/ecs/src/component/storage.rs @@ -1,621 +1,787 @@ -use std::any::type_name; -use std::borrow::Borrow; +use std::any::Any; +use std::array::IntoIter as ArrayIter; use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; -use std::slice::Iter as SliceIter; -use std::vec::IntoIter as OwnedVecIter; - -use crate::archetype::Id as ArchetypeId; -use crate::component::{ - Component, - IsOptional as ComponentIsOptional, - Metadata as ComponentMetadata, +use std::vec::IntoIter as VecIntoIter; + +use hashbrown::HashMap; + +use crate::component::storage::archetype::{ + Archetype, + Entity as ArchetypeEntity, + EntityComponent as ArchetypeEntityComponent, + Id as ArchetypeId, }; -use crate::type_name::TypeName; -use crate::uid::Uid; -use crate::util::Sortable; -use crate::EntityComponent; +use crate::component::storage::graph::{ + ArchetypeAddEdgeDfsIter, + ArchetypeAddEdgeDfsIterResult, + ArchetypeEdges, + Graph, +}; +use crate::uid::{Kind as UidKind, Uid}; +use crate::util::{BorrowedOrOwned, Either, StreamingIterator, VecExt}; -#[derive(Debug, Default)] -pub struct Storage +pub mod archetype; + +mod graph; + +#[derive(Debug)] +pub struct ArchetypeSearchTerms<'a> { - archetypes: Vec<Archetype>, - archetype_lookup: RefCell<HashMap<ArchetypeId, ArchetypeLookupEntry>>, - entity_archetype_lookup: HashMap<Uid, ArchetypeId>, + pub required_components: &'a [Uid], + pub excluded_components: &'a [Uid], } -impl Storage +impl ArchetypeSearchTerms<'_> { - pub fn find_entities<CompMetadataList>( - &self, - mut components_metadata: CompMetadataList, - ) -> ArchetypeRefIter<'_> - where - CompMetadataList: Sortable<Item = ComponentMetadata>, - CompMetadataList: AsRef<[ComponentMetadata]>, + fn excluded_contains(&self, comp_id: Uid) -> bool { - components_metadata.sort_by_key_b(|component_metadata| component_metadata.id); + let comp_id_kind = comp_id.kind(); - let archetype_id = ArchetypeId::from_components_metadata(&components_metadata); + debug_assert!( + comp_id_kind == UidKind::Component + || (comp_id_kind == UidKind::Pair + && comp_id.target_component() != Uid::wildcard()) + ); - // This looks stupid but the borrow checker complains otherwise - if self.archetype_lookup.borrow().contains_key(&archetype_id) { - return self.iter_archetypes_by_lookup(archetype_id); - } + let is_found = self.excluded_components.binary_search(&comp_id).is_ok(); - let comp_ids_set = create_non_opt_component_id_set(components_metadata.as_ref()); + if !is_found && comp_id_kind == UidKind::Pair { + return self.excluded_components.iter().any(|excluded_comp_id| { + excluded_comp_id.kind() == UidKind::Pair + && excluded_comp_id.has_same_relation_as(comp_id) + && excluded_comp_id.target_component() == Uid::wildcard() + }); + } - let matching_archetype_indices = self - .archetypes - .iter() - .enumerate() - .filter_map(|(index, archetype)| { - if archetype.component_ids_is_superset(&comp_ids_set) { - return Some(index); - } + is_found + } - None - }) - .collect(); - - self.archetype_lookup.borrow_mut().insert( - archetype_id, - ArchetypeLookupEntry { - component_ids: comp_ids_set, - archetype_indices: matching_archetype_indices, - }, - ); + fn contains_conflicting(&self) -> bool + { + self.excluded_components.iter().any(|excluded_comp_id| { + self.required_components + .binary_search(excluded_comp_id) + .is_ok() + }) + } - self.iter_archetypes_by_lookup(archetype_id) + fn archetype_contains_all_required(&self, archetype: &Archetype) -> bool + { + self.required_components + .iter() + .all(|comp_id| archetype.contains_matching_component(*comp_id)) } +} - pub fn get_entity_archetype(&self, entity_uid: Uid) -> Option<&Archetype> +#[derive(Debug, Default)] +pub struct Storage +{ + graph: Graph, + entity_archetype_lookup: HashMap<Uid, ArchetypeId>, + imaginary_archetypes: RefCell<Vec<ImaginaryArchetype>>, +} + +impl Storage +{ + pub fn search_archetypes<'search_terms>( + &self, + search_terms: ArchetypeSearchTerms<'search_terms>, + ) -> ArchetypeRefIter<'_, 'search_terms> { - let archetype_id = self.entity_archetype_lookup.get(&entity_uid)?; + let archetype_id = ArchetypeId::new(search_terms.required_components); + + if search_terms.contains_conflicting() { + return ArchetypeRefIter { + storage: self, + pre_iter: Either::B(Vec::new().into_iter()), + dfs_iter: ArchetypeAddEdgeDfsIter::new(&self.graph, &[]), + search_terms, + }; + } - let archetype_index = - self.find_archetype_index_with_entity(*archetype_id, entity_uid)?; + let Some(add_edge_recursive_iter) = + self.graph.dfs_archetype_add_edges(archetype_id) + else { + self.imaginary_archetypes + .borrow_mut() + .push(ImaginaryArchetype { + id: ArchetypeId::new(search_terms.required_components.iter().filter( + |required_comp_id| { + required_comp_id.kind() != UidKind::Pair + || required_comp_id.target_component() != Uid::wildcard() + }, + )), + component_ids: search_terms + .required_components + .iter() + .copied() + .filter(|required_comp_id| { + required_comp_id.kind() != UidKind::Pair + || required_comp_id.target_component() != Uid::wildcard() + }) + .collect(), + }); + + let found_archetypes = self.find_all_archetype_with_comps(&search_terms); + + return ArchetypeRefIter { + storage: self, + pre_iter: Either::B(found_archetypes.clone().into_iter()), + dfs_iter: ArchetypeAddEdgeDfsIter::new(&self.graph, &found_archetypes), + search_terms, + }; + }; - self.archetypes.get(archetype_index) + ArchetypeRefIter { + storage: self, + pre_iter: Either::A([archetype_id].into_iter()), + dfs_iter: add_edge_recursive_iter, + search_terms, + } } - #[cfg_attr(feature = "debug", tracing::instrument(skip_all))] - pub fn push_entity( - &mut self, - entity_uid: Uid, - mut components: Vec<Box<dyn Component>>, - ) -> Result<(ArchetypeId, Uid), Error> + pub fn get_archetype_by_id(&self, id: ArchetypeId) -> Option<&Archetype> { - if self.entity_archetype_lookup.contains_key(&entity_uid) { - return Err(Error::EntityAlreadyExists(entity_uid)); + Some(self.graph.get_node_by_id(id)?.archetype()) + } + + pub fn create_entity(&mut self, uid: Uid) -> Result<(), Error> + { + debug_assert_eq!(uid.kind(), UidKind::Entity); + + if self.entity_archetype_lookup.contains_key(&uid) { + return Err(Error::EntityAlreadyExists(uid)); } - components.sort_by_key(|component| component.self_id()); + let empty_archetype_id = ArchetypeId::new_empty(); - #[cfg(feature = "debug")] - tracing::debug!( - "Pushing entity with components: ({})", - components - .iter() - .map(|component| component.type_name()) - .collect::<Vec<_>>() - .join(", ") - ); + let archetype_node = self.graph.get_or_create_node(empty_archetype_id, &[]); - let archetype_id = ArchetypeId::from_components_metadata( - &components - .iter() - .map(|component| ComponentMetadata::get(&**component)) - .collect::<Vec<_>>(), - ); + archetype_node + .archetype_mut() + .push_entity(ArchetypeEntity::new(uid, [])); - let comp_ids_set = create_non_opt_component_id_set( - components - .iter() - .map(|component| ComponentMetadata::get(&**component)), - ); + self.entity_archetype_lookup.insert(uid, empty_archetype_id); - let archetype_index = - self.get_or_create_archetype(archetype_id, &comp_ids_set, &components); + Ok(()) + } - self.populate_matching_archetype_lookup_entries(&comp_ids_set, archetype_index); + pub fn remove_entity(&mut self, entity_uid: Uid) -> Result<ArchetypeEntity, Error> + { + let Some(archetype_id) = self.entity_archetype_lookup.get(&entity_uid) else { + return Err(Error::EntityDoesNotExist(entity_uid)); + }; - let archetype = self - .archetypes - .get_mut(archetype_index) - .expect("Archetype is gone"); + let archetype_node = self + .graph + .get_node_by_id_mut(*archetype_id) + .expect("Archetype should exist"); - archetype.push_entity(entity_uid, components); + let entity = archetype_node + .archetype_mut() + .remove_entity(entity_uid) + .expect("Entity should exist in archetype"); - archetype - .entity_lookup - .insert(entity_uid, archetype.entities.len() - 1); + self.entity_archetype_lookup.remove(&entity_uid); - self.entity_archetype_lookup - .insert(entity_uid, archetype_id); + Ok(entity) + } - Ok((archetype_id, entity_uid)) + pub fn get_entity_archetype(&self, entity_uid: Uid) -> Option<&Archetype> + { + let archetype_id = self.entity_archetype_lookup.get(&entity_uid)?; + + self.get_archetype_by_id(*archetype_id) } - pub fn add_components_to_entity( + pub fn add_entity_component( &mut self, entity_uid: Uid, - components: Vec<Box<dyn Component>>, - ) -> Option<()> + (component_id, component_name, component): (Uid, &'static str, Box<dyn Any>), + ) -> Result<(), Error> { - let archetype_id = self.entity_archetype_lookup.get(&entity_uid)?; + let Some(archetype_id) = self.entity_archetype_lookup.get(&entity_uid) else { + return Err(Error::EntityDoesNotExist(entity_uid)); + }; + + let archetype_id = *archetype_id; + + let archetype_node = self + .graph + .get_node_by_id_mut(archetype_id) + .expect("Archetype should exist"); + + if archetype_node + .archetype() + .contains_component_with_exact_id(component_id) + { + return Err(Error::ComponentAlreadyInEntity { + entity: entity_uid, + component: component_id, + }); + } - let archetype_index = - self.find_archetype_index_with_entity(*archetype_id, entity_uid)?; + let add_edge_archetype_id = if let Some(add_edge_id) = archetype_node + .get_or_insert_edges(component_id, ArchetypeEdges::default) + .add + { + if !self.graph.contains_archetype(add_edge_id) { + let (_, add_edge_comp_ids) = self + .graph + .get_node_by_id(archetype_id) + .expect("Archetype should exist") + .make_add_edge(component_id); + + self.graph.create_node(add_edge_id, &add_edge_comp_ids); + } - let archetype = self.archetypes.get_mut(archetype_index)?; + add_edge_id + } else { + let archetype_node = self + .graph + .get_node_by_id(archetype_id) + .expect("Archetype should exist"); - let contains_component_already = components - .iter() - .any(|component| archetype.component_ids.contains_key(&component.self_id())); - - if contains_component_already { - let component_cnt = components.len(); - - // TODO: return error - panic!( - "Entity with UID {:?} already contains one or more component(s) ({})", - entity_uid, - components - .iter() - .flat_map(|component| [component.type_name(), ", "]) - .enumerate() - .take_while(|(index, _)| { *index < (component_cnt * 2) - 1 }) - .map(|(_, component)| component) - .collect::<String>() - ); - } + let (add_edge_id, add_edge_comp_ids) = + archetype_node.make_add_edge(component_id); - let entity = archetype.take_entity(entity_uid)?; + if !self.graph.contains_archetype(add_edge_id) { + self.graph.create_node(add_edge_id, &add_edge_comp_ids); + } - self.entity_archetype_lookup.remove(&entity_uid); + add_edge_id + }; + + let archetype_node = self + .graph + .get_node_by_id_mut(archetype_id) + .expect("Archetype should exist"); + + let mut entity = archetype_node + .archetype_mut() + .remove_entity(entity_uid) + .expect("Entity should exist in archetype"); + + let add_edge_archetype = self + .graph + .get_node_by_id_mut(add_edge_archetype_id) + .expect("Add edge archetype should exist") + .archetype_mut(); + + entity.insert_component( + component_id, + ArchetypeEntityComponent::new(component, component_id, component_name), + add_edge_archetype, + ); - self.push_entity( - entity_uid, - entity - .components - .into_iter() - .map(|component| component.component.into_inner()) - .chain(components) - .collect(), - ) - .expect("Not supposed to return Err since the entity is removed"); + add_edge_archetype.push_entity(entity); - Some(()) + self.entity_archetype_lookup + .insert(entity_uid, add_edge_archetype_id); + + Ok(()) } - pub fn remove_components_from_entity( + pub fn remove_entity_component( &mut self, entity_uid: Uid, - component_ids: impl IntoIterator<Item = Uid>, - ) -> Option<()> + component_id: Uid, + ) -> Result<(), Error> { - let archetype_id = self.entity_archetype_lookup.get(&entity_uid)?; + let Some(archetype_id) = self.entity_archetype_lookup.get(&entity_uid) else { + return Err(Error::EntityDoesNotExist(entity_uid)); + }; + + let archetype_id = *archetype_id; + + let archetype_node = self + .graph + .get_node_by_id_mut(archetype_id) + .expect("Archetype should exist"); + + if !archetype_node + .archetype() + .contains_component_with_exact_id(component_id) + { + return Err(Error::ComponentNotFoundInEntity { + entity: entity_uid, + component: component_id, + }); + } - let archetype_index = - self.find_archetype_index_with_entity(*archetype_id, entity_uid)?; + let remove_edge_id = archetype_node + .get_or_insert_edges(component_id, ArchetypeEdges::default) + .remove + .unwrap_or_else(|| { + let archetype_node = self + .graph + .get_node_by_id_mut(archetype_id) + .expect("Archetype should exist"); + + let (remove_edge_id, remove_edge_comp_ids) = + archetype_node.make_remove_edge(component_id); + + if !self.graph.contains_archetype(remove_edge_id) { + self.graph + .create_node(remove_edge_id, &remove_edge_comp_ids); + } - let archetype = self.archetypes.get_mut(archetype_index)?; + remove_edge_id + }); - let entity = archetype.take_entity(entity_uid)?; + let archetype_node = self + .graph + .get_node_by_id_mut(archetype_id) + .expect("Archetype should exist"); - let component_ids_set = component_ids.into_iter().collect::<HashSet<_>>(); + let mut entity = archetype_node + .archetype_mut() + .remove_entity(entity_uid) + .expect("Entity should exist in archetype"); - self.entity_archetype_lookup.remove(&entity_uid); + entity.remove_component(component_id, archetype_node.archetype()); - self.push_entity( - entity_uid, - entity - .components - .into_iter() - .map(|component| component.component.into_inner()) - .filter(|component| !component_ids_set.contains(&component.self_id())) - .collect(), - ) - .expect("Not supposed to return Err since the entity is removed"); + self.graph + .get_node_by_id_mut(remove_edge_id) + .expect("Remove edge archetype should exist") + .archetype_mut() + .push_entity(entity); - Some(()) + self.entity_archetype_lookup + .insert(entity_uid, remove_edge_id); + + Ok(()) } - fn populate_matching_archetype_lookup_entries( - &mut self, - comp_ids_set: &HashSet<Uid>, - archetype_index: usize, - ) + pub fn create_imaginary_archetypes(&mut self) { - let mut archetype_lookup = self.archetype_lookup.borrow_mut(); - - for (_, lookup_entry) in archetype_lookup.iter_mut() { - if &lookup_entry.component_ids == comp_ids_set { + for imaginary_archetype in self.imaginary_archetypes.get_mut().drain(..) { + if self.graph.contains_archetype(imaginary_archetype.id) { continue; } - // There shouldn't be duplicate archetype indices in the lookup entry - if lookup_entry.archetype_indices.contains(&archetype_index) { - continue; - } - - if lookup_entry.component_ids.is_subset(comp_ids_set) { - lookup_entry.archetype_indices.push(archetype_index); - } + self.graph + .create_node(imaginary_archetype.id, &imaginary_archetype.component_ids); } } - fn get_or_create_archetype( - &mut self, - archetype_id: ArchetypeId, - comp_ids_set: &HashSet<Uid>, - components: &[Box<dyn Component>], - ) -> usize + fn find_all_archetype_with_comps( + &self, + search_terms: &ArchetypeSearchTerms<'_>, + ) -> Vec<ArchetypeId> { - let mut archetype_lookup = self.archetype_lookup.borrow_mut(); + let Some(mut search_iter) = + self.graph.dfs_archetype_add_edges(ArchetypeId::new_empty()) + else { + // If the root archetype doesn't exist, no other archetype can exist either + // + // TODO: The above comment is not true. Cases where imaginary archetypes have + // been created should be handled as well + return Vec::new(); + }; + + let mut found = Vec::<ArchetypeId>::new(); + + while let Some(node_id) = search_iter.streaming_next() { + let ArchetypeAddEdgeDfsIterResult::AddEdge { + add_edge_archetype_id: node_id, + add_edge_component_id, + } = node_id + else { + continue; + }; - let lookup_entry = archetype_lookup.entry(archetype_id).or_insert_with(|| { - ArchetypeLookupEntry { - component_ids: comp_ids_set.clone(), - archetype_indices: Vec::with_capacity(1), + if search_terms.excluded_contains(add_edge_component_id) { + search_iter.pop(); + continue; + } + + let node = self + .graph + .get_node_by_id(node_id) + .expect("Graph node found through DFS doesn't exist"); + + if node.archetype().component_cnt() < search_terms.required_components.len() { + continue; + } + + if !search_terms.archetype_contains_all_required(node.archetype()) { + continue; } - }); - if lookup_entry.archetype_indices.is_empty() { - self.archetypes.push(Archetype::new( - components.iter().map(|component| component.self_id()), - )); + found.push(node.archetype().id()); - lookup_entry - .archetype_indices - .push(self.archetypes.len() - 1); + search_iter.pop(); } - // SAFETY: Above, we push a archetype index if archetype_indices is empty so this - // cannot fail - unsafe { *lookup_entry.archetype_indices.first().unwrap_unchecked() } + found } +} - fn find_archetype_index_with_entity( +#[cfg(feature = "vizoxide")] +impl Storage +{ + pub fn create_vizoxide_archetype_graph( &self, - archetype_id: ArchetypeId, - entity_uid: Uid, - ) -> Option<usize> + graph_name: impl AsRef<str>, + params: VizoxideArchetypeGraphParams, + ) -> Result<vizoxide::Graph, vizoxide::GraphvizError> { - let archetype_lookup = self.archetype_lookup.borrow_mut(); + let viz_graph = vizoxide::Graph::builder(graph_name.as_ref()) + .strict(true) + .directed(true) + .build()?; + + let mut viz_node_lookup = HashMap::new(); + + for node in self.graph.iter_nodes() { + let id = node.archetype().id(); + + if !viz_node_lookup.contains_key(&id) { + let node = self.graph.get_node_by_id(id).unwrap(); + + let viz_node = (params.create_node_cb)( + node.archetype(), + ArchetypeMetadata { is_imaginary: false }, + viz_graph.create_node(&(params.create_node_name)( + node.archetype(), + ArchetypeMetadata { is_imaginary: false }, + )), + ) + .build()?; + + viz_node_lookup.insert(id, viz_node); + } - let archetype_lookup_entry = archetype_lookup.get(&archetype_id)?; + for (edge_comp_id, edges) in node.iter_edges() { + if let Some(add_edge) = edges.add { + if !viz_node_lookup.contains_key(&add_edge) { + let viz_node = self.create_vizoxide_archetype_graph_edge_node( + &viz_graph, + node, + add_edge, + *edge_comp_id, + ¶ms, + )?; + + viz_node_lookup.insert(add_edge, viz_node); + } + + (params.create_edge_cb)( + node.archetype(), + *edge_comp_id, + VizoxideArchetypeGraphEdgeKind::Add, + viz_graph.create_edge( + viz_node_lookup.get(&id).unwrap(), + viz_node_lookup.get(&add_edge).unwrap(), + Some(&format!("Add {}", edge_comp_id.id())), + ), + ) + .build()?; + } - // TODO: Also have a hashmap for precise archetype ID -> archetype index lookup. - // This way is slow - archetype_lookup_entry - .archetype_indices - .iter() - .find(|archetype_index| { - let Some(archetype) = self.archetypes.get(**archetype_index) else { - return false; - }; + if let Some(remove_edge) = edges.remove { + if !viz_node_lookup.contains_key(&remove_edge) { + let viz_node = self.create_vizoxide_archetype_graph_edge_node( + &viz_graph, + node, + remove_edge, + *edge_comp_id, + ¶ms, + )?; + + viz_node_lookup.insert(remove_edge, viz_node); + } + + (params.create_edge_cb)( + node.archetype(), + *edge_comp_id, + VizoxideArchetypeGraphEdgeKind::Remove, + viz_graph.create_edge( + viz_node_lookup.get(&id).unwrap(), + viz_node_lookup.get(&remove_edge).unwrap(), + Some(&format!("Remove {}", edge_comp_id.id())), + ), + ) + .build()?; + } + } + } - archetype.has_entity(entity_uid) - }) - .copied() + drop(viz_node_lookup); + + Ok(viz_graph) } - fn iter_archetypes_by_lookup(&self, archetype_id: ArchetypeId) - -> ArchetypeRefIter<'_> + fn create_vizoxide_archetype_graph_edge_node<'vizoxide_graph>( + &self, + viz_graph: &'vizoxide_graph vizoxide::Graph, + node: &graph::ArchetypeNode, + edge_id: ArchetypeId, + edge_comp_id: Uid, + params: &VizoxideArchetypeGraphParams, + ) -> Result<vizoxide::Node<'vizoxide_graph>, vizoxide::GraphvizError> { - let archetype_lookup = self.archetype_lookup.borrow(); - - // The archetype indices have to be cloned to prevent a panic when query - // iterations are nested. The panic happens because the archetype_lookup RefCell - // is borrowed and it tries to mutably borrow it - let archetype_indices = archetype_lookup - .get(&archetype_id) - .unwrap() - .archetype_indices - .clone(); - - ArchetypeRefIter { - indices: archetype_indices.into_iter(), - archetypes: &self.archetypes, + match self.graph.get_node_by_id(edge_id) { + Some(edge_node) => (params.create_node_cb)( + edge_node.archetype(), + ArchetypeMetadata { is_imaginary: false }, + viz_graph.create_node(&(params.create_node_name)( + edge_node.archetype(), + ArchetypeMetadata { is_imaginary: false }, + )), + ) + .build(), + None => { + let mut comp_ids = + node.archetype().component_ids_sorted().collect::<Vec<_>>(); + + let insert_index = comp_ids.partition_point(|cid| *cid <= edge_comp_id); + + comp_ids.insert(insert_index, edge_comp_id); + + let imaginary_edge_archetype = Archetype::new(edge_id, comp_ids); + + (params.create_node_cb)( + &imaginary_edge_archetype, + ArchetypeMetadata { is_imaginary: true }, + viz_graph.create_node(&(params.create_node_name)( + &imaginary_edge_archetype, + ArchetypeMetadata { is_imaginary: true }, + )), + ) + .build() + } } } } -/// Component storage error -#[derive(Debug, Clone, thiserror::Error)] -pub enum Error +#[cfg(feature = "vizoxide")] +pub struct VizoxideArchetypeGraphParams { - #[error("Entity already exists")] - EntityAlreadyExists(Uid), + pub create_node_name: fn(&Archetype, ArchetypeMetadata) -> std::borrow::Cow<'_, str>, + pub create_node_cb: for<'storage, 'graph> fn( + &'storage Archetype, + ArchetypeMetadata, + vizoxide::NodeBuilder<'graph>, + ) -> vizoxide::NodeBuilder<'graph>, + pub create_edge_cb: for<'storage, 'graph> fn( + &'storage Archetype, + Uid, + VizoxideArchetypeGraphEdgeKind, + vizoxide::EdgeBuilder<'graph>, + ) -> vizoxide::EdgeBuilder<'graph>, } -impl TypeName for Storage +#[cfg(feature = "vizoxide")] +#[derive(Debug, Clone)] +pub struct ArchetypeMetadata { - fn type_name(&self) -> &'static str - { - type_name::<Self>() - } + pub is_imaginary: bool, } -#[derive(Debug)] -struct ArchetypeLookupEntry +#[cfg(feature = "vizoxide")] +#[derive(Debug, Clone, Copy)] +pub enum VizoxideArchetypeGraphEdgeKind { - component_ids: HashSet<Uid>, - archetype_indices: Vec<usize>, + Add, + Remove, } #[derive(Debug)] -pub struct Archetype +pub struct ArchetypeRefIter<'storage, 'search_terms> { - component_ids: HashMap<Uid, usize>, - entity_lookup: HashMap<Uid, usize>, - entities: Vec<ArchetypeEntity>, + storage: &'storage Storage, + pre_iter: Either<ArrayIter<ArchetypeId, 1>, VecIntoIter<ArchetypeId>>, + dfs_iter: ArchetypeAddEdgeDfsIter<'storage>, + search_terms: ArchetypeSearchTerms<'search_terms>, } -impl Archetype +impl<'component_storage> Iterator for ArchetypeRefIter<'component_storage, '_> { - fn new(component_ids: impl IntoIterator<Item = Uid>) -> Self - { - Self { - component_ids: component_ids - .into_iter() - .enumerate() - .map(|(index, component_id)| (component_id, index)) - .collect(), - entity_lookup: HashMap::new(), - entities: Vec::new(), - } - } + type Item = &'component_storage Archetype; - pub fn component_ids_is_superset(&self, other_component_ids: &HashSet<Uid>) -> bool + fn next(&mut self) -> Option<Self::Item> { - if other_component_ids.len() <= self.component_ids.len() { - other_component_ids - .iter() - .all(|v| self.component_ids.contains_key(v)) - } else { - false + if let Some(pre_iter_archetype_id) = self.pre_iter.next() { + return Some( + self.storage + .get_archetype_by_id(pre_iter_archetype_id) + .expect("Archetype should exist"), + ); } - } - - pub fn get_entity(&self, entity_uid: Uid) -> Option<&ArchetypeEntity> - { - let entity_index = *self.entity_lookup.get(&entity_uid)?; - - self.entities.get(entity_index) - } - - pub fn entities(&self) -> EntityIter<'_> - { - EntityIter { iter: self.entities.iter() } - } - - pub fn get_index_for_component(&self, component_id: Uid) -> Option<usize> - { - self.component_ids.get(&component_id).copied() - } - - fn push_entity( - &mut self, - entity_uid: Uid, - components: impl IntoIterator<Item = Box<dyn Component>>, - ) - { - self.entities.push(ArchetypeEntity { - uid: entity_uid, - components: components.into_iter().map(Into::into).collect(), - }); - } - - pub fn take_entity(&mut self, entity_uid: Uid) -> Option<ArchetypeEntity> - { - let entity_index = self.entity_lookup.remove(&entity_uid)?; - - let entity = self.entities.remove(entity_index); - for index in self.entity_lookup.values_mut() { - if *index > entity_index { - *index -= 1; + let archetype_id = loop { + match self.dfs_iter.streaming_find(|res| { + matches!( + res, + ArchetypeAddEdgeDfsIterResult::AddEdge { .. } + | ArchetypeAddEdgeDfsIterResult::AddEdgeArchetypeNotFound { .. } + ) + })? { + ArchetypeAddEdgeDfsIterResult::AddEdge { + add_edge_archetype_id, + add_edge_component_id, + } => { + if self.search_terms.excluded_contains(add_edge_component_id) { + self.dfs_iter.pop(); + continue; + } + + break add_edge_archetype_id; + } + ArchetypeAddEdgeDfsIterResult::AddEdgeArchetypeNotFound { + archetype, + add_edge_archetype_id, + add_edge_component_id, + } => { + if self.search_terms.excluded_contains(add_edge_component_id) { + continue; + } + + let mut add_edge_archetype_comps = + archetype.component_ids_sorted().collect::<Vec<_>>(); + + add_edge_archetype_comps.insert_at_partition_point_by_key( + add_edge_component_id, + |comp_id| *comp_id, + ); + + self.storage.imaginary_archetypes.borrow_mut().push( + ImaginaryArchetype { + id: add_edge_archetype_id, + component_ids: add_edge_archetype_comps.clone(), + }, + ); + + let found = + self.find_edges_of_imaginary_archetype(&add_edge_archetype_comps); + + self.dfs_iter.push(( + BorrowedOrOwned::Owned(Archetype::new( + add_edge_archetype_id, + add_edge_archetype_comps.clone(), + )), + found.into_iter(), + )); + } + _ => { + unreachable!(); + } } - } + }; - Some(entity) - } - - fn has_entity(&self, entity_uid: Uid) -> bool - { - self.entity_lookup.contains_key(&entity_uid) + Some( + self.storage + .get_archetype_by_id(archetype_id) + .expect("Archetype should exist"), + ) } } -#[derive(Debug)] -pub struct ArchetypeEntity +impl ArchetypeRefIter<'_, '_> { - uid: Uid, - components: Vec<EntityComponent>, -} - -impl ArchetypeEntity -{ - pub fn uid(&self) -> Uid + fn find_edges_of_imaginary_archetype( + &self, + imaginary_archetype_comps: &[Uid], + ) -> Vec<(Uid, ArchetypeEdges)> { - self.uid - } + self.storage + .find_all_archetype_with_comps(&ArchetypeSearchTerms { + required_components: imaginary_archetype_comps, + excluded_components: &[], + }) + .into_iter() + .filter_map(|found_id| { + let found_archetype = self.storage.get_archetype_by_id(found_id).unwrap(); - pub fn components(&self) -> &[EntityComponent] - { - &self.components - } + if found_archetype.component_cnt() < imaginary_archetype_comps.len() + 1 { + return None; + } - pub fn get_component(&self, index: usize) -> Option<&EntityComponent> - { - self.components.get(index) - } -} + let unique_comp_id = found_archetype + .component_ids_sorted() + .find(|found_archetype_comp_id| { + !imaginary_archetype_comps.iter().any( + |imaginary_archetype_comp_id| { + *imaginary_archetype_comp_id == *found_archetype_comp_id + }, + ) + }) + .expect("Oh noooo"); -#[derive(Debug)] -pub struct ArchetypeRefIter<'component_storage> -{ - indices: OwnedVecIter<usize>, - archetypes: &'component_storage [Archetype], -} + let mut add_edge_comp_ids = imaginary_archetype_comps.to_vec(); -impl<'component_storage> Iterator for ArchetypeRefIter<'component_storage> -{ - type Item = &'component_storage Archetype; + add_edge_comp_ids + .insert_at_partition_point_by_key(unique_comp_id, |id| *id); - fn next(&mut self) -> Option<Self::Item> - { - let archetype_index = self.indices.next()?; + let add_edge = ArchetypeId::new(&add_edge_comp_ids); - Some( - self.archetypes - .get(archetype_index) - .expect("Archetype index in archetype lookup entry was not found"), - ) + Some(( + unique_comp_id, + ArchetypeEdges { add: Some(add_edge), remove: None }, + )) + }) + .collect::<Vec<_>>() } } -#[derive(Debug)] -pub struct EntityIter<'archetype> +#[derive(Debug, thiserror::Error)] +pub enum Error { - iter: SliceIter<'archetype, ArchetypeEntity>, -} + #[error("Entity with ID {0:?} already exists")] + EntityAlreadyExists(Uid), -impl<'archetype> Iterator for EntityIter<'archetype> -{ - type Item = &'archetype ArchetypeEntity; + #[error("Entity with ID {0:?} does not exist")] + EntityDoesNotExist(Uid), - fn next(&mut self) -> Option<Self::Item> + #[error("Entity with ID {entity:?} already has component with ID {component:?}")] + ComponentAlreadyInEntity { - self.iter.next() - } + entity: Uid, component: Uid + }, + + #[error("Entity with ID {entity:?} does not have component with ID {component:?}")] + ComponentNotFoundInEntity + { + entity: Uid, component: Uid + }, } -fn create_non_opt_component_id_set<Item>( - component_metadata_iter: impl IntoIterator<Item = Item>, -) -> HashSet<Uid> -where - Item: Borrow<ComponentMetadata>, +#[derive(Debug)] +struct ImaginaryArchetype { - component_metadata_iter - .into_iter() - .filter_map(|item| { - let component_metadata = item.borrow(); - - if component_metadata.is_optional == ComponentIsOptional::Yes { - return None; - } - - Some(component_metadata.id) - }) - .collect::<HashSet<_>>() + id: ArchetypeId, + component_ids: Vec<Uid>, } #[cfg(test)] mod tests { - - use ecs_macros::Component; - - use super::Storage; - use crate::archetype::Id as ArchetypeId; - use crate::component::{ - Component, - IsOptional as ComponentIsOptional, - Metadata as ComponentMetadata, - }; + use crate::component::storage::archetype::Id as ArchetypeId; + use crate::component::storage::Storage; use crate::uid::{Kind as UidKind, Uid}; - #[derive(Debug, Component)] - struct HealthPotion - { - _hp_restoration: u32, - } - - #[derive(Debug, Component)] - struct Hookshot - { - _range: u32, - } - - #[derive(Debug, Component)] - struct DekuNut - { - _throwing_damage: u32, - } - - #[derive(Debug, Component)] - struct Bow - { - _damage: u32, - } - - #[derive(Debug, Component)] - struct IronBoots; - #[test] - fn push_entity_works() + fn create_entity_works() { - let mut component_storage = Storage::default(); - - component_storage - .push_entity( - Uid::new_unique(UidKind::Entity), - vec![ - Box::new(HealthPotion { _hp_restoration: 12 }), - Box::new(Hookshot { _range: 50 }), - ], - ) - .expect("Expected Ok"); - - assert_eq!(component_storage.archetypes.len(), 1); - - let archetype = component_storage - .archetypes - .first() - .expect("Expected a archetype in archetypes Vec"); - - assert_eq!(archetype.component_ids.len(), 2); + let mut new_storage = Storage::default(); - // One entity - assert_eq!(archetype.entities.len(), 1); + let uid = Uid::new_unique(UidKind::Entity); - let entity_components = archetype - .entities - .first() - .expect("Expected a entity in archetype"); + new_storage.create_entity(uid).expect("Expected Ok"); - assert_eq!(entity_components.components.len(), 2); + let archetype_node = new_storage + .graph + .get_node_by_id(ArchetypeId::new_empty()) + .expect("Archetype for entities with no component doesn't exist"); - assert_eq!(component_storage.archetype_lookup.borrow().len(), 1); + assert_eq!(archetype_node.archetype().component_cnt(), 0); + assert_eq!(archetype_node.archetype().entity_cnt(), 1); - let mut components_metadata = [ - ComponentMetadata { - id: HealthPotion::id(), - is_optional: ComponentIsOptional::No, - }, - ComponentMetadata { - id: Hookshot::id(), - is_optional: ComponentIsOptional::No, - }, - ]; - - components_metadata.sort_by_key(|comp_metadata| comp_metadata.id); - - let archetype_lookup = component_storage.archetype_lookup.borrow(); - - let lookup_entry = archetype_lookup - .get(&ArchetypeId::from_components_metadata(&components_metadata)) - .expect("Expected entry in archetype lookup map"); - - let first_archetype_index = lookup_entry - .archetype_indices - .first() - .expect("Expected archetype lookup to contain a archetype reference"); - - assert_eq!(*first_archetype_index, 0); + assert_eq!( + new_storage.entity_archetype_lookup.get(&uid).copied(), + Some(ArchetypeId::new_empty()) + ); } } diff --git a/ecs/src/component/storage/archetype.rs b/ecs/src/component/storage/archetype.rs new file mode 100644 index 0000000..bb29701 --- /dev/null +++ b/ecs/src/component/storage/archetype.rs @@ -0,0 +1,387 @@ +use std::any::Any; +use std::array::IntoIter as ArrayIntoIter; +use std::hash::{DefaultHasher, Hash, Hasher}; +use std::iter::{Enumerate, Filter, Map, RepeatN, Zip}; +use std::option::IntoIter as OptionIntoIter; +use std::slice::Iter as SliceIter; + +use hashbrown::HashMap; + +use crate::lock::Lock; +use crate::uid::{Kind as UidKind, Uid}; +use crate::util::{Either, HashMapExt}; + +#[derive(Debug)] +pub struct Archetype +{ + id: Id, + entities: Vec<Entity>, + entity_index_lookup: HashMap<Uid, usize>, + component_index_lookup: HashMap<Uid, usize>, + component_ids: Vec<Uid>, +} + +impl Archetype +{ + pub fn new(id: Id, component_ids: impl AsRef<[Uid]>) -> Self + { + Self { + id, + entities: Vec::new(), + entity_index_lookup: HashMap::new(), + component_index_lookup: component_ids + .as_ref() + .iter() + .enumerate() + .map(|(index, id)| (*id, index)) + .collect(), + component_ids: component_ids.as_ref().to_vec(), + } + } + + pub fn id(&self) -> Id + { + self.id + } + + pub fn is_superset(&self, other: &Self) -> bool + { + self.component_index_lookup + .keys_is_superset(&other.component_index_lookup) + } + + pub fn is_subset(&self, other: &Self) -> bool + { + self.component_index_lookup + .keys_is_subset(&other.component_index_lookup) + } + + pub fn get_entity_by_id(&self, entity_uid: Uid) -> Option<&Entity> + { + let index = *self.entity_index_lookup.get(&entity_uid)?; + + Some(self.entities.get(index).unwrap_or_else(|| { + panic!( + "In invalid state! Index of entity with ID {entity_uid:?} is out of bounds" + ); + })) + } + + pub fn push_entity(&mut self, entity: Entity) + { + self.entity_index_lookup + .insert(entity.uid, self.entities.len()); + + self.entities.push(entity); + } + + pub fn remove_entity(&mut self, entity_uid: Uid) -> Option<Entity> + { + //debug_assert_eq!(entity_uid.kind(), UidKind::Entity); + + let entity_index = self.entity_index_lookup.remove(&entity_uid)?; + + if self.entities.len() == 1 { + return Some(self.entities.remove(entity_index)); + } + + let last_entity_uid = self + .entities + .last() + .expect(concat!( + "Invalid state. No entities in archetype but entry was ", + "removed successfully from entity index lookup" + )) + .uid; + + // By using swap_remove, no memory reallocation occurs and only one index in the + // entity lookup needs to be updated + let removed_entity = self.entities.swap_remove(entity_index); + + self.entity_index_lookup + .insert(last_entity_uid, entity_index); + + Some(removed_entity) + } + + pub fn entities(&self) -> EntityIter<'_> + { + EntityIter { iter: self.entities.iter() } + } + + pub fn entity_cnt(&self) -> usize + { + self.entities.len() + } + + pub fn component_cnt(&self) -> usize + { + self.component_index_lookup.len() + } + + pub fn get_matching_component_indices( + &self, + component_id: Uid, + ) -> MatchingComponentIter + { + assert!( + component_id.kind() == UidKind::Component + || component_id.kind() == UidKind::Pair + ); + + if component_id.kind() == UidKind::Pair + && component_id.target_component() == Uid::wildcard() + { + return MatchingComponentIter { + inner: Either::A( + self.component_ids + .iter() + .enumerate() + .zip(std::iter::repeat_n(component_id, self.component_ids.len())) + .filter( + (|((_, other_comp_id), component_id)| { + other_comp_id.kind() == UidKind::Pair + && other_comp_id.has_same_relation_as(*component_id) + }) + as MatchingComponentIterFilterFn, + ) + .map(|((index, other_comp_id), _)| (*other_comp_id, index)), + ), + }; + } + + MatchingComponentIter { + inner: Either::B( + [component_id] + .into_iter() + .zip(self.get_index_for_component(component_id)), + ), + } + } + + pub fn get_index_for_component(&self, component_id: Uid) -> Option<usize> + { + assert!( + component_id.kind() == UidKind::Component + || (component_id.kind() == UidKind::Pair + && component_id.target_component() != Uid::wildcard()) + ); + + self.component_index_lookup.get(&component_id).copied() + } + + pub fn component_ids_unsorted(&self) -> impl Iterator<Item = Uid> + '_ + { + self.component_index_lookup.keys().copied() + } + + pub fn component_ids_sorted(&self) -> impl Iterator<Item = Uid> + '_ + { + self.component_ids.iter().copied() + } + + pub fn contains_matching_component(&self, component_id: Uid) -> bool + { + let component_id_kind = component_id.kind(); + + debug_assert!( + component_id_kind == UidKind::Component || component_id_kind == UidKind::Pair + ); + + if component_id.kind() == UidKind::Pair + && component_id.target_component() == Uid::wildcard() + { + return self.component_ids.iter().any(|other_comp_id| { + other_comp_id.kind() == UidKind::Pair + && other_comp_id.has_same_relation_as(component_id) + }); + } + + self.contains_component_with_exact_id(component_id) + } + + pub fn contains_component_with_exact_id(&self, component_id: Uid) -> bool + { + let component_id_kind = component_id.kind(); + + debug_assert!( + component_id_kind == UidKind::Component + || (component_id_kind == UidKind::Pair + && component_id.target_component() != Uid::wildcard()) + ); + + self.component_index_lookup.contains_key(&component_id) + } +} + +type MatchingComponentIterFilterFn = fn(&((usize, &Uid), Uid)) -> bool; + +type MatchingComponentIterMapFn = fn(((usize, &Uid), Uid)) -> (Uid, usize); + +type InnerMatchingComponentIterA<'archetype> = Map< + Filter< + Zip<Enumerate<SliceIter<'archetype, Uid>>, RepeatN<Uid>>, + MatchingComponentIterFilterFn, + >, + MatchingComponentIterMapFn, +>; + +type InnerMatchingComponentIterB = Zip<ArrayIntoIter<Uid, 1>, OptionIntoIter<usize>>; + +#[derive(Debug)] +pub struct MatchingComponentIter<'archetype> +{ + inner: Either<InnerMatchingComponentIterA<'archetype>, InnerMatchingComponentIterB>, +} + +impl Iterator for MatchingComponentIter<'_> +{ + type Item = (Uid, usize); + + fn next(&mut self) -> Option<Self::Item> + { + self.inner.next() + } +} + +#[derive(Debug)] +pub struct EntityIter<'archetype> +{ + iter: SliceIter<'archetype, Entity>, +} + +impl<'archetype> Iterator for EntityIter<'archetype> +{ + type Item = &'archetype Entity; + + fn next(&mut self) -> Option<Self::Item> + { + self.iter.next() + } +} + +#[derive(Debug)] +pub struct Entity +{ + uid: Uid, + components: Vec<EntityComponent>, +} + +impl Entity +{ + pub fn new(uid: Uid, components: impl IntoIterator<Item = EntityComponent>) -> Self + { + Self { + uid, + components: components.into_iter().collect(), + } + } + + pub fn uid(&self) -> Uid + { + self.uid + } + + pub fn components(&self) -> &[EntityComponent] + { + &self.components + } + + pub fn remove_component(&mut self, component_id: Uid, archetype: &Archetype) + { + let index = archetype + .get_index_for_component(component_id) + .expect("Archetype should contain component"); + + self.components.remove(index); + } + + pub fn insert_component( + &mut self, + component_id: Uid, + component: EntityComponent, + archetype: &Archetype, + ) + { + let index = archetype + .get_index_for_component(component_id) + .expect("Archetype should contain component"); + + self.components.insert(index, component); + } +} + +#[derive(Debug)] +pub struct EntityComponent +{ + id: Uid, + component: Lock<Box<dyn Any>>, +} + +impl EntityComponent +{ + pub fn new( + component: Box<dyn Any>, + component_id: Uid, + component_name: &'static str, + ) -> Self + { + Self { + id: component_id, + component: Lock::new(component, component_name), + } + } + + #[allow(dead_code)] + pub fn id(&self) -> Uid + { + self.id + } + + pub fn component(&self) -> &Lock<Box<dyn Any>> + { + &self.component + } +} + +/// Archetype ID. +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct Id +{ + hash: u64, +} + +impl Id +{ + pub fn new_empty() -> Self + { + Self { hash: 0 } + } + + pub fn new<'a>(component_ids: impl IntoIterator<Item = &'a Uid>) -> Self + { + let mut hasher = DefaultHasher::new(); + + let mut prev_component_id: Option<Uid> = None; + + let mut component_id_iter = component_ids.into_iter().peekable(); + + if component_id_iter.peek().is_none() { + return Self::new_empty(); + } + + for comp_id in component_id_iter { + if prev_component_id.is_some_and(|prev_comp_id| *comp_id < prev_comp_id) { + panic!( + "Cannot create archetype ID from a unsorted component metadata list" + ); + } + + prev_component_id = Some(*comp_id); + + comp_id.hash(&mut hasher); + } + + Self { hash: hasher.finish() } + } +} diff --git a/ecs/src/component/storage/graph.rs b/ecs/src/component/storage/graph.rs new file mode 100644 index 0000000..29fa937 --- /dev/null +++ b/ecs/src/component/storage/graph.rs @@ -0,0 +1,432 @@ +use std::vec::IntoIter as VecIntoIter; + +use hashbrown::{HashMap, HashSet}; + +use crate::component::storage::archetype::{Archetype, Id as ArchetypeId}; +use crate::uid::{Kind as UidKind, Uid}; +use crate::util::{BorrowedOrOwned, StreamingIterator}; + +#[derive(Debug, Default)] +pub struct Graph +{ + nodes: Vec<ArchetypeNode>, + archetype_index_lookup: HashMap<ArchetypeId, usize>, +} + +impl Graph +{ + pub fn create_node(&mut self, id: ArchetypeId, component_ids: &impl AsRef<[Uid]>) + { + debug_assert!(!self.contains_archetype(id)); + + let _ = self.get_or_create_node(id, component_ids); + } + + pub fn get_or_create_node( + &mut self, + id: ArchetypeId, + component_ids: &impl AsRef<[Uid]>, + ) -> &mut ArchetypeNode + { + let exists_before = self.archetype_index_lookup.contains_key(&id); + + let index = *self.archetype_index_lookup.entry(id).or_insert_with(|| { + self.nodes.push(ArchetypeNode { + archetype: Archetype::new(id, component_ids.as_ref()), + edges: HashMap::new(), + }); + + self.nodes.len() - 1 + }); + + if !exists_before { + self.create_missing_edges(id); + } + + self.nodes + .get_mut(index) + .expect("Archetype index from lookup is out of bounds") + } + + pub fn contains_archetype(&self, id: ArchetypeId) -> bool + { + self.archetype_index_lookup.contains_key(&id) + } + + pub fn get_node_by_id(&self, id: ArchetypeId) -> Option<&ArchetypeNode> + { + let index = self.archetype_index_lookup.get(&id)?; + + Some(self.nodes.get(*index).unwrap_or_else(|| { + panic!("In invalid state! Index of archetype with ID {id:?} is out of bounds") + })) + } + + pub fn get_node_by_id_mut(&mut self, id: ArchetypeId) -> Option<&mut ArchetypeNode> + { + let index = self.archetype_index_lookup.get(&id)?; + + Some(self.nodes.get_mut(*index).unwrap_or_else(|| { + panic!("In invalid state! Index of archetype with ID {id:?} is out of bounds") + })) + } + + #[cfg(feature = "vizoxide")] + pub fn iter_nodes(&self) -> impl Iterator<Item = &ArchetypeNode> + { + self.nodes.iter() + } + + pub fn dfs_archetype_add_edges( + &self, + archetype_id: ArchetypeId, + ) -> Option<ArchetypeAddEdgeDfsIter> + { + let node = self.get_node_by_id(archetype_id)?; + + Some(ArchetypeAddEdgeDfsIter { + graph: self, + stack: vec![( + BorrowedOrOwned::Borrowned(node.archetype()), + node.edges + .iter() + .map(|(comp_id, edges)| (*comp_id, edges.clone())) + .collect::<Vec<_>>() + .into_iter(), + )], + visited: HashSet::new(), + }) + } + + fn create_missing_edges(&mut self, archetype_id: ArchetypeId) + { + let archetype_node_index = *self + .archetype_index_lookup + .get(&archetype_id) + .expect("Archetype should exist"); + + let (nodes_before, nodes_rest) = self.nodes.split_at_mut(archetype_node_index); + + let ([archetype_node], nodes_after) = nodes_rest.split_at_mut(1) else { + unreachable!(); + }; + + for other_archetype_node in nodes_before.iter_mut().chain(nodes_after.iter_mut()) + { + if archetype_node.archetype().component_cnt() + > other_archetype_node.archetype().component_cnt() + && other_archetype_node + .archetype() + .is_subset(archetype_node.archetype()) + { + Self::create_missing_subset_node_edges( + archetype_node, + other_archetype_node, + ); + + continue; + } + + if other_archetype_node + .archetype() + .is_superset(archetype_node.archetype()) + { + Self::create_missing_superset_node_edges( + archetype_node, + other_archetype_node, + ); + } + } + } + + fn create_missing_subset_node_edges( + target_node: &mut ArchetypeNode, + subset_node: &mut ArchetypeNode, + ) + { + let uniq_comp_id = target_node + .archetype() + .component_ids_sorted() + .find(|id| { + !subset_node + .archetype() + .contains_component_with_exact_id(*id) + }) + .unwrap(); + + subset_node + .get_or_insert_edges(uniq_comp_id, ArchetypeEdges::default) + .add = Some(subset_node.make_add_edge(uniq_comp_id).0); + + if target_node.archetype().component_cnt() + == subset_node.archetype().component_cnt() + 1 + { + target_node + .get_or_insert_edges(uniq_comp_id, ArchetypeEdges::default) + .remove = Some(subset_node.archetype().id()); + } + } + + fn create_missing_superset_node_edges( + target_node: &mut ArchetypeNode, + superset_node: &mut ArchetypeNode, + ) + { + if superset_node.archetype().component_cnt() + > target_node.archetype().component_cnt() + 1 + { + let first_unique_comp_id = superset_node + .archetype() + .component_ids_sorted() + .find(|other_archetype_comp_id| { + !target_node + .archetype() + .contains_component_with_exact_id(*other_archetype_comp_id) + }) + .or_else(|| { + if target_node.archetype().component_cnt() != 0 { + return None; + } + + superset_node.archetype().component_ids_sorted().next() + }) + .expect("Not possible"); + + target_node + .get_or_insert_edges(first_unique_comp_id, ArchetypeEdges::default) + .add = Some(target_node.make_add_edge(first_unique_comp_id).0); + + return; + } + + if superset_node.archetype().component_cnt() + != target_node.archetype().component_cnt() + 1 + { + return; + } + + let extra_comp_id = superset_node + .archetype() + .component_ids_unsorted() + .find(|comp_id| { + !target_node + .archetype() + .contains_component_with_exact_id(*comp_id) + }) + .expect("Archetype should contain one extra component ID"); + + superset_node + .get_or_insert_edges(extra_comp_id, ArchetypeEdges::default) + .remove = Some(target_node.archetype().id()); + + target_node + .get_or_insert_edges(extra_comp_id, ArchetypeEdges::default) + .add = Some(superset_node.archetype().id()); + } +} + +#[derive(Debug)] +pub struct ArchetypeNode +{ + archetype: Archetype, + edges: HashMap<Uid, ArchetypeEdges>, +} + +impl ArchetypeNode +{ + pub fn archetype(&self) -> &Archetype + { + &self.archetype + } + + pub fn archetype_mut(&mut self) -> &mut Archetype + { + &mut self.archetype + } + + pub fn get_or_insert_edges( + &mut self, + component_id: Uid, + insert_fn: impl FnOnce() -> ArchetypeEdges, + ) -> &mut ArchetypeEdges + { + debug_assert!(matches!( + component_id.kind(), + UidKind::Component | UidKind::Pair + )); + + self.edges.entry(component_id).or_insert_with(insert_fn) + } + + #[cfg(feature = "vizoxide")] + pub fn iter_edges(&self) -> impl Iterator<Item = (&Uid, &ArchetypeEdges)> + { + self.edges.iter() + } + + pub fn make_add_edge(&self, component_id: Uid) -> (ArchetypeId, Vec<Uid>) + { + let mut edge_comp_ids = self + .archetype() + .component_ids_unsorted() + .chain([component_id]) + .collect::<Vec<_>>(); + + edge_comp_ids.sort(); + + let add_edge_id = ArchetypeId::new(&edge_comp_ids); + + (add_edge_id, edge_comp_ids) + } + + pub fn make_remove_edge(&self, component_id: Uid) -> (ArchetypeId, Vec<Uid>) + { + let mut edge_comp_ids = self + .archetype() + .component_ids_unsorted() + .filter(|id| *id != component_id) + .collect::<Vec<_>>(); + + edge_comp_ids.sort(); + + let remove_edge_id = ArchetypeId::new(&edge_comp_ids); + + (remove_edge_id, edge_comp_ids) + } +} + +#[derive(Debug, Default, Clone)] +pub struct ArchetypeEdges +{ + pub add: Option<ArchetypeId>, + pub remove: Option<ArchetypeId>, +} + +type ArchetypeAddEdgeDfsIterStackElem<'graph> = ( + BorrowedOrOwned<'graph, Archetype>, + VecIntoIter<(Uid, ArchetypeEdges)>, +); + +#[derive(Debug)] +pub struct ArchetypeAddEdgeDfsIter<'graph> +{ + graph: &'graph Graph, + stack: Vec<ArchetypeAddEdgeDfsIterStackElem<'graph>>, + visited: HashSet<ArchetypeId>, +} + +impl<'graph> ArchetypeAddEdgeDfsIter<'graph> +{ + pub fn new(graph: &'graph Graph, start_nodes: &[ArchetypeId]) -> Self + { + Self { + graph, + stack: start_nodes + .iter() + .map(|start_node_id| { + let start_node = graph + .get_node_by_id(*start_node_id) + .expect("Start node does not exist"); + + ( + BorrowedOrOwned::Borrowned(start_node.archetype()), + start_node + .edges + .iter() + .map(|(comp_id, edges)| (*comp_id, edges.clone())) + .collect::<Vec<_>>() + .into_iter(), + ) + }) + .collect(), + visited: start_nodes.iter().copied().collect::<HashSet<_>>(), + } + } + + pub fn push( + &mut self, + item: ( + BorrowedOrOwned<'graph, Archetype>, + VecIntoIter<(Uid, ArchetypeEdges)>, + ), + ) + { + self.stack.push(item); + } + + pub fn pop(&mut self) + { + self.stack.pop(); + } +} + +impl<'graph> StreamingIterator for ArchetypeAddEdgeDfsIter<'graph> +{ + type Item<'a> + = ArchetypeAddEdgeDfsIterResult<'graph, 'a> + where + Self: 'a; + + fn streaming_next(&mut self) -> Option<Self::Item<'_>> + { + let (_, edges_iter) = self.stack.last_mut()?; + + let Some((component_id, edges)) = edges_iter.next() else { + self.stack.pop(); + + return Some(ArchetypeAddEdgeDfsIterResult::NoEdgesLeftForArchetype); + }; + + let Some(add_edge) = edges.add else { + return Some(ArchetypeAddEdgeDfsIterResult::NoAddEdge); + }; + + if self.visited.contains(&add_edge) { + return Some(ArchetypeAddEdgeDfsIterResult::AddEdgeAlreadyVisited); + } + + self.visited.insert(add_edge); + + let Some(add_edge_archetype) = self.graph.get_node_by_id(add_edge) else { + return Some(ArchetypeAddEdgeDfsIterResult::AddEdgeArchetypeNotFound { + archetype: &self.stack.last().unwrap().0, + add_edge_archetype_id: add_edge, + add_edge_component_id: component_id, + }); + }; + + self.stack.push(( + BorrowedOrOwned::Borrowned(add_edge_archetype.archetype()), + add_edge_archetype + .edges + .iter() + .map(|(comp_id, edges)| (*comp_id, edges.clone())) + .collect::<Vec<_>>() + .into_iter(), + )); + + Some(ArchetypeAddEdgeDfsIterResult::AddEdge { + add_edge_archetype_id: add_edge, + add_edge_component_id: component_id, + }) + } +} + +#[derive(Debug)] +pub enum ArchetypeAddEdgeDfsIterResult<'graph, 'iter> +{ + AddEdge + { + add_edge_archetype_id: ArchetypeId, + add_edge_component_id: Uid, + }, + NoEdgesLeftForArchetype, + NoAddEdge, + AddEdgeAlreadyVisited, + AddEdgeArchetypeNotFound + { + archetype: &'iter BorrowedOrOwned<'graph, Archetype>, + add_edge_archetype_id: ArchetypeId, + add_edge_component_id: Uid, + }, +} diff --git a/ecs/src/entity.rs b/ecs/src/entity.rs index 3de9cd5..bab3d61 100644 --- a/ecs/src/entity.rs +++ b/ecs/src/entity.rs @@ -1,6 +1,133 @@ +use std::any::type_name; + use linkme::distributed_slice; -use crate::World; +use crate::component::storage::archetype::{ + Archetype, + Entity as ArchetypeEntity, + MatchingComponentIter as ArchetypeMatchingComponentIter, +}; +use crate::component::{ + Component, + Handle as ComponentHandle, + HandleMut as ComponentHandleMut, +}; +use crate::uid::{Kind as UidKind, Uid}; +use crate::{EntityComponentRef, World}; + +/// A handle to a entity. +#[derive(Debug)] +pub struct Handle<'a> +{ + archetype: &'a Archetype, + entity: &'a ArchetypeEntity, +} + +impl<'a> Handle<'a> +{ + /// Returns the [`Uid`] of this entity. + #[inline] + #[must_use] + pub fn uid(&self) -> Uid + { + self.entity.uid() + } + + /// Returns a reference to the specified component in this entity. `None` is + /// returned if the component isn't found in the entity. + /// + /// # Panics + /// Will panic if: + /// - The component's ID is not a component ID + /// - The component is mutably borrowed elsewhere + #[must_use] + pub fn get<ComponentT: Component>(&self) -> Option<ComponentHandle<'_, ComponentT>> + { + assert_eq!(ComponentT::id().kind(), UidKind::Component); + + let component = self.get_matching_components(ComponentT::id()).next()?; + + Some( + ComponentHandle::from_entity_component_ref(component).unwrap_or_else(|err| { + panic!( + "Taking component {} lock failed: {err}", + type_name::<ComponentT>() + ); + }), + ) + } + + /// Returns a mutable reference to the specified component in this entity. `None` is + /// returned if the component isn't found in the entity. + /// + /// # Panics + /// Will panic if: + /// - The component's ID is not a component ID + /// - The component is borrowed elsewhere + #[must_use] + pub fn get_mut<ComponentT: Component>( + &self, + ) -> Option<ComponentHandleMut<'_, ComponentT>> + { + assert_eq!(ComponentT::id().kind(), UidKind::Component); + + let component = self.get_matching_components(ComponentT::id()).next()?; + + Some( + ComponentHandleMut::from_entity_component_ref(component).unwrap_or_else( + |err| { + panic!( + "Taking component {} lock failed: {err}", + type_name::<ComponentT>() + ); + }, + ), + ) + } + + #[inline] + #[must_use] + pub fn get_matching_components(&self, component_uid: Uid) + -> MatchingComponentIter<'a> + { + MatchingComponentIter { + inner: self.archetype.get_matching_component_indices(component_uid), + entity: self.entity, + } + } + + pub fn component_ids(&self) -> impl Iterator<Item = Uid> + '_ + { + self.archetype.component_ids_sorted() + } + + pub(crate) fn new(archetype: &'a Archetype, entity: &'a ArchetypeEntity) -> Self + { + Self { archetype, entity } + } +} + +#[derive(Debug)] +pub struct MatchingComponentIter<'a> +{ + inner: ArchetypeMatchingComponentIter<'a>, + entity: &'a ArchetypeEntity, +} + +impl<'a> Iterator for MatchingComponentIter<'a> +{ + type Item = EntityComponentRef<'a>; + + fn next(&mut self) -> Option<Self::Item> + { + let (matching_component_id, index) = self.inner.next()?; + + Some(EntityComponentRef::new( + matching_component_id, + self.entity.components().get(index).unwrap(), + )) + } +} #[allow(clippy::module_name_repetitions)] #[macro_export] @@ -19,7 +146,7 @@ macro_rules! static_entity { $crate::entity::CREATE_STATIC_ENTITIES )] #[linkme(crate=$crate::private::linkme)] - static CREATE_STATIC_ENTITY: fn(&$crate::World) = |world| { + static CREATE_STATIC_ENTITY: fn(&mut $crate::World) = |world| { world.create_entity_with_uid($components, *$ident); }; } @@ -29,4 +156,4 @@ macro_rules! static_entity { #[distributed_slice] #[doc(hidden)] -pub static CREATE_STATIC_ENTITIES: [fn(&World)]; +pub static CREATE_STATIC_ENTITIES: [fn(&mut World)]; diff --git a/ecs/src/event.rs b/ecs/src/event.rs index 1a4edcc..9cea807 100644 --- a/ecs/src/event.rs +++ b/ecs/src/event.rs @@ -1,96 +1 @@ -use std::any::TypeId; -use std::fmt::Debug; -use std::hash::{DefaultHasher, Hash, Hasher}; - -use seq_macro::seq; - pub mod component; - -pub trait Event: Debug + 'static -{ - /// Returns the ID of this event. - #[must_use] - fn id() -> Id - where - Self: Sized, - { - Id::new::<Self, ()>(None) - } -} - -pub mod start; - -/// The ID of a [`Event`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Id -{ - inner: TypeId, - extra: Option<u64>, -} - -impl Id -{ - #[must_use] - pub fn new<EventT, Extra>(extra: Option<Extra>) -> Self - where - EventT: Event, - Extra: Hash, - { - Self { - inner: TypeId::of::<EventT>(), - extra: extra.map(|extra| { - let mut hasher = DefaultHasher::new(); - - extra.hash(&mut hasher); - - hasher.finish() - }), - } - } -} - -pub trait Ids -{ - type Iter<'a>: Iterator<Item = &'a Id> - where - Self: 'a; - - fn iter(&self) -> Self::Iter<'_>; -} - -/// A sequence of events. -pub trait Sequence -{ - type Ids: Ids; - - fn ids() -> Self::Ids; -} - -macro_rules! impl_sequence { - ($c: tt) => { - seq!(I in 0..=$c { - impl Ids for [Id; $c + 1] - { - type Iter<'a> = std::slice::Iter<'a, Id>; - - fn iter(&self) -> Self::Iter<'_> { - self.into_iter() - } - } - - impl<#(Event~I: Event,)*> Sequence for (#(Event~I,)*) { - type Ids = [Id; $c + 1]; - - fn ids() -> Self::Ids { - [#( - Event~I::id(), - )*] - } - } - }); - }; -} - -seq!(C in 0..=64 { - impl_sequence!(C); -}); diff --git a/ecs/src/event/component.rs b/ecs/src/event/component.rs index 8b066a7..72a78a3 100644 --- a/ecs/src/event/component.rs +++ b/ecs/src/event/component.rs @@ -1,124 +1,12 @@ //! Component events. -use std::fmt::{Debug, Formatter}; -use std::marker::PhantomData; +use std::convert::Infallible; +use std::fmt::Debug; -use ecs_macros::Component; - -use crate::component::Component; -use crate::uid::Uid; -use crate::event::{Event, Id}; -use crate::tuple::{ReduceElement as TupleReduceElement, With as TupleWith}; - -/// Event emitted when: -/// a) A entity with component `ComponentT` is spawned. -/// b) A component `ComponentT` is added to a entity. -pub struct Added<ComponentT> -where - ComponentT: Component, -{ - _pd: PhantomData<ComponentT>, -} - -impl<ComponentT> Debug for Added<ComponentT> -where - ComponentT: Component, -{ - fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result - { - formatter - .debug_struct("Added") - .field("_pd", &self._pd) - .finish() - } -} - -impl<ComponentT> Default for Added<ComponentT> -where - ComponentT: Component, -{ - fn default() -> Self - { - Self { _pd: PhantomData } - } -} - -impl<ComponentT> Event for Added<ComponentT> -where - ComponentT: Component, -{ - fn id() -> Id - where - Self: Sized, - { - Id::new::<Added<ComponentForId>, _>(Some(ComponentT::id())) - } -} - -/// Event emitted when a `ComponentT` component is removed from a entity. -pub struct Removed<ComponentT> -where - ComponentT: Component, -{ - _pd: PhantomData<ComponentT>, -} - -impl<ComponentT> Debug for Removed<ComponentT> -where - ComponentT: Component, -{ - fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result - { - formatter - .debug_struct("Removed") - .field("_pd", &self._pd) - .finish() - } -} - -impl<ComponentT> Default for Removed<ComponentT> -where - ComponentT: Component, -{ - fn default() -> Self - { - Self { _pd: PhantomData } - } -} - -impl<ComponentT> Event for Removed<ComponentT> -where - ComponentT: Component, -{ - fn id() -> Id - where - Self: Sized, - { - Id::new::<Removed<ComponentForId>, _>(Some(ComponentT::id())) - } -} - -#[must_use] -pub fn create_added_id(component_id: Uid) -> Id -{ - Id::new::<Added<ComponentForId>, _>(Some(component_id)) -} - -#[must_use] -pub fn create_removed_id(component_id: Uid) -> Id -{ - Id::new::<Removed<ComponentForId>, _>(Some(component_id)) -} - -pub struct TypeTransformComponentsToAddedEvents; - -impl<ComponentT: Component, Accumulator> - TupleReduceElement<Accumulator, TypeTransformComponentsToAddedEvents> for ComponentT -where - Accumulator: TupleWith<Added<Self>>, -{ - type Return = Accumulator::With; -} +use crate::Component; +/// Pair relation for events emitted when: +/// a) A entity with the target component is spawned. +/// b) The target component is added to a entity. #[derive(Debug, Component)] -struct ComponentForId; +pub struct Added(Infallible); diff --git a/ecs/src/event/start.rs b/ecs/src/event/start.rs deleted file mode 100644 index 248dd50..0000000 --- a/ecs/src/event/start.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::event::Event; - -/// Start event. -#[derive(Debug, Clone, Copy)] -pub struct Start; - -impl Event for Start {} diff --git a/ecs/src/extension.rs b/ecs/src/extension.rs index a945d89..42ebef9 100644 --- a/ecs/src/extension.rs +++ b/ecs/src/extension.rs @@ -1,9 +1,7 @@ use crate::component::Sequence as ComponentSequence; -use crate::event::component::TypeTransformComponentsToAddedEvents; -use crate::event::{Event, Sequence as EventSequence}; use crate::sole::Sole; use crate::system::System; -use crate::tuple::Reduce as TupleReduce; +use crate::uid::Uid; use crate::{SoleAlreadyExistsError, World}; /// A collection of systems, entities & soles that can be added to a [`World`]. @@ -27,21 +25,19 @@ impl<'world> Collector<'world> } /// Adds a system to the [`World`]. - pub fn add_system<'this, EventT, SystemImpl>( + pub fn add_system<'this, SystemImpl>( &'this mut self, - event: EventT, + phase_euid: Uid, system: impl System<'this, SystemImpl>, - ) where - EventT: Event, + ) { - self.world.register_system(event, system); + self.world.register_system(phase_euid, system); } /// Adds a entity to the [`World`]. pub fn add_entity<Comps>(&mut self, components: Comps) where - Comps: ComponentSequence + TupleReduce<TypeTransformComponentsToAddedEvents>, - Comps::Out: EventSequence, + Comps: ComponentSequence, { self.world.create_entity(components); } diff --git a/ecs/src/lib.rs b/ecs/src/lib.rs index 78e526f..07b1cba 100644 --- a/ecs/src/lib.rs +++ b/ecs/src/lib.rs @@ -1,65 +1,75 @@ #![deny(clippy::all, clippy::pedantic)] -use std::any::{type_name, TypeId}; +use std::any::{type_name, Any, TypeId}; use std::cell::RefCell; -use std::collections::HashMap; use std::fmt::Debug; use std::mem::ManuallyDrop; +use std::rc::Rc; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use hashbrown::HashMap; + use crate::actions::Action; +use crate::component::storage::archetype::EntityComponent as ArchetypeEntityComponent; use crate::component::storage::Storage as ComponentStorage; -use crate::component::{Component, Sequence as ComponentSequence}; -use crate::entity::CREATE_STATIC_ENTITIES; -use crate::event::component::{ - create_added_id as create_component_added_event_id, - create_removed_id as create_component_removed_event_id, - TypeTransformComponentsToAddedEvents, +use crate::component::{ + Component, + IntoParts, + Parts as ComponentParts, + Removals as ComponentRemovals, + Sequence as ComponentSequence, }; -use crate::event::start::Start as StartEvent; -use crate::event::{Event, Id as EventId, Ids, Sequence as EventSequence}; +use crate::entity::{Handle as EntityHandle, CREATE_STATIC_ENTITIES}; +use crate::event::component::Added as ComponentAddedEvent; use crate::extension::{Collector as ExtensionCollector, Extension}; -use crate::lock::{Lock, WriteGuard}; -use crate::query::options::Options as QueryOptions; +use crate::lock::Lock; +use crate::pair::{ChildOf, DependsOn, Pair}; +use crate::phase::{Phase, START as START_PHASE}; +use crate::query::flexible::Query as FlexibleQuery; +use crate::query::term::Without; +use crate::query::{ + Iter as QueryIter, + TermWithFieldTuple as QueryTermWithFieldTuple, + TermWithoutFieldTuple as QueryTermWithoutFieldTuple, + Terms as QueryTerms, + TermsBuilderInterface, +}; use crate::sole::Sole; use crate::stats::Stats; -use crate::system::{System, TypeErased as TypeErasedSystem}; -use crate::tuple::Reduce as TupleReduce; -use crate::type_name::TypeName; -use crate::uid::{Kind as UidKind, Uid}; +use crate::system::{System, SystemComponent}; +use crate::uid::{Kind as UidKind, Uid, Wildcard}; pub mod actions; pub mod component; pub mod entity; pub mod event; pub mod extension; -pub mod lock; +pub mod pair; +pub mod phase; pub mod query; -pub mod relationship; pub mod sole; pub mod stats; pub mod system; pub mod tuple; -pub mod type_name; pub mod uid; +pub mod util; #[doc(hidden)] pub mod private; -mod archetype; -mod util; +mod lock; pub use ecs_macros::{Component, Sole}; pub use crate::query::Query; -#[derive(Debug, Default)] +#[derive(Debug)] pub struct World { - systems: Vec<TypeErasedSystem>, data: WorldData, stop: AtomicBool, + is_first_tick: AtomicBool, } impl World @@ -67,21 +77,25 @@ impl World #[must_use] pub fn new() -> Self { - let mut world = Self::default(); + let mut world = Self { + data: WorldData::default(), + stop: AtomicBool::new(false), + is_first_tick: AtomicBool::new(false), + }; world.add_sole(Stats::default()).ok(); + for create_static_entity in CREATE_STATIC_ENTITIES { + create_static_entity(&mut world); + } + world } /// Creates a new entity with the given components. - /// - /// # Panics - /// Will panic if mutable internal lock cannot be acquired. pub fn create_entity<Comps>(&mut self, components: Comps) -> Uid where - Comps: ComponentSequence + TupleReduce<TypeTransformComponentsToAddedEvents>, - Comps::Out: EventSequence, + Comps: ComponentSequence, { let entity_uid = Uid::new_unique(UidKind::Entity); @@ -90,31 +104,27 @@ impl World entity_uid } - #[cfg_attr(feature = "debug", tracing::instrument(skip_all))] + #[tracing::instrument(skip_all)] #[doc(hidden)] - pub fn create_entity_with_uid<Comps>(&self, components: Comps, entity_uid: Uid) + pub fn create_entity_with_uid<Comps>(&mut self, components: Comps, entity_uid: Uid) where - Comps: ComponentSequence + TupleReduce<TypeTransformComponentsToAddedEvents>, - Comps::Out: EventSequence, + Comps: ComponentSequence, { debug_assert_eq!(entity_uid.kind(), UidKind::Entity); - #[allow(unused_variables)] - if let Err(err) = self - .data - .component_storage - .write_nonblock() - .expect("Failed to acquire read-write component storage lock") - .push_entity(entity_uid, components.into_vec()) - { - #[cfg(feature = "debug")] - tracing::error!("Failed to create entity: {err}"); - + if let Err(err) = self.data.component_storage.create_entity(entity_uid) { + tracing::warn!("Failed to create entity: {err}"); return; - }; + } - for component_added_event_id in <Comps::Out as EventSequence>::ids().iter() { - self.emit_event_by_id(*component_added_event_id); + let added_component_ids = Self::add_entity_components( + entity_uid, + components.into_parts_array(), + &mut self.data.component_storage, + ); + + for comp_id in added_component_ids { + self.emit_event_by_id::<ComponentAddedEvent>(comp_id); } } @@ -129,28 +139,31 @@ impl World self.data.sole_storage.insert(sole) } - pub fn register_system<'this, EventT, SystemImpl>( + pub fn register_system<'this, SystemImpl>( &'this mut self, - event: EventT, + phase_euid: Uid, system: impl System<'this, SystemImpl>, - ) where - EventT: Event, + ) { - self.systems.push(system.into_type_erased()); - - self.data - .events - .entry(EventT::id()) - .or_default() - .push(self.systems.len() - 1); + self.create_entity(( + SystemComponent { system: system.into_type_erased() }, + Pair::new::<DependsOn>(phase_euid), + )); + } - drop(event); + pub fn register_observer_system<'this, SystemImpl>( + &'this mut self, + system: impl System<'this, SystemImpl>, + event: Pair<Uid, Uid>, + ) + { + self.create_entity(( + SystemComponent { system: system.into_type_erased() }, + event, + )); } /// Adds a extensions. - /// - /// # Panics - /// Will panic if mutable internal lock cannot be acquired. pub fn add_extension(&mut self, extension: impl Extension) { let extension_collector = ExtensionCollector::new(self); @@ -158,34 +171,177 @@ impl World extension.collect(extension_collector); } - /// Emits a event, running all systems listening to the event for each compatible - /// entity. - /// - /// # Panics - /// Will panic if a system has dissapeared. - pub fn emit<EventT>(&self, event: EventT) + pub fn query<FieldTerms, FieldlessTerms>(&self) -> Query<FieldTerms, FieldlessTerms> where - EventT: Event, + FieldTerms: QueryTermWithFieldTuple, + FieldlessTerms: QueryTermWithoutFieldTuple, { - self.emit_event_by_id(EventT::id()); - - drop(event); + Query::new(self) } - pub fn query<Comps, OptionsT>(&self) -> Query<Comps, OptionsT> - where - Comps: ComponentSequence, - OptionsT: QueryOptions, + pub fn flexible_query<const MAX_TERM_CNT: usize>( + &self, + terms: QueryTerms<MAX_TERM_CNT>, + ) -> FlexibleQuery<'_, MAX_TERM_CNT> { - Query::new(self) + FlexibleQuery::new(self, terms) } - /// Peforms the actions that have been queued up using [`Actions`]. - /// + /// Performs a single tick. /// # Panics - /// Will panic if a mutable internal lock cannot be acquired. - #[cfg_attr(feature = "debug", tracing::instrument(skip_all))] - pub fn perform_queued_actions(&self) + /// Will panic if mutable internal lock cannot be acquired. + pub fn step(&mut self) -> StepResult + { + if self.stop.load(Ordering::Relaxed) { + return StepResult::Stop; + } + + if self + .is_first_tick + .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + self.query_and_run_systems(*START_PHASE); + } + + self.perform_phases(); + + self.data.component_storage.create_imaginary_archetypes(); + + let prev_pending_removals = std::mem::take(&mut self.data.pending_removals); + + self.perform_queued_actions(); + + self.perform_removals(prev_pending_removals); + + if self.stop.load(Ordering::Relaxed) { + return StepResult::Stop; + } + + let mut stats_lock = self + .data + .sole_storage + .get::<Stats>() + .expect("No stats sole found") + .write_nonblock() + .expect("Failed to aquire read-write stats sole lock"); + + let stats = stats_lock + .downcast_mut::<Stats>() + .expect("Casting stats sole to Stats type failed"); + + stats.current_tick += 1; + + StepResult::Continue + } + + /// Starts a loop which calls [`Self::step`] until the world is stopped. + pub fn start_loop(&mut self) + { + while let StepResult::Continue = self.step() {} + } + + #[cfg(feature = "vizoxide")] + pub fn create_vizoxide_archetype_graph( + &self, + name: impl AsRef<str>, + ) -> Result<vizoxide::Graph, vizoxide::GraphvizError> + { + use std::borrow::Cow; + + use crate::component::storage::{ + VizoxideArchetypeGraphEdgeKind, + VizoxideArchetypeGraphParams, + }; + + self.data.component_storage.create_vizoxide_archetype_graph( + name, + VizoxideArchetypeGraphParams { + create_node_name: |archetype, _| { + Cow::Owned(format!( + "[{}]", + archetype + .component_ids_sorted() + .into_iter() + .map(|comp_id| comp_id.to_string()) + .collect::<Vec<_>>() + .join(", ") + )) + }, + create_node_cb: |_archetype, archetype_metadata, node_builder| { + if archetype_metadata.is_imaginary { + return node_builder.attribute("shape", "ellipse"); + } + + node_builder.attribute("shape", "box") + }, + create_edge_cb: |_, _, edge_kind, edge_builder| { + edge_builder.attribute( + "color", + match edge_kind { + VizoxideArchetypeGraphEdgeKind::Add => "green", + VizoxideArchetypeGraphEdgeKind::Remove => "red", + }, + ) + }, + }, + ) + } + + fn query_and_run_systems(&self, phase_euid: Uid) + { + let system_query = self.flexible_query( + QueryTerms::<2>::builder() + .with_required([ + SystemComponent::id(), + Pair::new::<DependsOn>(phase_euid).id(), + ]) + .build(), + ); + + for (system_component,) in + QueryIter::<(&SystemComponent,), _>::new(self, system_query.iter()) + { + // SAFETY: The world lives long enough + unsafe { + system_component.system.run(self); + } + } + } + + fn perform_child_phases(&self, parent_phase_euid: Uid) + { + let phase_query = self.flexible_query( + QueryTerms::<2>::builder() + .with_required([ + Phase::id(), + Pair::new::<ChildOf>(parent_phase_euid).id(), + ]) + .build(), + ); + + for child_phase_entity in &phase_query { + self.query_and_run_systems(child_phase_entity.uid()); + self.perform_child_phases(child_phase_entity.uid()); + } + } + + fn perform_phases(&self) + { + let phase_query = self.query::<(&Phase,), (Without<Pair<ChildOf, Wildcard>>,)>(); + + for (phase_entity_id, _) in phase_query.iter_with_euids() { + if phase_entity_id == *START_PHASE { + continue; + } + + self.query_and_run_systems(phase_entity_id); + self.perform_child_phases(phase_entity_id); + } + } + + #[tracing::instrument(skip_all)] + fn perform_queued_actions(&mut self) { let mut active_action_queue = match *self.data.action_queue.active_queue.borrow() { @@ -202,82 +358,64 @@ impl World let mut has_swapped_active_queue = false; + // TODO: Figure out a good way to handle situations where there are multiple + // AddComponents/RemoveComponents actions that affect the same entity. for action in active_action_queue.drain(..) { match action { Action::Spawn(components) => { - let mut component_storage_lock = self.lock_component_storage_rw(); - - let component_ids = components - .iter() - .map(|component| component.self_id()) - .collect::<Vec<_>>(); + let new_entity_uid = Uid::new_unique(UidKind::Entity); - #[allow(unused_variables)] - if let Err(err) = component_storage_lock - .push_entity(Uid::new_unique(UidKind::Entity), components) + if let Err(err) = + self.data.component_storage.create_entity(new_entity_uid) { - #[cfg(feature = "debug")] - tracing::error!("Failed to create entity: {err}"); - + tracing::warn!("Failed to create entity: {err}"); continue; } - drop(component_storage_lock); + let added_component_ids = Self::add_entity_components( + new_entity_uid, + components, + &mut self.data.component_storage, + ); if !has_swapped_active_queue { self.swap_event_queue(&mut has_swapped_active_queue); } - for component_id in component_ids { - self.emit_event_by_id(create_component_added_event_id( - component_id, - )); + for comp_id in added_component_ids { + self.emit_event_by_id::<ComponentAddedEvent>(comp_id); } } + Action::Despawn(entity_uid) => { + Self::schedule_removal( + &mut self.data.component_storage, + &mut self.data.pending_removals, + entity_uid, + PendingRemoval::Entity, + ); + } Action::AddComponents(entity_uid, components) => { - let mut component_storage_lock = self.lock_component_storage_rw(); - - let component_ids = components - .iter() - .map(|component| component.self_id()) - .collect::<Vec<_>>(); - - component_storage_lock - .add_components_to_entity(entity_uid, components); - - drop(component_storage_lock); + let added_component_ids = Self::add_entity_components( + entity_uid, + components, + &mut self.data.component_storage, + ); if !has_swapped_active_queue { self.swap_event_queue(&mut has_swapped_active_queue); } - for component_id in component_ids { - self.emit_event_by_id(create_component_added_event_id( - component_id, - )); + for comp_id in added_component_ids { + self.emit_event_by_id::<ComponentAddedEvent>(comp_id); } } - Action::RemoveComponents(entity_uid, components_metadata) => { - let mut component_storage_lock = self.lock_component_storage_rw(); - - component_storage_lock.remove_components_from_entity( + Action::RemoveComponents(entity_uid, component_ids) => { + Self::schedule_removal( + &mut self.data.component_storage, + &mut self.data.pending_removals, entity_uid, - components_metadata - .iter() - .map(|component_metadata| component_metadata.id), + PendingRemoval::Components(component_ids), ); - - drop(component_storage_lock); - - if !has_swapped_active_queue { - self.swap_event_queue(&mut has_swapped_active_queue); - } - - for component_metadata in components_metadata { - self.emit_event_by_id(create_component_removed_event_id( - component_metadata.id, - )); - } } Action::Stop => { self.stop.store(true, Ordering::Relaxed); @@ -286,60 +424,134 @@ impl World } } - /// A event loop which runs until a stop is issued with [`Flags::stop`]. Before the - /// loop begins, [`StartEvent`] is emitted. - /// - /// # Panics - /// Will panic if a internal lock cannot be acquired. - pub fn event_loop<EventSeq: EventSequence>(&self) + fn perform_removals(&mut self, removals: Vec<(Uid, PendingRemoval)>) { - for create_static_entity in CREATE_STATIC_ENTITIES { - create_static_entity(self); + for (entity_id, removal) in removals { + match removal { + PendingRemoval::Components(component_ids) => { + Self::remove_entity_components( + entity_id, + component_ids.into_iter().chain([ComponentRemovals::id()]), + &mut self.data.component_storage, + ); + } + PendingRemoval::Entity => { + if let Err(err) = self.data.component_storage.remove_entity(entity_id) + { + tracing::error!("Failed to remove entity {entity_id}: {err}"); + } + } + } } + } - self.emit(StartEvent); + #[tracing::instrument(skip(component_storage, pending_removals))] + fn schedule_removal( + component_storage: &mut ComponentStorage, + pending_removals: &mut Vec<(Uid, PendingRemoval)>, + entity_uid: Uid, + removal: PendingRemoval, + ) + { + let Some(ent_handle) = Self::get_entity(component_storage, entity_uid) else { + tracing::warn!("Cannot schedule removal. Entity does not exist"); + return; + }; - let event_seq = EventSeq::ids(); + let component_ids = match removal { + PendingRemoval::Components(ref component_ids) => component_ids, + PendingRemoval::Entity => &ent_handle.component_ids().collect::<Vec<_>>(), + }; - loop { - for event_id in event_seq.iter() { - self.emit_event_by_id(*event_id); - } + let Some(mut component_removals) = ent_handle.get_mut::<ComponentRemovals>() + else { + Self::add_entity_components( + entity_uid, + [ComponentRemovals::from_iter(component_ids.iter().copied()) + .into_parts()], + component_storage, + ); + + pending_removals.push((entity_uid, removal)); + + return; + }; + + component_removals.add_ids(component_ids.iter().copied()); - self.perform_queued_actions(); + drop(component_removals); - if self.stop.load(Ordering::Relaxed) { - break; + pending_removals.push((entity_uid, removal)); + } + + fn add_entity_components( + entity_uid: Uid, + components: impl IntoIterator<Item = ComponentParts>, + component_storage: &mut ComponentStorage, + ) -> Vec<Uid> + { + let component_iter = components.into_iter(); + + let mut added_component_ids = + Vec::<Uid>::with_capacity(component_iter.size_hint().0); + + for component_parts in component_iter { + let comp_id = component_parts.id(); + + if let Err(err) = component_storage.add_entity_component( + entity_uid, + (comp_id, component_parts.name(), component_parts.into_data()), + ) { + tracing::error!("Failed to add component to entity: {err}"); + continue; } - let mut stats_lock = self - .data - .sole_storage - .get::<Stats>() - .expect("No stats sole found") - .write_nonblock() - .expect("Failed to aquire read-write stats sole lock"); + added_component_ids.push(comp_id); + } + + added_component_ids + } + + fn remove_entity_components( + entity_uid: Uid, + component_ids: impl IntoIterator<Item = Uid>, + component_storage: &mut ComponentStorage, + ) -> Vec<Uid> + { + let component_id_iter = component_ids.into_iter(); + + let mut removed_component_ids = + Vec::<Uid>::with_capacity(component_id_iter.size_hint().0); - let stats = stats_lock - .downcast_mut::<Stats>() - .expect("Casting stats sole to Stats type failed"); + for component_id in component_id_iter { + if let Err(err) = + component_storage.remove_entity_component(entity_uid, component_id) + { + tracing::error!("Failed to remove component to entity: {err}"); + continue; + } - stats.current_tick += 1; + removed_component_ids.push(component_id); } + + removed_component_ids } - fn emit_event_by_id(&self, event_id: EventId) + fn emit_event_by_id<Event: Component>(&self, target: Uid) { - let Some(system_indices) = self.data.events.get(&event_id) else { + if target.kind() == UidKind::Pair { return; - }; + } - for system_index in system_indices { - let system = self.systems.get(*system_index).unwrap(); + let query = self.flexible_query( + QueryTerms::<2>::builder() + .with_required([SystemComponent::id(), Pair::new::<Event>(target).id()]) + .build(), + ); - // SAFETY: The world lives long enough + for (system,) in QueryIter::<(&SystemComponent,), _>::new(self, query.iter()) { unsafe { - system.run(self); + system.system.run(self); } } } @@ -356,45 +568,95 @@ impl World *has_swapped_active_queue = true; } - fn lock_component_storage_rw(&self) -> WriteGuard<'_, ComponentStorage> + fn get_entity( + component_storage: &mut ComponentStorage, + entity_uid: Uid, + ) -> Option<EntityHandle<'_>> { - self.data - .component_storage - .write_nonblock() - .expect("Failed to acquire read-write component storage lock") + let archetype = component_storage.get_entity_archetype(entity_uid)?; + + Some(EntityHandle::new( + archetype, + archetype + .get_entity_by_id(entity_uid) + .expect("Not possible"), + )) } } -#[derive(Debug, Default)] -pub struct WorldData +impl Default for World { - events: HashMap<EventId, Vec<usize>>, - component_storage: Arc<Lock<ComponentStorage>>, - sole_storage: SoleStorage, - action_queue: Arc<ActionQueue>, + fn default() -> Self + { + Self::new() + } +} + +/// The result of calling [`World::step`]. +pub enum StepResult +{ + /// Another step can be made. + Continue, + + /// The world have been stopped so no step can be made again. + Stop, } #[derive(Debug)] -#[non_exhaustive] -pub struct EntityComponent +struct WorldData { - pub id: Uid, - pub name: &'static str, - pub component: Lock<Box<dyn Component>>, + component_storage: ComponentStorage, + sole_storage: SoleStorage, + action_queue: Rc<ActionQueue>, + pending_removals: Vec<(Uid, PendingRemoval)>, } -impl From<Box<dyn Component>> for EntityComponent +impl Default for WorldData { - fn from(component: Box<dyn Component>) -> Self + fn default() -> Self { Self { - id: component.self_id(), - name: component.type_name(), - component: Lock::new(component), + component_storage: ComponentStorage::default(), + sole_storage: SoleStorage::default(), + action_queue: Rc::new(ActionQueue::default()), + pending_removals: Vec::new(), } } } +#[derive(Debug)] +enum PendingRemoval +{ + Components(Vec<Uid>), + Entity, +} + +#[derive(Debug)] +pub struct EntityComponentRef<'a> +{ + component_id: Uid, + component: &'a ArchetypeEntityComponent, +} + +impl<'a> EntityComponentRef<'a> +{ + fn component(&self) -> &'a Lock<Box<dyn Any>> + { + self.component.component() + } + + #[must_use] + pub fn id(&self) -> Uid + { + self.component_id + } + + fn new(component_id: Uid, comp: &'a ArchetypeEntityComponent) -> Self + { + Self { component_id, component: comp } + } +} + #[derive(Debug, Default, Clone, Copy)] enum ActiveActionQueue { @@ -403,7 +665,7 @@ enum ActiveActionQueue B, } -#[derive(Debug, Default)] +#[derive(Debug)] struct ActionQueue { queue_a: Lock<Vec<Action>>, @@ -430,11 +692,15 @@ impl ActionQueue } } -impl TypeName for ActionQueue +impl Default for ActionQueue { - fn type_name(&self) -> &'static str + fn default() -> Self { - type_name::<Self>() + Self { + queue_a: Lock::new(Vec::new(), type_name::<Vec<Action>>()), + queue_b: Lock::new(Vec::new(), type_name::<Vec<Action>>()), + active_queue: RefCell::new(ActiveActionQueue::default()), + } } } @@ -479,7 +745,7 @@ impl SoleStorage self.storage.insert( sole_type_id, ManuallyDrop::new(StoredSole { - sole: Arc::new(Lock::new(Box::new(sole))), + sole: Arc::new(Lock::new(Box::new(sole), type_name::<SoleT>())), drop_last, }), ); @@ -496,34 +762,16 @@ impl Drop for SoleStorage for sole in self.storage.values_mut() { if sole.drop_last { - #[cfg(feature = "debug")] - tracing::debug!( - "Sole {} pushed to dropping last queue", - sole.sole.read_nonblock().unwrap().type_name() - ); - soles_to_drop_last.push(sole); continue; } - #[cfg(feature = "debug")] - tracing::debug!( - "Dropping sole {}", - sole.sole.read_nonblock().unwrap().type_name() - ); - unsafe { ManuallyDrop::drop(sole); } } for sole in &mut soles_to_drop_last { - #[cfg(feature = "debug")] - tracing::debug!( - "Dropping sole {} last", - sole.sole.read_nonblock().unwrap().type_name() - ); - unsafe { ManuallyDrop::drop(sole); } diff --git a/ecs/src/lock.rs b/ecs/src/lock.rs index fbc6842..0b36922 100644 --- a/ecs/src/lock.rs +++ b/ecs/src/lock.rs @@ -1,24 +1,29 @@ -use std::mem::transmute; +use std::mem::forget; use std::ops::{Deref, DerefMut}; -use std::sync::{PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard, TryLockError}; -use crate::type_name::TypeName; +use parking_lot::{ + MappedRwLockReadGuard, + MappedRwLockWriteGuard, + RwLock, + RwLockReadGuard, + RwLockWriteGuard, +}; -#[derive(Debug, Default)] +#[derive(Debug)] pub struct Lock<Value> -where - Value: TypeName, { inner: RwLock<Value>, + value_type_name: &'static str, } impl<Value> Lock<Value> -where - Value: TypeName, { - pub fn new(value: Value) -> Self + pub fn new(value: Value, value_type_name: &'static str) -> Self { - Self { inner: RwLock::new(value) } + Self { + inner: RwLock::new(value), + value_type_name, + } } /// Tries to a acquire a handle to the resource with read access. @@ -27,15 +32,14 @@ where /// Returns `Err` if unavailable (A mutable handle is hold). pub fn read_nonblock(&self) -> Result<ReadGuard<Value>, Error> { - let guard = self.inner.try_read().or_else(|err| match err { - TryLockError::WouldBlock => Err(Error::Unavailable), - TryLockError::Poisoned(poison_err) => Ok(poison_err.into_inner()), - })?; + let guard = self.inner.try_read().ok_or(Error::ReadUnavailable)?; - #[cfg(feature = "debug")] - tracing::trace!("Acquired lock to value of type {}", guard.type_name()); + tracing::trace!("Acquired lock to value of type {}", self.value_type_name); - Ok(ReadGuard { inner: guard }) + Ok(ReadGuard { + inner: guard, + value_type_name: self.value_type_name, + }) } /// Tries to a acquire a handle to the resource with mutable access. @@ -44,62 +48,59 @@ where /// Returns `Err` if unavailable (A mutable or immutable handle is hold). pub fn write_nonblock(&self) -> Result<WriteGuard<Value>, Error> { - let guard = self.inner.try_write().or_else(|err| match err { - TryLockError::WouldBlock => Err(Error::Unavailable), - TryLockError::Poisoned(poison_err) => Ok(poison_err.into_inner()), - })?; + let guard = self.inner.try_write().ok_or(Error::WriteUnavailable)?; - #[cfg(feature = "debug")] tracing::trace!( "Acquired mutable lock to value of type {}", - guard.type_name() + self.value_type_name ); - Ok(WriteGuard { inner: guard }) - } - - pub fn into_inner(self) -> Value - { - self.inner - .into_inner() - .unwrap_or_else(PoisonError::into_inner) + Ok(WriteGuard { + inner: guard, + value_type_name: self.value_type_name, + }) } } #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("Lock is unavailable")] - Unavailable, + #[error("Lock is unavailable for reading")] + ReadUnavailable, + + #[error("Lock is unavailable for writing")] + WriteUnavailable, } #[derive(Debug)] pub struct ReadGuard<'guard, Value> -where - Value: TypeName, { inner: RwLockReadGuard<'guard, Value>, + value_type_name: &'static str, } impl<'guard, Value> ReadGuard<'guard, Value> -where - Value: TypeName, { - /// Converts the `ReadGuard` to a `ReadGuard` with a possibly longer lifetime. - /// - /// # Safety - /// The returned `ReadGuard` must **NOT** be used for longer than the original - /// lifetime. - #[must_use] - pub unsafe fn upgrade_lifetime<'new>(self) -> ReadGuard<'new, Value> + pub fn map<NewValue>( + self, + func: impl FnOnce(&Value) -> &NewValue, + ) -> MappedReadGuard<'guard, NewValue> { - unsafe { transmute(self) } + let value_type_name = self.value_type_name; + + // The 'inner' field cannot be moved out of ReadGuard in a normal way since + // ReadGuard implements Drop + let inner = unsafe { std::ptr::read(&self.inner) }; + forget(self); + + MappedReadGuard { + inner: RwLockReadGuard::map(inner, func), + value_type_name, + } } } -impl<'guard, Value> Deref for ReadGuard<'guard, Value> -where - Value: TypeName, +impl<Value> Deref for ReadGuard<'_, Value> { type Target = Value; @@ -109,28 +110,71 @@ where } } -impl<'guard, Value> Drop for ReadGuard<'guard, Value> -where - Value: TypeName, +impl<Value> Drop for ReadGuard<'_, Value> { fn drop(&mut self) { - #[cfg(feature = "debug")] - tracing::trace!("Dropped lock to value of type {}", self.type_name()); + tracing::trace!("Dropped lock to value of type {}", self.value_type_name); + } +} + +#[derive(Debug)] +pub struct MappedReadGuard<'guard, Value> +{ + inner: MappedRwLockReadGuard<'guard, Value>, + value_type_name: &'static str, +} + +impl<Value> Deref for MappedReadGuard<'_, Value> +{ + type Target = Value; + + fn deref(&self) -> &Self::Target + { + &self.inner + } +} + +impl<Value> Drop for MappedReadGuard<'_, Value> +{ + fn drop(&mut self) + { + tracing::trace!( + "Dropped mapped lock to value of type {}", + self.value_type_name + ); } } #[derive(Debug)] pub struct WriteGuard<'guard, Value> -where - Value: TypeName, { inner: RwLockWriteGuard<'guard, Value>, + value_type_name: &'static str, +} + +impl<'guard, Value> WriteGuard<'guard, Value> +{ + pub fn map<NewValue>( + self, + func: impl FnOnce(&mut Value) -> &mut NewValue, + ) -> MappedWriteGuard<'guard, NewValue> + { + let value_type_name = self.value_type_name; + + // The 'inner' field cannot be moved out of ReadGuard in a normal way since + // ReadGuard implements Drop + let inner = unsafe { std::ptr::read(&self.inner) }; + forget(self); + + MappedWriteGuard { + inner: RwLockWriteGuard::map(inner, func), + value_type_name, + } + } } -impl<'guard, Value> Deref for WriteGuard<'guard, Value> -where - Value: TypeName, +impl<Value> Deref for WriteGuard<'_, Value> { type Target = Value; @@ -140,9 +184,7 @@ where } } -impl<'guard, Value> DerefMut for WriteGuard<'guard, Value> -where - Value: TypeName, +impl<Value> DerefMut for WriteGuard<'_, Value> { fn deref_mut(&mut self) -> &mut Self::Target { @@ -150,13 +192,49 @@ where } } -impl<'guard, Value> Drop for WriteGuard<'guard, Value> -where - Value: TypeName, +impl<Value> Drop for WriteGuard<'_, Value> { fn drop(&mut self) { - #[cfg(feature = "debug")] - tracing::trace!("Dropped mutable lock to value of type {}", self.type_name()); + tracing::trace!( + "Dropped mutable lock to value of type {}", + self.value_type_name + ); + } +} + +#[derive(Debug)] +pub struct MappedWriteGuard<'guard, Value> +{ + inner: MappedRwLockWriteGuard<'guard, Value>, + value_type_name: &'static str, +} + +impl<Value> Deref for MappedWriteGuard<'_, Value> +{ + type Target = Value; + + fn deref(&self) -> &Self::Target + { + &self.inner + } +} + +impl<Value> DerefMut for MappedWriteGuard<'_, Value> +{ + fn deref_mut(&mut self) -> &mut Self::Target + { + &mut self.inner + } +} + +impl<Value> Drop for MappedWriteGuard<'_, Value> +{ + fn drop(&mut self) + { + tracing::trace!( + "Dropped mapped mutable lock to value of type {}", + self.value_type_name + ); } } diff --git a/ecs/src/pair.rs b/ecs/src/pair.rs new file mode 100644 index 0000000..4ff4995 --- /dev/null +++ b/ecs/src/pair.rs @@ -0,0 +1,198 @@ +use std::convert::Infallible; + +use crate::component::{IntoParts as IntoComponentParts, Parts as ComponentParts}; +use crate::entity::{ + Handle as EntityHandle, + MatchingComponentIter as EntityMatchingComponentIter, +}; +use crate::query::{ + TermWithField as QueryTermWithField, + TermsBuilder as QueryTermsBuilder, + TermsBuilderInterface, +}; +use crate::uid::{PairParams as UidPairParams, Uid, Wildcard, With as WithUid}; +use crate::{Component, World}; + +#[derive(Debug)] +pub struct Pair<RelationElem: Element, TargetElem: Element> +{ + relation: RelationElem, + target: TargetElem, +} + +impl Pair<Uid, Uid> +{ + #[must_use] + pub fn new<Relation: WithUid>(target: Uid) -> Self + { + Self { relation: Relation::uid(), target } + } + + #[must_use] + pub fn id(&self) -> Uid + { + Uid::new_pair(&UidPairParams { + relation: self.relation, + target: self.target, + }) + } +} + +impl IntoComponentParts for Pair<Uid, Uid> +{ + fn into_parts(self) -> ComponentParts + { + ComponentParts::builder().name("Pair").build(self.id(), ()) + } +} + +impl<Relation, Target> QueryTermWithField for Pair<Relation, Target> +where + Relation: WithUid, + Target: WithUid, +{ + type Field<'a> = Handle<'a>; + + fn apply_to_terms_builder<const MAX_TERM_CNT: usize>( + terms_builder: &mut QueryTermsBuilder<MAX_TERM_CNT>, + ) + { + terms_builder.with_required([Self::uid()]); + } + + fn get_field<'world>( + entity_handle: &EntityHandle<'world>, + world: &'world World, + ) -> Self::Field<'world> + { + let first_matching_comp = entity_handle + .get_matching_components(Self::uid()) + .next() + .expect("Not possible"); + + Handle { + world, + pair_uid: first_matching_comp.id(), + } + } +} + +impl<Relation, Target> WithUid for Pair<Relation, Target> +where + Relation: WithUid, + Target: WithUid, +{ + fn uid() -> Uid + { + Uid::new_pair(&UidPairParams { + relation: Relation::uid(), + target: Target::uid(), + }) + } +} + +impl<Relation> QueryTermWithField for &'static [Pair<Relation, Wildcard>] +where + Relation: WithUid, +{ + type Field<'a> = HandleIter<'a>; + + fn apply_to_terms_builder<const MAX_TERM_CNT: usize>( + terms_builder: &mut QueryTermsBuilder<MAX_TERM_CNT>, + ) + { + terms_builder.with_required([Pair::<Relation, Wildcard>::uid()]); + } + + fn get_field<'world>( + entity_handle: &EntityHandle<'world>, + world: &'world World, + ) -> Self::Field<'world> + { + HandleIter { + inner: entity_handle + .get_matching_components(Pair::<Relation, Wildcard>::uid()), + world, + } + } +} + +pub struct Handle<'world> +{ + world: &'world World, + pair_uid: Uid, +} + +impl Handle<'_> +{ + #[must_use] + pub fn get_target_entity(&self) -> Option<EntityHandle<'_>> + { + let archetype = self + .world + .data + .component_storage + .get_entity_archetype(self.pair_uid.target_entity())?; + + let Some(archetype_entity) = + archetype.get_entity_by_id(self.pair_uid.target_entity()) + else { + unreachable!(); + }; + + Some(EntityHandle::new(archetype, archetype_entity)) + } +} + +pub struct HandleIter<'a> +{ + inner: EntityMatchingComponentIter<'a>, + world: &'a World, +} + +impl<'a> Iterator for HandleIter<'a> +{ + type Item = Handle<'a>; + + fn next(&mut self) -> Option<Self::Item> + { + let matching_comp = self.inner.next()?; + + Some(Handle { + world: self.world, + pair_uid: matching_comp.id(), + }) + } +} + +pub trait Element: sealed::Sealed +{ + type Value; +} + +impl Element for Uid +{ + type Value = Uid; +} + +impl sealed::Sealed for Uid {} + +impl<WithUidT: WithUid> Element for WithUidT +{ + type Value = Infallible; +} + +impl<WithUidT: WithUid> sealed::Sealed for WithUidT {} + +/// Relation denoting a dependency to another entity +#[derive(Debug, Default, Clone, Copy, Component)] +pub struct DependsOn; + +/// Relation denoting being the child of another entity. +#[derive(Debug, Default, Clone, Copy, Component)] +pub struct ChildOf; + +mod sealed +{ + pub trait Sealed {} +} diff --git a/ecs/src/phase.rs b/ecs/src/phase.rs new file mode 100644 index 0000000..9f47fb8 --- /dev/null +++ b/ecs/src/phase.rs @@ -0,0 +1,13 @@ +use ecs_macros::Component; + +use crate::pair::{ChildOf, Pair}; +use crate::static_entity; + +#[derive(Debug, Default, Clone, Copy, Component)] +pub struct Phase; + +static_entity!(pub START, (Phase,)); + +static_entity!(pub PRE_UPDATE, (Phase,)); + +static_entity!(pub UPDATE, (Phase, Pair::new::<ChildOf>(*PRE_UPDATE))); diff --git a/ecs/src/query.rs b/ecs/src/query.rs index f3318bd..ccb7add 100644 --- a/ecs/src/query.rs +++ b/ecs/src/query.rs @@ -1,115 +1,91 @@ -use std::iter::{Filter, Flatten, Map}; +use std::any::type_name; use std::marker::PhantomData; -use crate::component::storage::{ - Archetype, - ArchetypeEntity, - ArchetypeRefIter, - EntityIter, - Storage as ComponentStorage, -}; +use seq_macro::seq; + use crate::component::{ Component, - Metadata as ComponentMetadata, - Sequence as ComponentSequence, -}; -use crate::lock::{ReadGuard, WriteGuard}; -use crate::query::options::Options; -use crate::system::{ - NoInitParamFlag as NoInitSystemParamFlag, - Param as SystemParam, - System, + Handle as ComponentHandle, + HandleMut as ComponentHandleMut, }; -use crate::uid::Uid; -use crate::{EntityComponent, World}; +use crate::entity::Handle as EntityHandle; +use crate::query::flexible::{Iter as FlexibleQueryIter, Query as FlexibleQuery}; +use crate::system::{Param as SystemParam, System}; +use crate::uid::{Kind as UidKind, Uid, With as WithUid}; +use crate::util::array_vec::ArrayVec; +use crate::util::Array; +use crate::World; -pub mod options; +pub mod flexible; +pub mod term; #[derive(Debug)] -pub struct Query<'world, Comps, OptionsT = ()> +pub struct Query<'world, FieldTerms, FieldlessTerms = ()> where - Comps: ComponentSequence, + FieldTerms: TermWithFieldTuple, + FieldlessTerms: TermWithoutFieldTuple, { world: &'world World, - component_storage: ReadGuard<'world, ComponentStorage>, - _pd: PhantomData<(Comps, OptionsT)>, + // A term tuple type can have a maximum of 17 elements + inner: FlexibleQuery<'world, 17>, + _pd: PhantomData<(FieldTerms, FieldlessTerms)>, } -impl<'world, Comps, OptionsT> Query<'world, Comps, OptionsT> +impl<'world, FieldTerms, FieldlessTerms> Query<'world, FieldTerms, FieldlessTerms> where - Comps: ComponentSequence, - OptionsT: Options, + FieldTerms: TermWithFieldTuple, + FieldlessTerms: TermWithoutFieldTuple, { - /// Iterates over the entities matching this query. + /// Iterates over the entities matching this query, the iterator item being the entity + /// components. #[must_use] - pub fn iter_mut( - &'world self, - ) -> ComponentIterMut<'world, Comps, QueryEntityIter<'world>> + pub fn iter<'query>( + &'query self, + ) -> Iter<'query, 'world, FieldTerms, FlexibleQueryIter<'query>> { - #[cfg(feature = "debug")] - tracing::debug!("Searching for {}", std::any::type_name::<Comps>()); + tracing::trace!("Searching for {}", std::any::type_name::<FieldTerms>()); - #[allow(clippy::map_flatten)] - ComponentIterMut { + Iter { world: self.world, - entities: self - .component_storage - .find_entities(Comps::metadata()) - .map(Archetype::entities as ComponentIterMapFn) - .flatten() - .filter(|entity| OptionsT::entity_filter(entity.components())), + iter: self.inner.iter(), comps_pd: PhantomData, } } - /// Iterates over the entities matching this query. + /// Iterates over the entities matching this query, the iterator item being the entity + /// [`Uid`] and the matching entity components. #[must_use] - pub fn iter(&'world self) -> ComponentIter<'world, Comps, QueryEntityIter<'world>> + pub fn iter_with_euids<'query>( + &'query self, + ) -> ComponentAndEuidIter<'query, 'world, FieldTerms, FlexibleQueryIter<'query>> { - #[cfg(feature = "debug")] - tracing::debug!("Searching for {}", std::any::type_name::<Comps>()); + tracing::trace!("Searching for {}", std::any::type_name::<FieldTerms>()); - #[allow(clippy::map_flatten)] - ComponentIter { + ComponentAndEuidIter { world: self.world, - entities: self - .component_storage - .find_entities(Comps::metadata()) - .map(Archetype::entities as ComponentIterMapFn) - .flatten() - .filter(|entity| OptionsT::entity_filter(entity.components())), + iter: self.inner.iter(), comps_pd: PhantomData, } } - /// Iterates over the entities matching this query and has the provided extra - /// component. + /// Iterates over the entities matching this query using the iterator returned by + /// `func`. + /// + /// This function exists so that a custom [`EntityHandle`] iterator can be given to + /// [`Iter`] without giving the user access to a reference to the [`World`]. #[must_use] - pub fn iter_with_extra_comps( - &'world self, - extra_components: impl IntoIterator<Item = ComponentMetadata>, - ) -> ComponentIter<'world, Comps, QueryEntityIter<'world>> + pub fn iter_with<'query, OutIter>( + &'query self, + func: impl FnOnce(FlexibleQueryIter<'query>) -> OutIter, + ) -> Iter<'query, 'world, FieldTerms, OutIter> + where + OutIter: Iterator<Item = EntityHandle<'query>>, { - #[cfg(feature = "debug")] - tracing::debug!( - "Searching for {} + extra components", - std::any::type_name::<Comps>() - ); - - #[allow(clippy::map_flatten)] - ComponentIter { + tracing::trace!("Searching for {}", std::any::type_name::<FieldTerms>()); + + Iter { world: self.world, - entities: self - .component_storage - .find_entities( - Comps::metadata() - .into_iter() - .chain(extra_components) - .collect::<Vec<_>>(), - ) - .map(Archetype::entities as ComponentIterMapFn) - .flatten() - .filter(|entity| OptionsT::entity_filter(entity.components())), + iter: func(self.inner.iter()), comps_pd: PhantomData, } } @@ -118,51 +94,45 @@ where #[must_use] pub fn get_entity_uid(&self, entity_index: usize) -> Option<Uid> { - Some( - self.component_storage - .find_entities(Comps::metadata()) - .flat_map(|archetype| archetype.entities()) - .filter(|entity| OptionsT::entity_filter(entity.components())) - .nth(entity_index)? - .uid(), - ) + Some(self.inner.iter().nth(entity_index)?.uid()) } pub(crate) fn new(world: &'world World) -> Self { + let mut terms_builder = Terms::builder(); + + FieldTerms::apply_terms_to_builder(&mut terms_builder); + FieldlessTerms::apply_terms_to_builder(&mut terms_builder); + Self { world, - component_storage: world - .data - .component_storage - .read_nonblock() - .expect("Failed to acquire read-only component storage lock"), + inner: world.flexible_query(terms_builder.build()), _pd: PhantomData, } } } -impl<'world, Comps, OptionsT> IntoIterator for &'world Query<'world, Comps, OptionsT> +impl<'query, 'world, FieldTerms, FieldlessTerms> IntoIterator + for &'query Query<'world, FieldTerms, FieldlessTerms> where - Comps: ComponentSequence, - OptionsT: Options, + FieldTerms: TermWithFieldTuple, + FieldlessTerms: TermWithoutFieldTuple, { - type IntoIter = ComponentIterMut<'world, Comps, QueryEntityIter<'world>>; - type Item = Comps::MutRefs<'world>; + type IntoIter = Iter<'query, 'world, FieldTerms, FlexibleQueryIter<'query>>; + type Item = FieldTerms::Fields<'query>; fn into_iter(self) -> Self::IntoIter { - self.iter_mut() + self.iter() } } -unsafe impl<'world, Comps, OptionsT> SystemParam<'world> - for Query<'world, Comps, OptionsT> +impl<'world, FieldTerms, FieldlessTerms> SystemParam<'world> + for Query<'world, FieldTerms, FieldlessTerms> where - Comps: ComponentSequence, - OptionsT: Options, + FieldTerms: TermWithFieldTuple, + FieldlessTerms: TermWithoutFieldTuple, { - type Flags = NoInitSystemParamFlag; type Input = (); fn initialize<SystemImpl>( @@ -181,93 +151,429 @@ where } } -type ComponentIterMapFn = for<'a> fn(&'a Archetype) -> EntityIter<'a>; +#[derive(Debug)] +pub struct Terms<const MAX_TERM_CNT: usize> +{ + required_components: ArrayVec<Uid, MAX_TERM_CNT>, + excluded_components: ArrayVec<Uid, MAX_TERM_CNT>, +} -type ComponentIterFilterFn = for<'a, 'b> fn(&'a &'b ArchetypeEntity) -> bool; +impl<const MAX_TERM_CNT: usize> Terms<MAX_TERM_CNT> +{ + pub fn builder() -> TermsBuilder<MAX_TERM_CNT> + { + TermsBuilder::default() + } +} -type QueryEntityIter<'world> = Filter< - Flatten<Map<ArchetypeRefIter<'world>, ComponentIterMapFn>>, - ComponentIterFilterFn, ->; +#[derive(Debug, Default)] +#[must_use] +pub struct TermsBuilder<const MAX_TERM_CNT: usize> +{ + required_components: ArrayVec<Uid, MAX_TERM_CNT>, + excluded_components: ArrayVec<Uid, MAX_TERM_CNT>, +} -pub struct ComponentIterMut<'world, Comps, EntityIter> -where - EntityIter: Iterator<Item = &'world ArchetypeEntity>, +#[allow(clippy::return_self_not_must_use)] +pub trait TermsBuilderInterface { - world: &'world World, - entities: EntityIter, - comps_pd: PhantomData<Comps>, + fn with<WithUidT: WithUid>(self) -> Self; + + fn without<WithUidT: WithUid>(self) -> Self; + + fn with_required(self, ids: impl Array<Uid>) -> Self; + + fn without_ids(self, ids: impl Array<Uid>) -> Self; } -impl<'world, Comps, EntityIter> Iterator for ComponentIterMut<'world, Comps, EntityIter> -where - Comps: ComponentSequence + 'world, - EntityIter: Iterator<Item = &'world ArchetypeEntity>, +macro_rules! impl_terms_builder { + ($($impl_content: tt)*) => { + impl<const MAX_TERM_CNT: usize> + TermsBuilderInterface for TermsBuilder<MAX_TERM_CNT> + { + $($impl_content)* + } + + impl<const MAX_TERM_CNT: usize> + TermsBuilderInterface for &mut TermsBuilder<MAX_TERM_CNT> + { + $($impl_content)* + } + }; +} + +impl_terms_builder! { + #[allow(unused_mut)] + fn with<WithUidT: WithUid>(mut self) -> Self + { + let insert_index = self.required_components + .partition_point(|id| *id <= WithUidT::uid()); + + self.required_components + .insert(insert_index, WithUidT::uid()); + + self + } + + #[allow(unused_mut)] + fn without<WithUidT: WithUid>(mut self) -> Self + { + let insert_index = self.excluded_components + .partition_point(|id| *id <= WithUidT::uid()); + + self.excluded_components + .insert(insert_index, WithUidT::uid()); + + self + } + + #[allow(unused_mut)] + fn with_required(mut self, mut ids: impl Array<Uid>) -> Self + { + if !ids.as_ref().is_sorted() { + ids.as_mut().sort(); + } + + if self.required_components.is_empty() { + self.required_components.extend(ids); + return self; + } + + let mut id_iter = ids.into_iter(); + + while let Some(id) = id_iter.next() { + let insert_index = self.required_components + .partition_point(|other_id| *other_id <= id); + + if insert_index == self.required_components.len() { + self.required_components.extend([id].into_iter().chain(id_iter)); + + return self; + } + + self.required_components + .insert(insert_index, id); + + } + + self + } + + #[allow(unused_mut)] + fn without_ids(mut self, mut ids: impl Array<Uid>) -> Self + { + if !ids.as_ref().is_sorted() { + ids.as_mut().sort(); + } + + if self.excluded_components.is_empty() { + self.excluded_components.extend(ids); + return self; + } + + let mut id_iter = ids.into_iter(); + + while let Some(id) = id_iter.next() { + let insert_index = self.excluded_components + .partition_point(|other_id| *other_id <= id); + + if insert_index == self.excluded_components.len() { + self.excluded_components.extend([id].into_iter().chain(id_iter)); + + return self; + } + + self.excluded_components + .insert(insert_index, id); + + } + + self + } +} + +impl<const MAX_TERM_CNT: usize> TermsBuilder<MAX_TERM_CNT> { - type Item = Comps::MutRefs<'world>; + #[must_use] + pub fn build(self) -> Terms<MAX_TERM_CNT> + { + debug_assert!(self.required_components.is_sorted()); + debug_assert!(self.excluded_components.is_sorted()); - fn next(&mut self) -> Option<Self::Item> + Terms { + required_components: self.required_components, + excluded_components: self.excluded_components, + } + } +} + +pub trait TermWithoutField +{ + fn apply_to_terms_builder<const MAX_TERM_CNT: usize>( + terms_builder: &mut TermsBuilder<MAX_TERM_CNT>, + ); +} + +pub trait TermWithField +{ + type Field<'a>; + + fn apply_to_terms_builder<const MAX_TERM_CNT: usize>( + terms_builder: &mut TermsBuilder<MAX_TERM_CNT>, + ); + + fn get_field<'world>( + entity_handle: &EntityHandle<'world>, + world: &'world World, + ) -> Self::Field<'world>; +} + +impl<ComponentT: Component> TermWithField for &ComponentT +{ + type Field<'a> = ComponentHandle<'a, ComponentT>; + + fn apply_to_terms_builder<const MAX_TERM_CNT: usize>( + terms_builder: &mut TermsBuilder<MAX_TERM_CNT>, + ) { - Some(Comps::from_components_mut( - self.entities.next()?.components().iter(), - self.world, - lock_component_rw, - )) + terms_builder.with::<ComponentT>(); + } + + fn get_field<'world>( + entity_handle: &EntityHandle<'world>, + _world: &'world World, + ) -> Self::Field<'world> + { + assert_eq!(ComponentT::id().kind(), UidKind::Component); + + let Some(component) = entity_handle + .get_matching_components(ComponentT::id()) + .next() + else { + panic!( + concat!( + "Component {} was not found in entity {}. There ", + "is most likely a bug in the entity querying" + ), + type_name::<ComponentT>(), + entity_handle.uid() + ); + }; + + Self::Field::from_entity_component_ref(component).unwrap_or_else(|err| { + panic!( + "Creating handle to component {} failed: {err}", + type_name::<ComponentT>() + ); + }) } } -fn lock_component_rw( - entity_component: &EntityComponent, -) -> WriteGuard<'_, Box<dyn Component>> +impl<ComponentT: Component> TermWithField for &mut ComponentT { - entity_component - .component - .write_nonblock() - .unwrap_or_else(|_| { + type Field<'a> = ComponentHandleMut<'a, ComponentT>; + + fn apply_to_terms_builder<const MAX_TERM_CNT: usize>( + terms_builder: &mut TermsBuilder<MAX_TERM_CNT>, + ) + { + terms_builder.with::<ComponentT>(); + } + + fn get_field<'world>( + entity_handle: &EntityHandle<'world>, + _world: &'world World, + ) -> Self::Field<'world> + { + assert_eq!(ComponentT::id().kind(), UidKind::Component); + + let Some(component) = entity_handle + .get_matching_components(ComponentT::id()) + .next() + else { + panic!( + concat!( + "Component {} was not found in entity {}. There ", + "is most likely a bug in the entity querying" + ), + type_name::<ComponentT>(), + entity_handle.uid() + ); + }; + + Self::Field::from_entity_component_ref(component).unwrap_or_else(|err| { panic!( - "Failed to acquire read-write lock to component {}", - entity_component.name + "Creating handle to component {} failed: {err}", + type_name::<ComponentT>() ); }) + } +} + +pub trait TermWithoutFieldTuple +{ + fn apply_terms_to_builder<const MAX_TERM_CNT: usize>( + terms_builder: &mut TermsBuilder<MAX_TERM_CNT>, + ); +} + +pub trait TermWithFieldTuple +{ + type Fields<'component>; + + fn apply_terms_to_builder<const MAX_TERM_CNT: usize>( + terms_builder: &mut TermsBuilder<MAX_TERM_CNT>, + ); + + fn get_fields<'component>( + entity_handle: &EntityHandle<'component>, + world: &'component World, + ) -> Self::Fields<'component>; +} + +pub struct Iter<'query, 'world, FieldTerms, EntityHandleIter> +where + FieldTerms: TermWithFieldTuple, + EntityHandleIter: Iterator<Item = EntityHandle<'query>>, +{ + world: &'world World, + iter: EntityHandleIter, + comps_pd: PhantomData<FieldTerms>, } -pub struct ComponentIter<'world, Comps, EntityIter> +impl<'query, 'world, FieldTerms, EntityHandleIter> + Iter<'query, 'world, FieldTerms, EntityHandleIter> where - EntityIter: Iterator<Item = &'world ArchetypeEntity>, + FieldTerms: TermWithFieldTuple, + EntityHandleIter: Iterator<Item = EntityHandle<'query>>, + 'world: 'query, +{ + /// Creates a new iterator from the given entity handle iterator. + /// + /// # Important + /// All of the yielded entities of the entity handle iterator should match the + /// terms `Terms`. The [`Self::next`] function will panic if it encounters a + /// entity that does not match the terms `Terms`. + pub fn new(world: &'world World, iter: EntityHandleIter) -> Self + { + Self { world, iter, comps_pd: PhantomData } + } +} + +impl<'query, 'world, FieldTerms, EntityHandleIter> Iterator + for Iter<'query, 'world, FieldTerms, EntityHandleIter> +where + FieldTerms: TermWithFieldTuple, + EntityHandleIter: Iterator<Item = EntityHandle<'query>>, + 'world: 'query, +{ + type Item = FieldTerms::Fields<'query>; + + fn next(&mut self) -> Option<Self::Item> + { + let entity_handle = self.iter.next()?; + + Some(FieldTerms::get_fields(&entity_handle, self.world)) + } +} + +pub struct ComponentAndEuidIter<'query, 'world, FieldTerms, EntityHandleIter> +where + FieldTerms: TermWithFieldTuple, + EntityHandleIter: Iterator<Item = EntityHandle<'query>>, { world: &'world World, - entities: EntityIter, - comps_pd: PhantomData<Comps>, + iter: EntityHandleIter, + comps_pd: PhantomData<FieldTerms>, } -impl<'world, Comps, EntityIter> Iterator for ComponentIter<'world, Comps, EntityIter> +impl<'query, 'world, FieldTerms, EntityHandleIter> Iterator + for ComponentAndEuidIter<'query, 'world, FieldTerms, EntityHandleIter> where - Comps: ComponentSequence + 'world, - EntityIter: Iterator<Item = &'world ArchetypeEntity>, + FieldTerms: TermWithFieldTuple, + EntityHandleIter: Iterator<Item = EntityHandle<'query>>, + 'world: 'query, { - type Item = Comps::Refs<'world>; + type Item = (Uid, FieldTerms::Fields<'query>); fn next(&mut self) -> Option<Self::Item> { - Some(Comps::from_components( - self.entities.next()?.components().iter(), - self.world, - lock_component_ro, + let entity_handle = self.iter.next()?; + + Some(( + entity_handle.uid(), + FieldTerms::get_fields(&entity_handle, self.world), )) } } -fn lock_component_ro( - entity_component: &EntityComponent, -) -> ReadGuard<'_, Box<dyn Component>> +macro_rules! impl_term_sequence { + ($c: tt) => { + seq!(I in 0..=$c { + impl<#(Term~I: TermWithoutField,)*> TermWithoutFieldTuple for (#(Term~I,)*) + { + fn apply_terms_to_builder<const MAX_TERM_CNT: usize>( + terms_builder: &mut TermsBuilder<MAX_TERM_CNT> + ) + { + #( + Term~I::apply_to_terms_builder(terms_builder); + )* + } + } + + impl<#(Term~I: TermWithField,)*> TermWithFieldTuple for (#(Term~I,)*) + { + type Fields<'component> = (#(Term~I::Field<'component>,)*); + + fn apply_terms_to_builder<const MAX_TERM_CNT: usize>( + terms_builder: &mut TermsBuilder<MAX_TERM_CNT> + ) + { + #( + Term~I::apply_to_terms_builder(terms_builder); + )* + } + + fn get_fields<'component>( + entity_handle: &EntityHandle<'component>, + world: &'component World, + ) -> Self::Fields<'component> + { + (#(Term~I::get_field(entity_handle, world),)*) + } + } + }); + }; +} + +seq!(C in 0..=16 { + impl_term_sequence!(C); +}); + +impl TermWithoutFieldTuple for () +{ + fn apply_terms_to_builder<const MAX_TERM_CNT: usize>( + _terms_builder: &mut TermsBuilder<MAX_TERM_CNT>, + ) + { + } +} + +impl TermWithFieldTuple for () { - entity_component - .component - .read_nonblock() - .unwrap_or_else(|_| { - panic!( - "Failed to acquire read-write lock to component {}", - entity_component.name - ); - }) + type Fields<'component> = (); + + fn apply_terms_to_builder<const MAX_TERM_CNT: usize>( + _terms_builder: &mut TermsBuilder<MAX_TERM_CNT>, + ) + { + } + + fn get_fields<'component>( + _entity_handle: &EntityHandle<'_>, + _world: &'component World, + ) -> Self::Fields<'component> + { + } } diff --git a/ecs/src/query/flexible.rs b/ecs/src/query/flexible.rs new file mode 100644 index 0000000..add30b0 --- /dev/null +++ b/ecs/src/query/flexible.rs @@ -0,0 +1,84 @@ +//! Low-level querying. +use std::iter::{repeat_n, FlatMap, RepeatN, Zip}; + +use crate::component::storage::archetype::{Archetype, EntityIter}; +use crate::component::storage::{ArchetypeRefIter, ArchetypeSearchTerms}; +use crate::entity::Handle as EntityHandle; +use crate::query::Terms; +use crate::World; + +/// Low-level entity query structure. +#[derive(Debug)] +pub struct Query<'world, const MAX_TERM_CNT: usize> +{ + world: &'world World, + terms: Terms<MAX_TERM_CNT>, +} + +impl<'world, const MAX_TERM_CNT: usize> Query<'world, MAX_TERM_CNT> +{ + /// Iterates over the entities matching this query. + #[must_use] + pub fn iter(&self) -> Iter<'_> + { + Iter { + iter: self + .world + .data + .component_storage + .search_archetypes(ArchetypeSearchTerms { + required_components: &self.terms.required_components, + excluded_components: &self.terms.excluded_components, + }) + .flat_map( + (|archetype| { + repeat_n(archetype, archetype.entity_cnt()) + .zip(archetype.entities()) + }) as ComponentIterMapFn, + ), + } + } + + pub(crate) fn new(world: &'world World, terms: Terms<MAX_TERM_CNT>) -> Self + { + Self { world, terms } + } +} + +impl<'query, const MAX_TERM_CNT: usize> IntoIterator for &'query Query<'_, MAX_TERM_CNT> +{ + type IntoIter = Iter<'query>; + type Item = EntityHandle<'query>; + + fn into_iter(self) -> Self::IntoIter + { + self.iter() + } +} + +pub struct Iter<'query> +{ + iter: QueryEntityIter<'query>, +} + +impl<'query> Iterator for Iter<'query> +{ + type Item = EntityHandle<'query>; + + fn next(&mut self) -> Option<Self::Item> + { + let (archetype, entity) = self.iter.next()?; + + Some(EntityHandle::new(archetype, entity)) + } +} + +type ComponentIterMapFnOutput<'a> = Zip<RepeatN<&'a Archetype>, EntityIter<'a>>; + +type ComponentIterMapFn = for<'a> fn(&'a Archetype) -> ComponentIterMapFnOutput<'a>; + +type QueryEntityIter<'query> = FlatMap< + ArchetypeRefIter<'query, 'query>, + ComponentIterMapFnOutput<'query>, + ComponentIterMapFn, +>; diff --git a/ecs/src/query/options.rs b/ecs/src/query/options.rs deleted file mode 100644 index bbbe0a8..0000000 --- a/ecs/src/query/options.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::collections::HashSet; -use std::marker::PhantomData; - -use crate::component::Component; -use crate::EntityComponent; - -/// Query options. -pub trait Options -{ - fn entity_filter<'component>( - components: impl IntoIterator<Item = &'component EntityComponent>, - ) -> bool; -} - -impl Options for () -{ - fn entity_filter<'component>( - _: impl IntoIterator<Item = &'component EntityComponent>, - ) -> bool - { - true - } -} - -pub struct With<ComponentT> -where - ComponentT: Component, -{ - _pd: PhantomData<ComponentT>, -} - -impl<ComponentT> Options for With<ComponentT> -where - ComponentT: Component, -{ - fn entity_filter<'component>( - components: impl IntoIterator<Item = &'component EntityComponent>, - ) -> bool - { - let ids_set = components - .into_iter() - .map(|component| component.id) - .collect::<HashSet<_>>(); - - ids_set.contains(&ComponentT::id()) - } -} - -pub struct Not<OptionsT> -where - OptionsT: Options, -{ - _pd: PhantomData<OptionsT>, -} - -impl<OptionsT> Options for Not<OptionsT> -where - OptionsT: Options, -{ - fn entity_filter<'component>( - components: impl IntoIterator<Item = &'component EntityComponent>, - ) -> bool - { - !OptionsT::entity_filter(components) - } -} diff --git a/ecs/src/query/term.rs b/ecs/src/query/term.rs new file mode 100644 index 0000000..9c772da --- /dev/null +++ b/ecs/src/query/term.rs @@ -0,0 +1,115 @@ +use std::any::type_name; +use std::marker::PhantomData; + +use crate::component::{ + Component, + Handle as ComponentHandle, + HandleMut as ComponentHandleMut, +}; +use crate::query::{ + TermWithField, + TermWithoutField, + TermsBuilder, + TermsBuilderInterface, +}; +use crate::uid::With as WithUid; + +pub struct With<WithUidT> +where + WithUidT: WithUid, +{ + _pd: PhantomData<WithUidT>, +} + +impl<WithUidT> TermWithoutField for With<WithUidT> +where + WithUidT: WithUid, +{ + fn apply_to_terms_builder<const MAX_TERM_CNT: usize>( + terms_builder: &mut TermsBuilder<MAX_TERM_CNT>, + ) + { + terms_builder.with::<WithUidT>(); + } +} + +pub struct Without<WithUidT> +where + WithUidT: WithUid, +{ + _pd: PhantomData<WithUidT>, +} + +impl<WithUidT> TermWithoutField for Without<WithUidT> +where + WithUidT: WithUid, +{ + fn apply_to_terms_builder<const MAX_TERM_CNT: usize>( + terms_builder: &mut TermsBuilder<MAX_TERM_CNT>, + ) + { + terms_builder.without::<WithUidT>(); + } +} + +impl<ComponentT: Component> TermWithField for Option<&ComponentT> +{ + type Field<'a> = Option<ComponentHandle<'a, ComponentT>>; + + fn apply_to_terms_builder<const MAX_TERM_CNT: usize>( + _terms_builder: &mut TermsBuilder<MAX_TERM_CNT>, + ) + { + } + + fn get_field<'world>( + entity_handle: &crate::entity::Handle<'world>, + _world: &'world crate::World, + ) -> Self::Field<'world> + { + Some( + ComponentHandle::<'world, ComponentT>::from_entity_component_ref( + entity_handle + .get_matching_components(ComponentT::id()) + .next()?, + ) + .unwrap_or_else(|err| { + panic!( + "Creating handle to component {} failed: {err}", + type_name::<ComponentT>() + ); + }), + ) + } +} + +impl<ComponentT: Component> TermWithField for Option<&mut ComponentT> +{ + type Field<'a> = Option<ComponentHandleMut<'a, ComponentT>>; + + fn apply_to_terms_builder<const MAX_TERM_CNT: usize>( + _terms_builder: &mut TermsBuilder<MAX_TERM_CNT>, + ) + { + } + + fn get_field<'world>( + entity_handle: &crate::entity::Handle<'world>, + _world: &'world crate::World, + ) -> Self::Field<'world> + { + Some( + ComponentHandleMut::<'world, ComponentT>::from_entity_component_ref( + entity_handle + .get_matching_components(ComponentT::id()) + .next()?, + ) + .unwrap_or_else(|err| { + panic!( + "Creating handle to component {} failed: {err}", + type_name::<ComponentT>() + ); + }), + ) + } +} diff --git a/ecs/src/relationship.rs b/ecs/src/relationship.rs deleted file mode 100644 index 0ebd9c2..0000000 --- a/ecs/src/relationship.rs +++ /dev/null @@ -1,418 +0,0 @@ -use std::any::type_name; -use std::marker::PhantomData; - -use ecs_macros::Component; - -use crate::component::storage::Storage as ComponentStorage; -use crate::component::{ - Component, - FromOptional as FromOptionalComponent, - FromOptionalMut as FromOptionalMutComponent, -}; -use crate::lock::ReadGuard; -use crate::system::{ComponentRef, ComponentRefMut}; -use crate::uid::{Kind as UidKind, Uid}; -use crate::World; - -/// A relationship to one or more targets. -#[derive(Debug, Component)] -#[component( - ref_type = Relation<'component, Kind, ComponentT>, - ref_mut_type = RelationMut<'component, Kind, ComponentT>, -)] -pub struct Relationship<Kind, ComponentT: Component> -where - Kind: 'static, -{ - entity_uid: SingleOrMultiple<Uid>, - _pd: PhantomData<(Kind, ComponentT)>, -} - -impl<Kind, ComponentT> Relationship<Kind, ComponentT> -where - ComponentT: Component, -{ - /// Creates a new `Relationship` with a single target. - #[must_use] - pub fn new(entity_uid: Uid) -> Self - { - debug_assert_eq!(entity_uid.kind(), UidKind::Entity); - - Self { - entity_uid: SingleOrMultiple::Single(entity_uid), - _pd: PhantomData, - } - } - - /// Creates a new `Relationship` with multiple targets. - #[must_use] - pub fn new_multiple(entity_uids: impl IntoIterator<Item = Uid>) -> Self - { - let uids = entity_uids.into_iter().collect::<Vec<_>>(); - - for euid in &uids { - debug_assert_eq!(euid.kind(), UidKind::Entity); - } - - Self { - entity_uid: SingleOrMultiple::Multiple(uids), - _pd: PhantomData, - } - } -} - -pub struct RelationMut<'rel_comp, Kind, ComponentT> -where - Kind: 'static, - ComponentT: Component, -{ - component_storage_lock: ReadGuard<'static, ComponentStorage>, - relationship_comp: ComponentRefMut<'rel_comp, Relationship<Kind, ComponentT>>, -} - -impl<'rel_comp, Kind, ComponentT> FromOptionalMutComponent<'rel_comp> - for RelationMut<'rel_comp, Kind, ComponentT> -where - ComponentT: Component, -{ - fn from_optional_mut_component( - optional_component: Option< - crate::lock::WriteGuard<'rel_comp, Box<dyn Component>>, - >, - world: &'rel_comp World, - ) -> Self - { - let relationship_comp = - ComponentRefMut::<Relationship<Kind, ComponentT>>::from_optional_mut_component( - optional_component, - world, - ); - - let component_storage_lock = world - .data - .component_storage - .read_nonblock() - .expect("Failed to aquire read-only component storage lock"); - - Self { - relationship_comp, - // SAFETY: The component lock is not used for longer than the original - // lifetime - component_storage_lock: unsafe { component_storage_lock.upgrade_lifetime() }, - } - } -} - -impl<'rel_comp, Kind, ComponentT> RelationMut<'rel_comp, Kind, ComponentT> -where - ComponentT: Component, -{ - /// Returns the component of the target at the specified index. - /// - /// # Panics - /// Will panic if the entity does not exist in the archetype it belongs to. This - /// should hopefully never happend. - #[must_use] - pub fn get(&self, index: usize) -> Option<ComponentRefMut<'_, ComponentT>> - { - let target = self.get_target(index)?; - - let archetype = self.component_storage_lock.get_entity_archetype(*target)?; - - let entity = archetype - .get_entity(*target) - .expect("Target entity is gone from archetype"); - - let component_index = archetype.get_index_for_component(ComponentT::id())?; - - let component = ComponentRefMut::new( - entity - .get_component(component_index)? - .component - .write_nonblock() - .unwrap_or_else(|_| { - panic!( - "Failed to aquire read-write lock of component {}", - type_name::<ComponentT>() - ) - }), - ); - - Some(component) - } - - /// Returns a reference to the target at the specified index. - #[must_use] - pub fn get_target(&self, index: usize) -> Option<&Uid> - { - match &self.relationship_comp.entity_uid { - SingleOrMultiple::Single(entity_uid) if index == 0 => Some(entity_uid), - SingleOrMultiple::Multiple(entity_uids) => entity_uids.get(index), - SingleOrMultiple::Single(_) => None, - } - } - - /// Returns a mutable reference to the target at the specified index. - #[must_use] - pub fn get_target_mut(&mut self, index: usize) -> Option<&mut Uid> - { - match &mut self.relationship_comp.entity_uid { - SingleOrMultiple::Single(entity_uid) if index == 0 => Some(entity_uid), - SingleOrMultiple::Multiple(entity_uids) => entity_uids.get_mut(index), - SingleOrMultiple::Single(_) => None, - } - } - - /// Adds a target to the relationship. - pub fn add_target(&mut self, entity_uid: Uid) - { - debug_assert_eq!(entity_uid.kind(), UidKind::Entity); - - match &mut self.relationship_comp.entity_uid { - SingleOrMultiple::Single(prev_entity_uid) => { - self.relationship_comp.entity_uid = - SingleOrMultiple::Multiple(vec![*prev_entity_uid, entity_uid]); - } - SingleOrMultiple::Multiple(entity_uids) => entity_uids.push(entity_uid), - } - } - - /// Removes a target to the relationship, returning it. - pub fn remove_target(&mut self, index: usize) -> Option<Uid> - { - match &mut self.relationship_comp.entity_uid { - SingleOrMultiple::Single(entity_uid) => { - let prev_entity_uid = *entity_uid; - - self.relationship_comp.entity_uid = - SingleOrMultiple::Multiple(Vec::new()); - - Some(prev_entity_uid) - } - SingleOrMultiple::Multiple(entity_uids) => { - if index >= entity_uids.len() { - return None; - } - - Some(entity_uids.remove(index)) - } - } - } - - #[must_use] - pub fn target_count(&self) -> usize - { - match &self.relationship_comp.entity_uid { - SingleOrMultiple::Single(_) => 1, - SingleOrMultiple::Multiple(entity_uids) => entity_uids.len(), - } - } - - /// Returns a iterator of the components of the targets of this relationship. - #[must_use] - pub fn iter(&self) -> TargetComponentIterMut<'_, 'rel_comp, Kind, ComponentT> - { - TargetComponentIterMut { relation: self, index: 0 } - } -} - -impl<'relationship, 'rel_comp, Kind, ComponentT> IntoIterator - for &'relationship RelationMut<'rel_comp, Kind, ComponentT> -where - 'relationship: 'rel_comp, - ComponentT: Component, -{ - type IntoIter = TargetComponentIterMut<'relationship, 'rel_comp, Kind, ComponentT>; - type Item = ComponentRefMut<'rel_comp, ComponentT>; - - fn into_iter(self) -> Self::IntoIter - { - self.iter() - } -} - -/// Iterator of the components of the targets of a relationship. -pub struct TargetComponentIterMut<'relationship, 'rel_comp, Kind, ComponentT> -where - Kind: 'static, - ComponentT: Component, -{ - relation: &'relationship RelationMut<'rel_comp, Kind, ComponentT>, - index: usize, -} - -impl<'relationship, 'rel_comp, Kind, ComponentT> Iterator - for TargetComponentIterMut<'relationship, 'rel_comp, Kind, ComponentT> -where - 'relationship: 'rel_comp, - Kind: 'static, - ComponentT: Component, -{ - type Item = ComponentRefMut<'rel_comp, ComponentT>; - - fn next(&mut self) -> Option<Self::Item> - { - let index = self.index; - - self.index += 1; - - self.relation.get(index) - } -} - -#[derive(Debug)] -enum SingleOrMultiple<Value> -{ - Single(Value), - Multiple(Vec<Value>), -} - -pub struct Relation<'rel_comp, Kind, ComponentT> -where - Kind: 'static, - ComponentT: Component, -{ - component_storage_lock: ReadGuard<'static, ComponentStorage>, - relationship_comp: ComponentRef<'rel_comp, Relationship<Kind, ComponentT>>, -} - -impl<'rel_comp, Kind, ComponentT> FromOptionalComponent<'rel_comp> - for Relation<'rel_comp, Kind, ComponentT> -where - ComponentT: Component, -{ - fn from_optional_component( - optional_component: Option<ReadGuard<'rel_comp, Box<dyn Component>>>, - world: &'rel_comp World, - ) -> Self - { - let relationship_comp = - ComponentRef::<Relationship<Kind, ComponentT>>::from_optional_component( - optional_component, - world, - ); - - let component_storage_lock = world - .data - .component_storage - .read_nonblock() - .expect("Failed to aquire read-only component storage lock"); - - Self { - relationship_comp, - // SAFETY: The component lock is not used for longer than the original - // lifetime - component_storage_lock: unsafe { component_storage_lock.upgrade_lifetime() }, - } - } -} - -impl<'rel_comp, Kind, ComponentT> Relation<'rel_comp, Kind, ComponentT> -where - ComponentT: Component, -{ - /// Returns the component of the target at the specified index. - /// - /// # Panics - /// Will panic if the entity does not exist in the archetype it belongs to. This - /// should hopefully never happend. - #[must_use] - pub fn get(&self, index: usize) -> Option<ComponentRef<'_, ComponentT>> - { - let target = self.get_target(index)?; - - let archetype = self.component_storage_lock.get_entity_archetype(*target)?; - - let entity = archetype - .get_entity(*target) - .expect("Target entity is gone from archetype"); - - let component_index = archetype.get_index_for_component(ComponentT::id())?; - - let component = ComponentRef::new( - entity - .get_component(component_index)? - .component - .read_nonblock() - .unwrap_or_else(|_| { - panic!( - "Failed to aquire read-write lock of component {}", - type_name::<ComponentT>() - ) - }), - ); - - Some(component) - } - - /// Returns a reference to the target at the specified index. - #[must_use] - pub fn get_target(&self, index: usize) -> Option<&Uid> - { - match &self.relationship_comp.entity_uid { - SingleOrMultiple::Single(entity_uid) if index == 0 => Some(entity_uid), - SingleOrMultiple::Multiple(entity_uids) => entity_uids.get(index), - SingleOrMultiple::Single(_) => None, - } - } - - #[must_use] - pub fn target_count(&self) -> usize - { - match &self.relationship_comp.entity_uid { - SingleOrMultiple::Single(_) => 1, - SingleOrMultiple::Multiple(entity_uids) => entity_uids.len(), - } - } - - /// Returns a iterator of the components of the targets of this relationship. - #[must_use] - pub fn iter(&self) -> TargetComponentIter<'_, 'rel_comp, Kind, ComponentT> - { - TargetComponentIter { relation: self, index: 0 } - } -} - -impl<'relationship, 'rel_comp, Kind, ComponentT> IntoIterator - for &'relationship Relation<'rel_comp, Kind, ComponentT> -where - 'relationship: 'rel_comp, - ComponentT: Component, -{ - type IntoIter = TargetComponentIter<'relationship, 'rel_comp, Kind, ComponentT>; - type Item = ComponentRef<'rel_comp, ComponentT>; - - fn into_iter(self) -> Self::IntoIter - { - self.iter() - } -} - -/// Iterator of the components of the targets of a relationship. -pub struct TargetComponentIter<'relationship, 'rel_comp, Kind, ComponentT> -where - Kind: 'static, - ComponentT: Component, -{ - relation: &'relationship Relation<'rel_comp, Kind, ComponentT>, - index: usize, -} - -impl<'relationship, 'rel_comp, Kind, ComponentT> Iterator - for TargetComponentIter<'relationship, 'rel_comp, Kind, ComponentT> -where - 'relationship: 'rel_comp, - Kind: 'static, - ComponentT: Component, -{ - type Item = ComponentRef<'rel_comp, ComponentT>; - - fn next(&mut self) -> Option<Self::Item> - { - let index = self.index; - - self.index += 1; - - self.relation.get(index) - } -} diff --git a/ecs/src/sole.rs b/ecs/src/sole.rs index 084a06b..1cce419 100644 --- a/ecs/src/sole.rs +++ b/ecs/src/sole.rs @@ -5,12 +5,11 @@ use std::ops::{Deref, DerefMut}; use std::sync::{Arc, Weak}; use crate::lock::{Lock, WriteGuard}; -use crate::system::{NoInitParamFlag, Param as SystemParam, System}; -use crate::type_name::TypeName; +use crate::system::{Param as SystemParam, System}; use crate::World; /// A type which has a single instance and is shared globally. -pub trait Sole: Any + TypeName +pub trait Sole: Any { fn drop_last(&self) -> bool; @@ -40,14 +39,6 @@ impl Debug for dyn Sole } } -impl TypeName for Box<dyn Sole> -{ - fn type_name(&self) -> &'static str - { - self.as_ref().type_name() - } -} - /// Holds a reference to a globally shared singleton value. #[derive(Debug)] pub struct Single<'world, SoleT: Sole> @@ -88,11 +79,10 @@ where } } -unsafe impl<'world, SoleT> SystemParam<'world> for Single<'world, SoleT> +impl<'world, SoleT> SystemParam<'world> for Single<'world, SoleT> where SoleT: Sole, { - type Flags = NoInitParamFlag; type Input = (); fn initialize<SystemImpl>( @@ -115,7 +105,7 @@ where } } -impl<'world, SoleT> Deref for Single<'world, SoleT> +impl<SoleT> Deref for Single<'_, SoleT> where SoleT: Sole, { @@ -127,7 +117,7 @@ where } } -impl<'world, SoleT> DerefMut for Single<'world, SoleT> +impl<SoleT> DerefMut for Single<'_, SoleT> where SoleT: Sole, { @@ -174,7 +164,7 @@ where _pd: PhantomData<&'weak_ref SoleT>, } -impl<'weak_ref, SoleT> SingleRef<'weak_ref, SoleT> +impl<SoleT> SingleRef<'_, SoleT> where SoleT: Sole, { diff --git a/ecs/src/system.rs b/ecs/src/system.rs index 046d25b..603c015 100644 --- a/ecs/src/system.rs +++ b/ecs/src/system.rs @@ -1,19 +1,12 @@ -use std::any::{type_name, Any}; +use std::any::Any; use std::convert::Infallible; use std::fmt::Debug; -use std::marker::PhantomData; -use std::ops::{Deref, DerefMut}; -use std::panic::{RefUnwindSafe, UnwindSafe}; +use ecs_macros::Component; use seq_macro::seq; -use crate::component::{ - Component, - FromOptional as FromOptionalComponent, - FromOptionalMut as FromOptionalMutComponent, -}; -use crate::lock::{ReadGuard, WriteGuard}; -use crate::tuple::{ReduceElement as TupleReduceElement, With as TupleWith}; +use crate::component::{Component, HandleMut as ComponentHandleMut}; +use crate::tuple::{ReduceElement as TupleReduceElement, Tuple}; use crate::World; pub mod stateful; @@ -33,7 +26,7 @@ pub trait System<'world, Impl>: 'static fn get_local_component_mut<LocalComponent: Component>( &self, - ) -> Option<ComponentRefMut<LocalComponent>>; + ) -> Option<ComponentHandleMut<LocalComponent>>; fn set_local_component<LocalComponent: Component>( &mut self, @@ -47,8 +40,8 @@ macro_rules! impl_system { impl<'world, Func, #(TParam~I,)*> System<'world, fn(#(TParam~I,)*)> for Func where - Func: Fn(#(TParam~I,)*) + Copy + RefUnwindSafe + UnwindSafe + 'static, - #(TParam~I: Param<'world, Flags = NoInitParamFlag>,)* + Func: Fn(#(TParam~I,)*) + Copy + 'static, + #(TParam~I: Param<'world, Input = ()>,)* { type Input = Infallible; @@ -92,7 +85,7 @@ macro_rules! impl_system { fn get_local_component_mut<LocalComponent: Component>( &self, - ) -> Option<ComponentRefMut<LocalComponent>> + ) -> Option<ComponentHandleMut<LocalComponent>> { panic!("System does not have any local components"); } @@ -121,7 +114,7 @@ pub trait Into<Impl> pub struct TypeErased { - data: Box<dyn Any + RefUnwindSafe + UnwindSafe>, + data: Box<dyn Any>, run: Box<TypeErasedRunFn>, } @@ -149,13 +142,12 @@ impl Debug for TypeErased } /// Function in [`TypeErased`] used to run the system. -type TypeErasedRunFn = dyn Fn(&dyn Any, &World) + RefUnwindSafe + UnwindSafe; +type TypeErasedRunFn = dyn Fn(&dyn Any, &World); /// A parameter to a [`System`]. -pub unsafe trait Param<'world> +pub trait Param<'world> { type Input; - type Flags; fn initialize<SystemImpl>( system: &mut impl System<'world, SystemImpl>, @@ -168,8 +160,6 @@ pub unsafe trait Param<'world> ) -> Self; } -pub struct NoInitParamFlag {} - /// A type which can be used as input to a [`System`]. pub trait Input: 'static {} @@ -179,9 +169,9 @@ pub struct ParamWithInputFilter; impl<InputT: Input, Accumulator> TupleReduceElement<Accumulator, ParamWithInputFilter> for InputT where - Accumulator: TupleWith<Self>, + Accumulator: Tuple, { - type Return = Accumulator::With; + type Return = Accumulator::WithElementAtEnd<Self>; } impl<Accumulator> TupleReduceElement<Accumulator, ParamWithInputFilter> for () @@ -189,128 +179,8 @@ impl<Accumulator> TupleReduceElement<Accumulator, ParamWithInputFilter> for () type Return = Accumulator; } -#[derive(Debug)] -pub struct ComponentRefMut<'a, ComponentT: Component> -{ - inner: WriteGuard<'a, Box<dyn Component>>, - _ph: PhantomData<ComponentT>, -} - -impl<'a, ComponentT: Component> ComponentRefMut<'a, ComponentT> -{ - pub(crate) fn new(inner: WriteGuard<'a, Box<dyn Component>>) -> Self - { - Self { inner, _ph: PhantomData } - } -} - -impl<'component, ComponentT: Component> FromOptionalMutComponent<'component> - for ComponentRefMut<'component, ComponentT> +#[derive(Debug, Component)] +pub(crate) struct SystemComponent { - fn from_optional_mut_component( - inner: Option<WriteGuard<'component, Box<dyn Component>>>, - _world: &'component World, - ) -> Self - { - Self { - inner: inner.unwrap_or_else(|| { - panic!( - "Component {} was not found in entity", - type_name::<ComponentT>() - ); - }), - _ph: PhantomData, - } - } -} - -impl<'comp, ComponentT> FromOptionalMutComponent<'comp> - for Option<ComponentRefMut<'comp, ComponentT>> -where - ComponentT: Component, -{ - fn from_optional_mut_component( - optional_component: Option<WriteGuard<'comp, Box<dyn Component>>>, - _world: &'comp World, - ) -> Self - { - optional_component.map(|component| ComponentRefMut::new(component)) - } -} - -impl<'a, ComponentT: Component> Deref for ComponentRefMut<'a, ComponentT> -{ - type Target = ComponentT; - - fn deref(&self) -> &Self::Target - { - self.inner.downcast_ref().unwrap() - } -} - -impl<'a, ComponentT: Component> DerefMut for ComponentRefMut<'a, ComponentT> -{ - fn deref_mut(&mut self) -> &mut Self::Target - { - self.inner.downcast_mut().unwrap() - } -} - -#[derive(Debug)] -pub struct ComponentRef<'a, ComponentT: Component> -{ - inner: ReadGuard<'a, Box<dyn Component>>, - _ph: PhantomData<ComponentT>, -} - -impl<'a, ComponentT: Component> ComponentRef<'a, ComponentT> -{ - pub(crate) fn new(inner: ReadGuard<'a, Box<dyn Component>>) -> Self - { - Self { inner, _ph: PhantomData } - } -} - -impl<'component, ComponentT: Component> FromOptionalComponent<'component> - for ComponentRef<'component, ComponentT> -{ - fn from_optional_component( - inner: Option<ReadGuard<'component, Box<dyn Component>>>, - _world: &'component World, - ) -> Self - { - Self { - inner: inner.unwrap_or_else(|| { - panic!( - "Component {} was not found in entity", - type_name::<ComponentT>() - ); - }), - _ph: PhantomData, - } - } -} - -impl<'comp, ComponentT> FromOptionalComponent<'comp> - for Option<ComponentRef<'comp, ComponentT>> -where - ComponentT: Component, -{ - fn from_optional_component( - optional_component: Option<ReadGuard<'comp, Box<dyn Component>>>, - _world: &'comp World, - ) -> Self - { - optional_component.map(|component| ComponentRef::new(component)) - } -} - -impl<'a, ComponentT: Component> Deref for ComponentRef<'a, ComponentT> -{ - type Target = ComponentT; - - fn deref(&self) -> &Self::Target - { - self.inner.downcast_ref().unwrap() - } + pub(crate) system: TypeErased, } diff --git a/ecs/src/system/stateful.rs b/ecs/src/system/stateful.rs index 536e6ed..54f6979 100644 --- a/ecs/src/system/stateful.rs +++ b/ecs/src/system/stateful.rs @@ -1,13 +1,12 @@ use std::any::{type_name, Any, TypeId}; -use std::collections::HashMap; use std::panic::{RefUnwindSafe, UnwindSafe}; +use hashbrown::HashMap; use seq_macro::seq; -use crate::component::Component; +use crate::component::{Component, HandleMut as ComponentHandleMut}; use crate::lock::Lock; use crate::system::{ - ComponentRefMut, Into as IntoSystem, Param, ParamWithInputFilter, @@ -15,10 +14,9 @@ use crate::system::{ TypeErased, }; use crate::tuple::{ - IntoInOptions as TupleIntoInOptions, Reduce as TupleReduce, - TakeOptionElementResult as TupleTakeOptionElementResult, - WithOptionElements as TupleWithOptionElements, + Tuple, + WithAllElemLtStatic as TupleWithAllElemLtStatic, }; use crate::uid::Uid; use crate::World; @@ -27,7 +25,7 @@ use crate::World; pub struct Stateful<Func> { func: Func, - local_components: HashMap<Uid, Lock<Box<dyn Component>>>, + local_components: HashMap<Uid, Lock<Box<dyn Any>>>, } macro_rules! impl_system { @@ -39,9 +37,10 @@ macro_rules! impl_system { Func: Fn(#(TParam~I,)*) + Copy + RefUnwindSafe + UnwindSafe + 'static, #(TParam~I: Param<'world>,)* #(TParam~I::Input: 'static,)* - (#(TParam~I::Input,)*): TupleReduce<ParamWithInputFilter>, - <(#(TParam~I::Input,)*) as TupleReduce<ParamWithInputFilter>>::Out: - TupleIntoInOptions + (#(TParam~I::Input,)*): TupleReduce< + ParamWithInputFilter, + Out: Tuple<InOptions: TupleWithAllElemLtStatic> + >, { type Input = <(#(TParam~I::Input,)*) as TupleReduce<ParamWithInputFilter>>::Out; @@ -50,35 +49,27 @@ macro_rules! impl_system { { let mut option_input = input.into_in_options(); + let mut index = 0; + #( if TypeId::of::<TParam~I::Input>() != TypeId::of::<()>() { - let input = match option_input.take::<TParam~I::Input>() { - TupleTakeOptionElementResult::Found(input) => input, - TupleTakeOptionElementResult::NotFound => { - panic!( - "Parameter input {} not found", - type_name::<TParam~I::Input>() - ); - } - TupleTakeOptionElementResult::AlreadyTaken => { - panic!( - concat!( - "Parameter {} is already initialized. ", - "System cannot contain multiple inputs with ", - "the same type", - ), - type_name::<TParam~I>() - ); - - } - }; + let input = option_input + .get_mut::<Option<TParam~I::Input>>(index) + .expect("Input element index out of range") + .take() + .expect("Input element is already taken"); TParam~I::initialize( &mut self, input ); + + #[allow(unused_assignments)] + { + index += 1; + } } )* @@ -118,14 +109,14 @@ macro_rules! impl_system { fn get_local_component_mut<LocalComponent: Component>( &self, - ) -> Option<ComponentRefMut<LocalComponent>> + ) -> Option<ComponentHandleMut<LocalComponent>> { let local_component = self.local_components .get(&LocalComponent::id())? .write_nonblock() .expect("Failed to aquire read-write local component lock"); - Some(ComponentRefMut::new(local_component)) + Some(ComponentHandleMut::new(local_component)) } fn set_local_component<LocalComponent: Component>( @@ -136,7 +127,10 @@ macro_rules! impl_system { self.local_components .insert( LocalComponent::id(), - Lock::new(Box::new(local_component)) + Lock::new( + Box::new(local_component), + type_name::<LocalComponent>() + ) ); } } diff --git a/ecs/src/tuple.rs b/ecs/src/tuple.rs index 1434592..def25a0 100644 --- a/ecs/src/tuple.rs +++ b/ecs/src/tuple.rs @@ -1,36 +1,44 @@ use std::any::TypeId; -use std::mem::{transmute_copy, ManuallyDrop}; use paste::paste; use seq_macro::seq; use util_macros::sub; -/// Used to append a element to a tuple type. -pub trait With<OtherElem> +pub trait Tuple: sealed::Sealed { - type With; -} - -/// Used to remove the last element of a tuple type. -/// -/// For example: `(u32, String, PathBuf)` becomes `(u32, String)`. -pub trait WithoutLast -{ - type Return; -} - -/// Used to make all elements of a tuple type wrapped in [`Option`]. -pub trait IntoInOptions -{ - type InOptions: WithOptionElements; - + /// `Self` with the given type added as the last element. + /// + /// `(String, i32, u8)::WithElementAtEnd<Path> = (String, i32, u8, Path)` + /// + /// # Important note + /// If `Self` has 16 elements, this will be `()`. The reason for this is that the + /// `Tuple` trait is only implemented for tuples with up to and including 16 elements. + type WithElementAtEnd<NewElem>: Tuple; + + /// `Self` without the last element. + /// + /// `(u16, AtomicU8)::WithoutLastElement = (u16,)` + type WithoutLastElement: Tuple; + + /// The last element of `Self`. + type LastElement; + + /// Self with all elements wrapped in [`Option`]. + type InOptions: Tuple; + + /// Pops the last element from this tuple, returning the new tuple and the popped + /// element. + fn pop_last(self) -> (Self::WithoutLastElement, Self::LastElement); + + /// Converts this tuple so that all elements are wrapped in [`Option`]. fn into_in_options(self) -> Self::InOptions; } -/// A tuple with all elements wrapped in [`Option`]. -pub trait WithOptionElements +/// A tuple with element types that all have the lifetime `'static`. +pub trait WithAllElemLtStatic: Tuple + sealed::Sealed { - fn take<Element: 'static>(&mut self) -> TakeOptionElementResult<Element>; + /// Returns the element at the given index. + fn get_mut<Element: 'static>(&mut self, index: usize) -> Option<&mut Element>; } /// Using the type system, reduces the elements of a tuple to a single one. Each element @@ -45,22 +53,6 @@ pub trait ReduceElement<Accumulator, Operation> type Return; } -/// The result of trying to [`take`] a element from a implementation of -/// [`WithOptionElements`]. -/// -/// [`take`]: WithOptionElements::take -pub enum TakeOptionElementResult<Element> -{ - /// The element was succesfully taken. - Found(Element), - - /// The element is not a element of the tuple. - NotFound, - - /// The element has already been taken - AlreadyTaken, -} - macro_rules! tuple_reduce_elem_tuple { (overflow) => { () @@ -95,13 +87,6 @@ macro_rules! elem_by_index { }; } -pub trait Pop: WithoutLast -{ - type LastElem; - - fn pop(self) -> (<Self as WithoutLast>::Return, Self::LastElem); -} - macro_rules! all_except_last { (start $($rest: tt)*) => { all_except_last!(@[] $($rest)*) @@ -131,19 +116,23 @@ macro_rules! all_except_last { macro_rules! impl_tuple_traits { ($cnt: tt) => { seq!(I in 0..$cnt { - impl<OtherElem, #(Elem~I,)*> With<OtherElem> for (#(Elem~I,)*) + impl<#(Elem~I,)*> Tuple for (#(Elem~I,)*) { - type With = (#(Elem~I,)* OtherElem,); - } + type WithElementAtEnd<NewElem> = (#(Elem~I,)* NewElem,); - impl<#(Elem~I,)*> WithoutLast for (#(Elem~I,)*) - { - type Return = all_except_last!(start #(Elem~I)*); - } + type WithoutLastElement = all_except_last!(start #(Elem~I)*); - impl<#(Element~I: 'static,)*> IntoInOptions for (#(Element~I,)*) - { - type InOptions = (#(Option<Element~I>,)*); + type LastElement = sub!($cnt - 1, elem_type_by_index); + + type InOptions = (#(Option<Elem~I>,)*); + + fn pop_last(self) -> (Self::WithoutLastElement, Self::LastElement) + { + ( + all_except_last!(start #((self.I))*), + sub!($cnt - 1, elem_by_index, (self)) + ) + } fn into_in_options(self) -> Self::InOptions { @@ -152,32 +141,28 @@ macro_rules! impl_tuple_traits { } } - impl<#(Element~I: 'static,)*> WithOptionElements for (#(Option<Element~I>,)*) + impl<#(Elem~I: 'static,)*> WithAllElemLtStatic for (#(Elem~I,)*) { - fn take<Element: 'static>(&mut self) - -> TakeOptionElementResult<Element> + fn get_mut<Element: 'static>(&mut self, index: usize) -> Option<&mut Element> { - #( - if TypeId::of::<Element~I>() == TypeId::of::<Element>() { - let input = match self.I.take() { - Some(input) => ManuallyDrop::new(input), - None => { - return TakeOptionElementResult::AlreadyTaken; - } - }; - - return TakeOptionElementResult::Found( - // SAFETY: It can be transmuted safely since it is the - // same type and the type is 'static - unsafe { transmute_copy(&input) } - ); - } - )* - - TakeOptionElementResult::NotFound + match index { + #( + I => { + assert!(TypeId::of::<Element>() == TypeId::of::<Elem~I>()); + + // SAFETY: It is checked above that the type is correct + Some(unsafe { &mut *(&raw mut self.I).cast::<Element>() }) + } + )* + _ => None + } } } + impl<#(Elem~I,)*> sealed::Sealed for (#(Elem~I,)*) + { + } + paste! { impl<Operation, #(Elem~I,)*> Reduce<Operation> for (#(Elem~I,)*) where @@ -190,19 +175,6 @@ macro_rules! impl_tuple_traits { type Out = sub!($cnt - 1, tuple_reduce_elem_tuple); } } - - impl<#(Elem~I,)*> Pop for (#(Elem~I,)*) - { - type LastElem = sub!($cnt - 1, elem_type_by_index); - - fn pop(self) -> (<Self as WithoutLast>::Return, Self::LastElem) - { - ( - all_except_last!(start #((self.I))*), - sub!($cnt - 1, elem_by_index, (self)) - ) - } - } }); }; } @@ -210,3 +182,57 @@ macro_rules! impl_tuple_traits { seq!(N in 0..16 { impl_tuple_traits!(N); }); + +seq!(I in 0..16 { + impl<#(Elem~I,)*> Tuple for (#(Elem~I,)*) + { + type WithElementAtEnd<NewElem> = (); + + type WithoutLastElement = all_except_last!(start #(Elem~I)*); + + type LastElement = Elem15; + + type InOptions = (#(Option<Elem~I>,)*); + + fn pop_last(self) -> (Self::WithoutLastElement, Self::LastElement) + { + ( + all_except_last!(start #((self.I))*), + self.15 + ) + } + + fn into_in_options(self) -> Self::InOptions + { + #![allow(clippy::unused_unit)] + (#(Some(self.I),)*) + } + } + + impl<#(Elem~I: 'static,)*> WithAllElemLtStatic for (#(Elem~I,)*) + { + fn get_mut<Element: 'static>(&mut self, index: usize) -> Option<&mut Element> + { + match index { + #( + I => { + assert!(TypeId::of::<Element>() == TypeId::of::<Elem~I>()); + + // SAFETY: It is checked above that the type is correct + Some(unsafe { &mut *(&raw mut self.I).cast::<Element>() }) + } + )* + _ => None + } + } + } + + impl<#(Elem~I,)*> sealed::Sealed for (#(Elem~I,)*) + { + } +}); + +mod sealed +{ + pub trait Sealed {} +} diff --git a/ecs/src/type_name.rs b/ecs/src/type_name.rs deleted file mode 100644 index 54179be..0000000 --- a/ecs/src/type_name.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::any::type_name; - -pub trait TypeName -{ - /// Returns the name of this type. - fn type_name(&self) -> &'static str; -} - -impl<Item> TypeName for Vec<Item> -{ - fn type_name(&self) -> &'static str - { - type_name::<Self>() - } -} diff --git a/ecs/src/uid.rs b/ecs/src/uid.rs index 0e5d88a..feed62c 100644 --- a/ecs/src/uid.rs +++ b/ecs/src/uid.rs @@ -1,21 +1,29 @@ +use std::fmt::{Debug, Display, Formatter}; use std::mem::transmute; use std::sync::atomic::{AtomicU32, Ordering}; -static NEXT: AtomicU32 = AtomicU32::new(1); +use crate::component::Component; +use crate::util::{gen_mask_64, BitMask, NumberExt}; -// Bit 0 and 1 for the kind -const KIND_BITS: u64 = 0x03; +static NEXT: AtomicU32 = AtomicU32::new(Uid::FIRST_UNIQUE_ID); + +static WILDCARD_ID: u32 = 1; + +const ID_BITS: BitMask<u64> = BitMask::new(gen_mask_64!(32..=63)); +const RELATION_BITS: BitMask<u64> = BitMask::new(gen_mask_64!(6..=31)); +const KIND_BITS: BitMask<u64> = BitMask::new(gen_mask_64!(0..=1)); #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] #[repr(u8)] pub enum Kind { + Pair = 3, Entity = 2, Component = 1, } -/// Unique entity/component ID. -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] +/// A unique identifier. +#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct Uid { inner: u64, @@ -23,21 +31,213 @@ pub struct Uid impl Uid { + /// The id part of the first unique `Uid`. The ids `0..Uid::FIRST_UNIQUE_ID` are + /// reserved. + pub const FIRST_UNIQUE_ID: u32 = 5; + /// Returns a new unique entity/component ID. pub fn new_unique(kind: Kind) -> Self { - let id_part = NEXT.fetch_add(1, Ordering::Relaxed); + let id = NEXT.fetch_add(1, Ordering::Relaxed); + + Self { + inner: ID_BITS.field_prep(u64::from(id)) | KIND_BITS.field_prep(kind as u64), + } + } + #[must_use] + pub fn wildcard() -> Self + { Self { - inner: (u64::from(id_part) << 32) | kind as u64, + inner: ID_BITS.field_prep(u64::from(WILDCARD_ID)) + | KIND_BITS.field_prep(Kind::Component as u64), } } + /// Returns a new pair UID. + /// + /// # Panics + /// Will panic if either the given relation or target is a pair UID. + #[must_use] + pub fn new_pair(params: &PairParams) -> Self + { + assert_ne!( + params.relation.kind(), + Kind::Pair, + "Pair relation cannot be a pair" + ); + + assert_ne!( + params.target.kind(), + Kind::Pair, + "Pair target cannot be a pair" + ); + + Self { + inner: ID_BITS.field_prep(u64::from(params.target.id())) + | RELATION_BITS.field_prep(u64::from(params.relation.id())) + | KIND_BITS.field_prep(Kind::Pair as u64), + } + } + + #[must_use] + pub fn id(&self) -> u32 + { + let Ok(id) = u32::try_from(self.inner.field_get(ID_BITS)) else { + unreachable!("Uid id does not fit in u32"); + }; + + id + } + #[must_use] pub fn kind(&self) -> Kind { + let Ok(kind) = u8::try_from(self.inner.field_get(KIND_BITS)) else { + unreachable!("Uid kind does not fit in u8"); + }; + // SAFETY: The kind bits cannot be invalid since they are set using the Kind enum // in the new_unique function - unsafe { transmute((self.inner & KIND_BITS) as u8) } + unsafe { transmute::<u8, Kind>(kind) } + } + + /// If this `Uid` is a pair, returns the relation as a component `Uid`. + /// + /// # Panics + /// Will panic if this `Uid` is not a pair. + #[must_use] + pub fn relation_component(&self) -> Self + { + assert_eq!(self.kind(), Kind::Pair, "Uid is not a pair"); + + Self { + inner: ID_BITS.field_prep(u64::from(self.relation())) + | KIND_BITS.field_prep(Kind::Component as u64), + } + } + + #[must_use] + pub fn has_same_relation_as(&self, other: Self) -> bool + { + self.relation() == other.relation() + } + + /// If this `Uid` is a pair, returns the relation as a entity `Uid`. + /// + /// # Panics + /// Will panic if this `Uid` is not a pair. + #[must_use] + pub fn relation_entity(&self) -> Self + { + assert_eq!(self.kind(), Kind::Pair, "Uid is not a pair"); + + Self { + inner: ID_BITS.field_prep(u64::from(self.relation())) + | KIND_BITS.field_prep(Kind::Entity as u64), + } + } + + /// If this `Uid` is a pair, returns the target as a component `Uid`. + /// + /// # Panics + /// Will panic if this `Uid` is not a pair. + #[must_use] + pub fn target_component(&self) -> Self + { + assert_eq!(self.kind(), Kind::Pair, "Uid is not a pair"); + + Self { + inner: ID_BITS.field_prep(u64::from(self.id())) + | KIND_BITS.field_prep(Kind::Component as u64), + } + } + + /// If this `Uid` is a pair, returns the target as a entity `Uid`. + /// + /// # Panics + /// Will panic if this `Uid` is not a pair. + #[must_use] + pub fn target_entity(&self) -> Self + { + assert_eq!(self.kind(), Kind::Pair, "Uid is not a pair"); + + Self { + inner: ID_BITS.field_prep(u64::from(self.id())) + | KIND_BITS.field_prep(Kind::Entity as u64), + } + } + + fn relation(self) -> u32 + { + let Ok(relation) = u32::try_from(self.inner.field_get(RELATION_BITS)) else { + unreachable!("Uid relation does not fit in u32"); + }; + + relation + } +} + +impl Debug for Uid +{ + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result + { + formatter + .debug_struct("Uid") + .field("id", &self.id()) + .field("kind", &self.kind()) + .finish_non_exhaustive() + } +} + +impl Display for Uid +{ + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result + { + if self.kind() == Kind::Pair { + return write!( + formatter, + "({}, {})", + self.relation(), + self.target_component() + ); + } + + if *self == Uid::wildcard() { + return write!(formatter, "*"); + } + + write!(formatter, "{}", self.id()) + } +} + +#[derive(Debug, Clone)] +pub struct PairParams +{ + pub relation: Uid, + pub target: Uid, +} + +pub trait With: 'static +{ + fn uid() -> Uid; +} + +impl<ComponentT: Component> With for ComponentT +{ + fn uid() -> Uid + { + Self::id() + } +} + +#[derive(Debug)] +pub enum Wildcard {} + +impl With for Wildcard +{ + fn uid() -> Uid + { + Uid::wildcard() } } diff --git a/ecs/src/util.rs b/ecs/src/util.rs index 4480fc8..9ab4dc6 100644 --- a/ecs/src/util.rs +++ b/ecs/src/util.rs @@ -1,3 +1,186 @@ +use std::hash::Hash; +use std::mem::transmute; +use std::ops::{BitAnd, Deref}; + +use hashbrown::HashMap; + +pub(crate) mod array_vec; + +pub trait VecExt<Item> +{ + fn insert_at_partition_point_by_key<Key>( + &mut self, + item: Item, + func: impl FnMut(&Item) -> Key, + ) where + Key: Ord; +} + +impl<Item> VecExt<Item> for Vec<Item> +{ + fn insert_at_partition_point_by_key<Key>( + &mut self, + item: Item, + mut func: impl FnMut(&Item) -> Key, + ) where + Key: Ord, + { + let key = func(&item); + + let insert_index = self.partition_point(|other_item| func(other_item) <= key); + + self.insert(insert_index, item); + } +} + +pub trait StreamingIterator +{ + type Item<'a> + where + Self: 'a; + + fn streaming_next(&mut self) -> Option<Self::Item<'_>>; + + fn streaming_map<NewItem, Func>(self, func: Func) -> StreamingMap<Self, Func> + where + Self: Sized, + Func: FnMut(Self::Item<'_>) -> NewItem, + { + StreamingMap { iter: self, func } + } + + fn streaming_find<'this, Predicate>( + &'this mut self, + mut predicate: Predicate, + ) -> Option<Self::Item<'this>> + where + Self: Sized, + Predicate: FnMut(&Self::Item<'this>) -> bool, + { + while let Some(item) = unsafe { + transmute::<Option<Self::Item<'_>>, Option<Self::Item<'_>>>( + self.streaming_next(), + ) + } { + if predicate(&item) { + return Some(item); + } + } + + None + } +} + +pub struct StreamingMap<Iter, Func> +{ + iter: Iter, + func: Func, +} + +impl<Iter, Func, Item> StreamingIterator for StreamingMap<Iter, Func> +where + Iter: StreamingIterator, + Func: FnMut(Iter::Item<'_>) -> Item, +{ + type Item<'a> + = Item + where + Iter: 'a, + Func: 'a; + + fn streaming_next(&mut self) -> Option<Self::Item<'_>> + { + Some((self.func)(self.iter.streaming_next()?)) + } +} + +#[derive(Debug)] +pub enum BorrowedOrOwned<'a, Value> +{ + Borrowned(&'a Value), + Owned(Value), +} + +impl<Value> Deref for BorrowedOrOwned<'_, Value> +{ + type Target = Value; + + fn deref(&self) -> &Self::Target + { + match self { + Self::Borrowned(value) => value, + Self::Owned(value) => value, + } + } +} + +#[derive(Debug, Clone)] +pub enum Either<A, B> +{ + A(A), + B(B), +} + +impl<A, B> Iterator for Either<A, B> +where + A: Iterator, + B: Iterator<Item = A::Item>, +{ + type Item = A::Item; + + fn next(&mut self) -> Option<Self::Item> + { + match self { + Self::A(a) => a.next(), + Self::B(b) => b.next(), + } + } +} + +pub trait HashMapExt<Key, Value> +{ + /// Returns true if the keys are a subset of another [`HashMap`]'s keys, i.e., `other` + /// contains at least all the keys in `self`. + fn keys_is_subset(&self, other: &Self) -> bool; + + /// Returns true if the keys are a superset of another [`HashMap`]'s keys, i.e., + /// `self` contains at least all the keys in `other`. + fn keys_is_superset(&self, other: &Self) -> bool; +} + +impl<Key, Value> HashMapExt<Key, Value> for HashMap<Key, Value> +where + Key: Eq + Hash, +{ + fn keys_is_subset(&self, other: &Self) -> bool + { + if self.len() <= other.len() { + self.keys().all(|key| other.contains_key(key)) + } else { + false + } + } + + fn keys_is_superset(&self, other: &Self) -> bool + { + other.keys_is_subset(self) + } +} + +pub trait Array<Item>: + AsRef<[Item]> + + AsMut<[Item]> + + IntoIterator<Item = Item> + + Into<Vec<Item>> + + Sortable<Item = Item> + + sealed::Sealed +{ +} + +impl<Item, const CNT: usize> Array<Item> for [Item; CNT] {} + +impl<Item, const CNT: usize> sealed::Sealed for [Item; CNT] {} + pub trait Sortable { type Item; @@ -46,3 +229,105 @@ impl<Item> Sortable for Vec<Item> self.sort_by_key(func); } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BitMask<Value> +{ + mask: Value, +} + +impl BitMask<u64> +{ + #[must_use] + pub const fn new(mask: u64) -> Self + { + Self { mask } + } + + #[must_use] + pub const fn value(self) -> u64 + { + self.mask + } + + /// Prepares a bitfield value in the range of bits specified by this `BitMask`. + #[must_use] + pub const fn field_prep(self, field_value: u64) -> u64 + { + debug_assert!(field_value < 1 << self.mask.count_ones()); + + ((field_value) << self.mask.trailing_zeros()) & (self.mask) + } +} + +impl BitAnd<u64> for BitMask<u64> +{ + type Output = u64; + + fn bitand(self, rhs: u64) -> Self::Output + { + self.mask & rhs + } +} + +pub trait NumberExt: Sized +{ + /// Returns a range of bits (field) specified by the provided [`BitMask`]. + #[must_use] + fn field_get(self, field_mask: BitMask<Self>) -> Self; +} + +impl NumberExt for u64 +{ + fn field_get(self, field_mask: BitMask<Self>) -> Self + { + (field_mask & self) >> field_mask.value().trailing_zeros() + } +} + +macro_rules! gen_mask_64 { + ($low: literal..=$high: literal) => { + const { + if $high <= $low { + panic!("High bit index cannot be less than or equal to low bit index"); + } + + (((!0u64) - (1u64 << ($low)) + 1) + & (!0u64 >> (u64::BITS as u64 - 1 - ($high)))) + } + }; +} + +pub(crate) use gen_mask_64; + +mod sealed +{ + pub trait Sealed {} +} + +#[cfg(test)] +mod tests +{ + + use super::BitMask; + use crate::util::NumberExt; + + #[test] + fn field_get_works() + { + assert_eq!(0b11011u64.field_get(BitMask::new(0b11100)), 0b00110); + } + + #[test] + fn bitmask_field_prep_works() + { + assert_eq!(BitMask::new(0b11000).field_prep(3), 0b11000); + } + + #[test] + #[should_panic] + fn bitmask_field_prep_too_large_value_panics() + { + let _ = BitMask::new(0b001110).field_prep(9); + } +} diff --git a/ecs/src/util/array_vec.rs b/ecs/src/util/array_vec.rs new file mode 100644 index 0000000..a37b1f9 --- /dev/null +++ b/ecs/src/util/array_vec.rs @@ -0,0 +1,131 @@ +use std::mem::MaybeUninit; +use std::ops::{Deref, DerefMut}; + +#[derive(Debug)] +pub struct ArrayVec<Item, const CAPACITY: usize> +{ + items: [MaybeUninit<Item>; CAPACITY], + len: usize, +} + +impl<Item, const CAPACITY: usize> ArrayVec<Item, CAPACITY> +{ + #[inline] + #[must_use] + pub fn len(&self) -> usize + { + self.len + } + + #[inline] + #[must_use] + pub fn is_empty(&self) -> bool + { + self.len == 0 + } + + pub fn push(&mut self, item: Item) + { + assert!(self.len < CAPACITY); + + self.items[self.len].write(item); + + self.len += 1; + } + + pub fn insert(&mut self, index: usize, item: Item) + { + assert!(index <= self.len); + assert!(self.len < CAPACITY); + + if index == self.len { + self.push(item); + return; + } + + unsafe { + std::ptr::copy( + &self.items[index], + &mut self.items[index + 1], + self.len - index, + ); + } + + self.items[index].write(item); + + self.len += 1; + } +} + +impl<Item, const CAPACITY: usize> Extend<Item> for ArrayVec<Item, CAPACITY> +{ + fn extend<IntoIter: IntoIterator<Item = Item>>(&mut self, iter: IntoIter) + { + for item in iter { + self.push(item); + } + } +} + +impl<Item, const CAPACITY: usize> AsRef<[Item]> for ArrayVec<Item, CAPACITY> +{ + fn as_ref(&self) -> &[Item] + { + let ptr = &raw const self.items[..self.len]; + + unsafe { &*(ptr as *const [Item]) } + } +} + +impl<Item, const CAPACITY: usize> AsMut<[Item]> for ArrayVec<Item, CAPACITY> +{ + fn as_mut(&mut self) -> &mut [Item] + { + let ptr = &raw mut self.items[..self.len]; + + unsafe { &mut *(ptr as *mut [Item]) } + } +} + +impl<Item, const CAPACITY: usize> Deref for ArrayVec<Item, CAPACITY> +{ + type Target = [Item]; + + fn deref(&self) -> &Self::Target + { + self.as_ref() + } +} + +impl<Item, const CAPACITY: usize> DerefMut for ArrayVec<Item, CAPACITY> +{ + fn deref_mut(&mut self) -> &mut Self::Target + { + self.as_mut() + } +} + +impl<Item, const CAPACITY: usize> Default for ArrayVec<Item, CAPACITY> +{ + fn default() -> Self + { + Self { + items: [const { MaybeUninit::uninit() }; CAPACITY], + len: 0, + } + } +} + +impl<Item, const CAPACITY: usize> Drop for ArrayVec<Item, CAPACITY> +{ + fn drop(&mut self) + { + for item in &mut self.items[..self.len] { + // SAFETY: The items from index 0 to the length index will always be + // initialized and satisfy all the invariants of the Item type. + unsafe { + item.assume_init_drop(); + } + } + } +} diff --git a/ecs/tests/query.rs b/ecs/tests/query.rs new file mode 100644 index 0000000..062fd5a --- /dev/null +++ b/ecs/tests/query.rs @@ -0,0 +1,322 @@ +use ecs::component::Component; +use ecs::query::term::Without; +use ecs::query::{ + TermWithFieldTuple as QueryTermWithFieldTuple, + TermWithoutFieldTuple as QueryTermWithoutFieldTuple, +}; +use ecs::uid::Uid; +use ecs::{Component, Query, World}; +use parking_lot::{Mutex, Once}; + +pub static SETUP: Once = Once::new(); + +pub static TEST_LOCK: Mutex<()> = Mutex::new(()); + +#[derive(Component)] +struct A; + +#[derive(Component)] +struct B; + +#[derive(Component)] +struct C; + +#[derive(Component)] +struct D; + +#[derive(Component)] +struct E; + +#[derive(Component)] +struct F; + +#[derive(Component)] +struct G; + +fn setup() +{ + SETUP.call_once_force(|_| { + assert_eq!(A::id().id(), Uid::FIRST_UNIQUE_ID); + assert_eq!(B::id().id(), Uid::FIRST_UNIQUE_ID + 1); + assert_eq!(C::id().id(), Uid::FIRST_UNIQUE_ID + 2); + assert_eq!(D::id().id(), Uid::FIRST_UNIQUE_ID + 3); + assert_eq!(E::id().id(), Uid::FIRST_UNIQUE_ID + 4); + assert_eq!(F::id().id(), Uid::FIRST_UNIQUE_ID + 5); + assert_eq!(G::id().id(), Uid::FIRST_UNIQUE_ID + 6); + }); +} + +fn assert_query_finds_ents<QueryFieldTerms, QueryFieldlessTerms>( + query: Query<'_, QueryFieldTerms, QueryFieldlessTerms>, + mut expected_ent_ids: Vec<Uid>, +) where + QueryFieldTerms: QueryTermWithFieldTuple, + QueryFieldlessTerms: QueryTermWithoutFieldTuple, +{ + assert!( + query.iter_with_euids().all(|(ent_id, _)| { + let Some(index) = expected_ent_ids + .iter() + .position(|expected_id| *expected_id == ent_id) + else { + return false; + }; + + expected_ent_ids.remove(index); + + true + }), + "Unexpected entity was found. Expected entities left: {expected_ent_ids:?}" + ); + + assert_eq!( + expected_ent_ids.len(), + 0, + concat!( + "Not all entities expected to be found was found. ", + "Expected entities left: {:?}" + ), + expected_ent_ids + ); +} + +#[test] +fn query_archetype_exists_with_edges_to_next_archetypes() +{ + setup(); + + let _test_lock = TEST_LOCK.lock(); + + let mut world = World::new(); + + let ent_1_id = world.create_entity((A, B, C)); + let ent_2_id = world.create_entity((A, B, C, D, E)); + let ent_3_id = world.create_entity((A, B, C, E)); + let ent_4_id = world.create_entity((A, B, C, G, F)); + + assert_query_finds_ents( + world.query::<(&A, &B, &C), ()>(), + vec![ent_1_id, ent_2_id, ent_3_id, ent_4_id], + ); +} + +#[test] +fn query_archetype_exists_with_2_comps_diff_to_next_archetype() +{ + setup(); + + let _test_lock = TEST_LOCK.lock(); + + let mut world = World::new(); + + let ent_1_id = world.create_entity((A, B, C, D, F)); + + let ent_2_id = world.create_entity((A, B, F)); + + assert_query_finds_ents(world.query::<(&A, &B, &F), ()>(), vec![ent_1_id, ent_2_id]); +} + +#[test] +fn query_archetype_exists_with_2_comps_diff_to_next_archetype_rev() +{ + setup(); + + let _test_lock = TEST_LOCK.lock(); + + let mut world = World::new(); + + let ent_1_id = world.create_entity((A, B, F)); + + let ent_2_id = world.create_entity((A, B, C, D, F)); + + assert_query_finds_ents(world.query::<(&A, &B, &F), ()>(), vec![ent_1_id, ent_2_id]); +} + +#[test] +fn query_archetype_exists_with_3_comps_diff_to_next_archetype() +{ + setup(); + + let _test_lock = TEST_LOCK.lock(); + + let mut world = World::new(); + + let ent_1_id = world.create_entity((A, B, C, D, E, F)); + + let ent_2_id = world.create_entity((A, B, F)); + + assert_query_finds_ents(world.query::<(&A, &B, &F), ()>(), vec![ent_1_id, ent_2_id]); +} + +#[test] +fn query_archetype_exists_with_3_comps_diff_to_next_archetype_rev() +{ + setup(); + + let _test_lock = TEST_LOCK.lock(); + + let mut world = World::new(); + + let ent_1_id = world.create_entity((A, B, F)); + + let ent_2_id = world.create_entity((A, B, C, D, E, F)); + + assert_query_finds_ents(world.query::<(&A, &B, &F), ()>(), vec![ent_1_id, ent_2_id]); +} + +#[test] +fn query_archetype_exists_with_4_comps_diff_to_next_archetype() +{ + setup(); + + let _test_lock = TEST_LOCK.lock(); + + let mut world = World::new(); + + let ent_1_id = world.create_entity((A, B, C, D, E, F, G)); + + let ent_2_id = world.create_entity((A, B, G)); + + assert_query_finds_ents(world.query::<(&A, &B, &G), ()>(), vec![ent_1_id, ent_2_id]); +} + +#[test] +fn query_archetype_exists_with_4_comps_diff_to_next_archetype_rev() +{ + setup(); + + let _test_lock = TEST_LOCK.lock(); + + let mut world = World::new(); + + let ent_1_id = world.create_entity((A, B, G)); + + let ent_2_id = world.create_entity((A, B, C, D, E, F, G)); + + assert_query_finds_ents(world.query::<(&A, &B, &G), ()>(), vec![ent_1_id, ent_2_id]); +} + +#[test] +fn query_archetype_exists_with_4_comps_diff_to_next_archetype_and_opt_comp() +{ + setup(); + + let _test_lock = TEST_LOCK.lock(); + + let mut world = World::new(); + + let ent_1_id = world.create_entity((A, B, C, D, E, F, G)); + + let ent_2_id = world.create_entity((A, B, G)); + + assert_query_finds_ents( + world.query::<(&A, Option<&E>, &G), ()>(), + vec![ent_1_id, ent_2_id], + ); +} + +#[test] +fn query_archetype_nonexistant() +{ + setup(); + + let _test_lock = TEST_LOCK.lock(); + + let mut world = World::new(); + + world.create_entity((A, B, C)); + + let ent_2_id = world.create_entity((A, B, C, D, E)); + let ent_3_id = world.create_entity((A, B, C, E)); + + world.create_entity((A, B, C, G, F)); + + assert_query_finds_ents(world.query::<(&A, &E), ()>(), vec![ent_2_id, ent_3_id]); +} + +#[test] +fn query_archetype_nonexistant_and_opt_comp() +{ + setup(); + + let _test_lock = TEST_LOCK.lock(); + + let mut world = World::new(); + + world.create_entity((A, B, C)); + let ent_2_id = world.create_entity((A, B, C, D, E)); + let ent_3_id = world.create_entity((A, B, C, E)); + world.create_entity((A, B, C, G, F)); + + assert_query_finds_ents( + world.query::<(&A, &E, Option<&D>), ()>(), + vec![ent_2_id, ent_3_id], + ); +} + +#[test] +fn query_without_comp_and_archetype_exists() +{ + setup(); + + let _test_lock = TEST_LOCK.lock(); + + let mut world = World::new(); + + let ent_1_id = world.create_entity((A, B, C)); + + world.create_entity((A, B, C, E)); + world.create_entity((A, B, C, F, E)); + + let ent_2_id = world.create_entity((A, B, C, G)); + let ent_3_id = world.create_entity((A, B, C, G, F)); + + assert_query_finds_ents( + world.query::<(&A, &B, &C), (Without<E>,)>(), + vec![ent_1_id, ent_2_id, ent_3_id], + ); +} + +#[test] +fn query_without_required_comp_and_archetype_exists() +{ + setup(); + + let _test_lock = TEST_LOCK.lock(); + + let mut world = World::new(); + + world.create_entity((A, B, C)); + + world.create_entity((A, B, C, E)); + world.create_entity((A, B, C, F, E)); + + world.create_entity((A, B, C, G)); + world.create_entity((A, B, C, G, F)); + + assert_query_finds_ents(world.query::<(&A, &B), (Without<B>,)>(), vec![]); +} + +#[test] +fn query_without_comp_and_archetype_nonexistant() +{ + setup(); + + let _test_lock = TEST_LOCK.lock(); + + let mut world = World::new(); + + world.create_entity((A, B, C)); + + let ent_1_id = world.create_entity((A, B, C, E)); + + world.create_entity((A, B, C, F, E)); + + let ent_2_id = world.create_entity((A, B, C, G, E)); + world.create_entity((A, B, C, G, F, E)); + + assert_query_finds_ents( + world.query::<(&A, &E), (Without<F>,)>(), + vec![ent_1_id, ent_2_id], + ); +} diff --git a/engine/Cargo.toml b/engine/Cargo.toml index b3868ac..a62f458 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -3,20 +3,19 @@ name = "engine" version = "0.1.0" edition = "2021" -[features] -debug = ["dep:tracing"] - [dependencies] glfw = { path = "../glfw", features = ["opengl"] } thiserror = "1.0.49" gl = "0.14.0" bitflags = "2.4.0" -tracing = { version = "0.1.39", optional = true } +tracing = "0.1.39" seq-macro = "0.3.5" paste = "1.0.14" ecs = { path = "../ecs" } +util-macros = { path = "../util-macros" } -[dependencies.image] +[dependencies.image_rs] version = "0.24.7" default-features = false features = ["png", "jpeg"] +package = "image" diff --git a/engine/src/asset.rs b/engine/src/asset.rs new file mode 100644 index 0000000..db4d23c --- /dev/null +++ b/engine/src/asset.rs @@ -0,0 +1,777 @@ +use std::any::{type_name, Any}; +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::HashMap; +use std::convert::Infallible; +use std::ffi::{OsStr, OsString}; +use std::fmt::{Debug, Display}; +use std::hash::{DefaultHasher, Hash, Hasher}; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; +use std::sync::mpsc::{ + channel as mpsc_channel, + Receiver as MpscReceiver, + Sender as MpscSender, +}; +use std::sync::Arc; + +use ecs::phase::PRE_UPDATE as PRE_UPDATE_PHASE; +use ecs::sole::Single; +use ecs::Sole; + +use crate::work_queue::{Work, WorkQueue}; + +/// Asset label. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Label<'a> +{ + pub path: Cow<'a, Path>, + pub name: Option<Cow<'a, str>>, +} + +impl Label<'_> +{ + pub fn to_owned(&self) -> LabelOwned + { + LabelOwned { + path: self.path.to_path_buf(), + name: self.name.as_ref().map(|name| name.to_string()), + } + } +} + +impl<'a> From<&'a Path> for Label<'a> +{ + fn from(path: &'a Path) -> Self + { + Self { path: path.into(), name: None } + } +} + +impl From<PathBuf> for Label<'_> +{ + fn from(path: PathBuf) -> Self + { + Self { path: path.into(), name: None } + } +} + +impl<'a> From<&'a LabelOwned> for Label<'a> +{ + fn from(label: &'a LabelOwned) -> Self + { + Self { + path: (&label.path).into(), + name: label.name.as_ref().map(|name| Cow::Borrowed(name.as_str())), + } + } +} + +impl Display for Label<'_> +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + write!(formatter, "{}", self.path.display())?; + + if let Some(name) = &self.name { + formatter.write_str("::")?; + formatter.write_str(&name)?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LabelOwned +{ + pub path: PathBuf, + pub name: Option<String>, +} + +impl LabelOwned +{ + pub fn to_label(&self) -> Label<'_> + { + Label { + path: (&self.path).into(), + name: self.name.as_ref().map(|name| Cow::Borrowed(name.as_str())), + } + } +} + +impl Display for LabelOwned +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + write!(formatter, "{}", self.path.display())?; + + if let Some(name) = &self.name { + formatter.write_str("::")?; + formatter.write_str(&name)?; + } + + Ok(()) + } +} + +#[derive(Debug, Sole)] +pub struct Assets +{ + assets: Vec<StoredAsset>, + asset_lookup: RefCell<HashMap<LabelHash, LookupEntry>>, + importers: Vec<WrappedImporterFn>, + importer_lookup: HashMap<OsString, usize>, + import_work_queue: WorkQueue<ImportWorkUserData>, + import_work_msg_receiver: MpscReceiver<ImportWorkMessage>, + import_work_msg_sender: MpscSender<ImportWorkMessage>, +} + +impl Assets +{ + pub fn with_capacity(capacity: usize) -> Self + { + let (import_work_msg_sender, import_work_msg_receiver) = + mpsc_channel::<ImportWorkMessage>(); + + Self { + assets: Vec::with_capacity(capacity), + asset_lookup: RefCell::new(HashMap::with_capacity(capacity)), + importers: Vec::new(), + importer_lookup: HashMap::new(), + import_work_queue: WorkQueue::new(), + import_work_msg_receiver, + import_work_msg_sender, + } + } + + pub fn set_importer<'file_ext, AssetSettings, Err>( + &mut self, + file_extensions: impl IntoIterator<Item: Into<Cow<'file_ext, str>>>, + func: impl Fn(&mut Submitter<'_>, &Path, Option<&AssetSettings>) -> Result<(), Err>, + ) where + AssetSettings: 'static, + Err: std::error::Error + 'static, + { + self.importers.push(WrappedImporterFn::new(func)); + + let importer_index = self.importers.len() - 1; + + self.importer_lookup + .extend(file_extensions.into_iter().map(|file_ext| { + let file_ext: Cow<str> = file_ext.into(); + + (file_ext.into_owned().into(), importer_index) + })); + } + + #[tracing::instrument(skip_all, fields(asset_type=type_name::<Asset>()))] + pub fn get<'this, 'handle, Asset: 'static + Send + Sync>( + &'this self, + handle: &'handle Handle<Asset>, + ) -> Option<&'handle Asset> + where + 'this: 'handle, + { + let LookupEntry::Occupied(asset_index) = + *self.asset_lookup.borrow().get(&handle.id.label_hash)? + else { + return None; + }; + + let stored_asset = self.assets.get(asset_index).expect("Not possible"); + + let Some(asset) = stored_asset.strong.downcast_ref::<Asset>() else { + tracing::error!("Wrong asset type"); + return None; + }; + + Some(asset) + } + + #[tracing::instrument(skip(self))] + pub fn load<'i, Asset: 'static + Send + Sync>( + &self, + label: impl Into<Label<'i>> + Debug, + ) -> Handle<Asset> + { + let label = label.into(); + + let label_hash = LabelHash::new(&label); + + let mut asset_lookup = self.asset_lookup.borrow_mut(); + + if Self::is_pending(&asset_lookup, &label) { + return Handle::new(label_hash); + } + + let Some(lookup_entry) = asset_lookup.get(&label_hash) else { + self.add_import_work::<Infallible>( + &label, + label_hash, + None, + &mut asset_lookup, + ); + + return Handle::new(label_hash); + }; + + match *lookup_entry { + LookupEntry::Occupied(asset_index) => { + let stored_asset = self.assets.get(asset_index).expect("Not possible"); + + if stored_asset.strong.downcast_ref::<Asset>().is_none() { + tracing::error!("Wrong asset type {}", type_name::<Asset>()); + } + } + LookupEntry::Pending => {} + } + + Handle::new(label_hash) + } + + #[tracing::instrument(skip(self))] + pub fn load_with_settings<'i, Asset, AssetSettings>( + &self, + label: impl Into<Label<'i>> + Debug, + asset_settings: AssetSettings, + ) -> Handle<Asset> + where + Asset: Send + Sync + 'static, + AssetSettings: Send + Sync + Debug + 'static, + { + let label = label.into(); + + let label_hash = LabelHash::new(&label); + + let mut asset_lookup = self.asset_lookup.borrow_mut(); + + if Self::is_pending(&asset_lookup, &label) { + return Handle::new(label_hash); + } + + let Some(lookup_entry) = asset_lookup.get(&label_hash) else { + self.add_import_work::<AssetSettings>( + &label, + label_hash, + Some(asset_settings), + &mut asset_lookup, + ); + + return Handle::new(label_hash); + }; + + match *lookup_entry { + LookupEntry::Occupied(asset_index) => { + let stored_asset = self.assets.get(asset_index).expect("Not possible"); + + if stored_asset.strong.downcast_ref::<Asset>().is_none() { + tracing::error!( + "Wrong asset type {} for asset", + type_name::<Asset>() + ); + } + } + LookupEntry::Pending => {} + } + + Handle::new(label_hash) + } + + pub fn store_with_name<'name, Asset: 'static + Send + Sync>( + &mut self, + name: impl Into<Cow<'name, str>>, + asset: Asset, + ) -> Handle<Asset> + { + self.store_with_label( + Label { + path: Path::new("").into(), + name: Some(name.into()), + }, + asset, + ) + } + + #[tracing::instrument(skip(self, asset), fields(asset_type=type_name::<Asset>()))] + pub fn store_with_label<'i, Asset: 'static + Send + Sync>( + &mut self, + label: impl Into<Label<'i>> + Debug, + asset: Asset, + ) -> Handle<Asset> + { + let label = label.into(); + + let label_hash = LabelHash::new(&label); + + if matches!( + self.asset_lookup.get_mut().get(&label_hash), + Some(LookupEntry::Occupied(_)) + ) { + tracing::error!("Asset already exists"); + + return Handle::new(label_hash); + } + + tracing::debug!("Storing asset"); + + self.assets.push(StoredAsset::new(asset)); + + let index = self.assets.len() - 1; + + self.asset_lookup + .get_mut() + .insert(label_hash, LookupEntry::Occupied(index)); + + if label.name.is_some() { + let parent_asset_label_hash = + LabelHash::new(&Label { path: label.path, name: None }); + + if matches!( + self.asset_lookup.get_mut().get(&parent_asset_label_hash), + Some(LookupEntry::Pending) + ) { + self.asset_lookup.get_mut().remove(&parent_asset_label_hash); + } else if self + .asset_lookup + .get_mut() + .get(&parent_asset_label_hash) + .is_none() + { + self.assets + .push(StoredAsset::new::<Option<Infallible>>(None)); + + self.asset_lookup.get_mut().insert( + parent_asset_label_hash, + LookupEntry::Occupied(self.assets.len() - 1), + ); + } + } + + Handle::new(label_hash) + } + + fn is_pending(asset_lookup: &HashMap<LabelHash, LookupEntry>, label: &Label) -> bool + { + if label.name.is_some() { + if let Some(LookupEntry::Pending) = + asset_lookup.get(&LabelHash::new(&Label { + path: label.path.as_ref().into(), + name: None, + })) + { + return true; + } + } + + if let Some(LookupEntry::Pending) = asset_lookup.get(&LabelHash::new(label)) { + return true; + }; + + false + } + + fn add_import_work<AssetSettings>( + &self, + label: &Label<'_>, + label_hash: LabelHash, + asset_settings: Option<AssetSettings>, + asset_lookup: &mut HashMap<LabelHash, LookupEntry>, + ) where + AssetSettings: Any + Send + Sync, + { + let Some(file_ext) = label.path.extension() else { + tracing::error!("Asset file is missing a file extension"); + return; + }; + + let Some(importer) = self.get_importer(file_ext) else { + tracing::error!( + "No importer exists for asset file extension {}", + file_ext.to_string_lossy() + ); + return; + }; + + self.import_work_queue.add_work(Work { + func: |ImportWorkUserData { + import_work_msg_sender, + asset_path, + asset_settings, + importer, + }| { + if let Err(err) = importer.call( + import_work_msg_sender, + asset_path.as_path(), + asset_settings.as_deref(), + ) { + tracing::error!( + "Failed to load asset {}: {err}", + asset_path.display() + ); + } + }, + user_data: ImportWorkUserData { + import_work_msg_sender: self.import_work_msg_sender.clone(), + asset_path: label.path.to_path_buf(), + asset_settings: asset_settings.map(|asset_settings| { + Box::new(asset_settings) as Box<dyn Any + Send + Sync> + }), + importer: importer.clone(), + }, + }); + + asset_lookup.insert(label_hash, LookupEntry::Pending); + + if label.name.is_some() { + asset_lookup.insert( + LabelHash::new(&Label { + path: label.path.as_ref().into(), + name: None, + }), + LookupEntry::Pending, + ); + } + } + + fn get_importer(&self, file_ext: &OsStr) -> Option<&WrappedImporterFn> + { + let index = *self.importer_lookup.get(file_ext)?; + + Some(self.importers.get(index).expect("Not possible")) + } +} + +impl Default for Assets +{ + fn default() -> Self + { + Self::with_capacity(0) + } +} + +pub struct Submitter<'path> +{ + import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: &'path Path, +} + +impl Submitter<'_> +{ + pub fn submit_load_other<'label, Asset: Send + Sync + 'static>( + &self, + label: impl Into<Label<'label>>, + ) -> Handle<Asset> + { + let label = label.into(); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Load { + do_load: |assets, label, _asset_settings| { + let _ = assets.load::<Asset>(label); + }, + label: label.to_owned(), + asset_settings: None, + }); + + Handle::new(LabelHash::new(&label)) + } + + pub fn submit_load_other_with_settings<'label, Asset, AssetSettings>( + &self, + label: impl Into<Label<'label>>, + asset_settings: AssetSettings, + ) -> Handle<Asset> + where + Asset: Send + Sync + 'static, + AssetSettings: Send + Sync + Debug + 'static, + { + let label = label.into(); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Load { + do_load: |assets, label, asset_settings| { + let asset_settings = *asset_settings + .expect("Not possible") + .downcast::<AssetSettings>() + .expect("Not possible"); + + let _ = assets + .load_with_settings::<Asset, AssetSettings>(label, asset_settings); + }, + label: label.to_owned(), + asset_settings: Some(Box::new(asset_settings)), + }); + + Handle::new(LabelHash::new(&label)) + } + + pub fn submit_store<Asset: Send + Sync + 'static>( + &self, + asset: Asset, + ) -> Handle<Asset> + { + let label = LabelOwned { + path: self.asset_path.into(), + name: None, + }; + + let label_hash = LabelHash::new(&label.to_label()); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Store { + do_store: |assets, label, boxed_asset| { + let Ok(asset) = boxed_asset.downcast::<Asset>() else { + unreachable!(); + }; + + assets.store_with_label::<Asset>(&label, *asset); + }, + label, + asset: Box::new(asset), + }); + + Handle::new(label_hash) + } + + pub fn submit_store_named<Asset: Send + Sync + 'static>( + &self, + name: impl AsRef<str>, + asset: Asset, + ) -> Handle<Asset> + { + let label = LabelOwned { + path: self.asset_path.into(), + name: Some(name.as_ref().into()), + }; + + let label_hash = LabelHash::new(&label.to_label()); + + let _ = self.import_work_msg_sender.send(ImportWorkMessage::Store { + do_store: |assets, label, boxed_asset| { + let Ok(asset) = boxed_asset.downcast::<Asset>() else { + unreachable!(); + }; + + assets.store_with_label::<Asset>(&label, *asset); + }, + label, + asset: Box::new(asset), + }); + + Handle::new(label_hash) + } +} + +/// Asset handle. +#[derive(Debug)] +pub struct Handle<Asset: 'static> +{ + id: Id, + _pd: PhantomData<Asset>, +} + +impl<Asset: 'static> Handle<Asset> +{ + pub fn id(&self) -> Id + { + self.id + } + + fn new(label_hash: LabelHash) -> Self + { + Self { + id: Id { label_hash }, + _pd: PhantomData, + } + } +} + +impl<Asset: 'static> Clone for Handle<Asset> +{ + fn clone(&self) -> Self + { + Self { id: self.id, _pd: PhantomData } + } +} + +/// Asset ID. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id +{ + label_hash: LabelHash, +} + +#[derive(Debug, thiserror::Error)] +enum ImporterError +{ + #[error("Settings has a incorrect type")] + IncorrectAssetSettingsType(PathBuf), + + #[error(transparent)] + Other(Box<dyn std::error::Error>), +} + +#[derive(Debug, Clone)] +struct WrappedImporterFn +{ + wrapper_func: fn( + MpscSender<ImportWorkMessage>, + &Path, + Option<&(dyn Any + Send + Sync)>, + ) -> Result<(), ImporterError>, +} + +impl WrappedImporterFn +{ + fn new<InnerFunc, AssetSettings, Err>(inner_func_param: InnerFunc) -> Self + where + InnerFunc: + Fn(&mut Submitter<'_>, &Path, Option<&AssetSettings>) -> Result<(), Err>, + AssetSettings: 'static, + Err: std::error::Error + 'static, + { + assert_eq!(size_of::<InnerFunc>(), 0); + + let wrapper_func = + |import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: &Path, + asset_settings: Option<&(dyn Any + Send + Sync)>| { + let inner_func = unsafe { std::mem::zeroed::<InnerFunc>() }; + + let asset_settings = asset_settings + .map(|asset_settings| { + asset_settings + .downcast_ref::<AssetSettings>() + .ok_or_else(|| { + ImporterError::IncorrectAssetSettingsType( + asset_path.to_path_buf(), + ) + }) + }) + .transpose()?; + + inner_func( + &mut Submitter { import_work_msg_sender, asset_path }, + asset_path, + asset_settings, + ) + .map_err(|err| ImporterError::Other(Box::new(err)))?; + + Ok(()) + }; + + std::mem::forget(inner_func_param); + + Self { wrapper_func } + } + + fn call( + &self, + import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: &Path, + asset_settings: Option<&(dyn Any + Send + Sync)>, + ) -> Result<(), ImporterError> + { + (self.wrapper_func)(import_work_msg_sender, asset_path, asset_settings) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct LabelHash(u64); + +impl LabelHash +{ + fn new(label: &Label<'_>) -> Self + { + let mut hasher = DefaultHasher::new(); + + label.hash(&mut hasher); + + Self(hasher.finish()) + } +} + +#[derive(Debug, Default)] +pub(crate) struct Extension +{ + pub assets: Assets, +} + +impl ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: ecs::extension::Collector<'_>) + { + let _ = collector.add_sole(self.assets); + + collector.add_system(*PRE_UPDATE_PHASE, add_received_assets); + } +} + +fn add_received_assets(mut assets: Single<Assets>) +{ + while let Some(import_work_msg) = assets.import_work_msg_receiver.try_recv().ok() { + match import_work_msg { + ImportWorkMessage::Store { do_store, label, asset } => { + do_store(&mut assets, label, asset); + } + ImportWorkMessage::Load { do_load, label, asset_settings } => { + do_load( + &assets, + Label { + path: label.path.as_path().into(), + name: label.name.as_deref().map(|name| name.into()), + }, + asset_settings, + ); + } + } + } +} + +#[derive(Debug)] +struct ImportWorkUserData +{ + import_work_msg_sender: MpscSender<ImportWorkMessage>, + asset_path: PathBuf, + asset_settings: Option<Box<dyn Any + Send + Sync>>, + importer: WrappedImporterFn, +} + +#[derive(Debug)] +enum ImportWorkMessage +{ + Store + { + do_store: fn(&mut Assets, LabelOwned, Box<dyn Any + Send + Sync>), + label: LabelOwned, + asset: Box<dyn Any + Send + Sync>, + }, + + Load + { + do_load: fn(&Assets, Label<'_>, Option<Box<dyn Any + Send + Sync>>), + label: LabelOwned, + asset_settings: Option<Box<dyn Any + Send + Sync>>, + }, +} + +#[derive(Debug, Clone, Copy)] +enum LookupEntry +{ + Occupied(usize), + Pending, +} + +#[derive(Debug)] +struct StoredAsset +{ + strong: Arc<dyn Any + Send + Sync>, +} + +impl StoredAsset +{ + fn new<Asset: Any + Send + Sync>(asset: Asset) -> Self + { + let strong = Arc::new(asset); + + Self { strong } + } +} diff --git a/engine/src/camera/fly.rs b/engine/src/camera/fly.rs index b6ba7aa..d6eac62 100644 --- a/engine/src/camera/fly.rs +++ b/engine/src/camera/fly.rs @@ -1,15 +1,14 @@ use ecs::component::local::Local; +use ecs::phase::UPDATE as UPDATE_PHASE; use ecs::sole::Single; use ecs::system::{Into, System}; use ecs::{Component, Query}; -use glfw::window::{Key, KeyState}; use crate::camera::{Active as ActiveCamera, Camera}; use crate::delta_time::DeltaTime; -use crate::event::Update as UpdateEvent; -use crate::input::{Cursor, CursorFlags, Keys}; -use crate::transform::Position; -use crate::util::builder; +use crate::input::{Cursor, CursorFlags, Key, KeyState, Keys}; +use crate::transform::WorldPosition; +use crate::builder; use crate::vector::{Vec2, Vec3}; builder! { @@ -61,7 +60,7 @@ impl ecs::extension::Extension for Extension fn collect(self, mut collector: ecs::extension::Collector<'_>) { collector.add_system( - UpdateEvent, + *UPDATE_PHASE, update .into_system() .initialize((CursorState::default(), self.0)), @@ -76,7 +75,7 @@ pub struct Options } fn update( - camera_query: Query<(Camera, Position, Fly, ActiveCamera)>, + camera_query: Query<(&mut Camera, &mut WorldPosition, &mut Fly, &ActiveCamera)>, keys: Single<Keys>, cursor: Single<Cursor>, cursor_flags: Single<CursorFlags>, @@ -85,9 +84,8 @@ fn update( options: Local<Options>, ) { - for (mut camera, mut camera_pos, mut fly_camera, _) in &camera_query { + for (mut camera, mut camera_world_pos, mut fly_camera, _) in &camera_query { if cursor.has_moved && cursor_flags.is_first_move.flag { - #[cfg(feature = "debug")] tracing::debug!("First cursor move"); cursor_state.last_pos = cursor.position; @@ -123,30 +121,31 @@ fn update( camera.global_up = cam_right.cross(&direction).normalize(); - if matches!(keys.get_key_state(Key::W), KeyState::Pressed) { - camera_pos.position += + if keys.get_key_state(Key::W) == KeyState::Pressed { + camera_world_pos.position += direction * fly_camera.speed * delta_time.as_secs_f32(); } - if matches!(keys.get_key_state(Key::S), KeyState::Pressed) { - camera_pos.position -= + if keys.get_key_state(Key::S) == KeyState::Pressed { + camera_world_pos.position -= direction * fly_camera.speed * delta_time.as_secs_f32(); } - if matches!(keys.get_key_state(Key::A), KeyState::Pressed) { + if keys.get_key_state(Key::A) == KeyState::Pressed { let cam_left = -direction.cross(&Vec3::UP).normalize(); - camera_pos.position += cam_left * fly_camera.speed * delta_time.as_secs_f32(); + camera_world_pos.position += + cam_left * fly_camera.speed * delta_time.as_secs_f32(); } - if matches!(keys.get_key_state(Key::D), KeyState::Pressed) { + if keys.get_key_state(Key::D) == KeyState::Pressed { let cam_right = direction.cross(&Vec3::UP).normalize(); - camera_pos.position += + camera_world_pos.position += cam_right * fly_camera.speed * delta_time.as_secs_f32(); } - camera.target = camera_pos.position + direction; + camera.target = camera_world_pos.position + direction; } } diff --git a/engine/src/collision.rs b/engine/src/collision.rs new file mode 100644 index 0000000..aefd9b6 --- /dev/null +++ b/engine/src/collision.rs @@ -0,0 +1,142 @@ +use ecs::Component; + +use crate::mesh::Mesh; +use crate::vector::Vec3; + +pub trait Collider<Other> +{ + fn intersects(&self, other: &Other) -> bool; +} + +#[derive(Debug, Default, Clone, Component)] +#[non_exhaustive] +pub struct BoxCollider +{ + pub min: Vec3<f32>, + pub max: Vec3<f32>, +} + +impl BoxCollider +{ + pub fn for_mesh(mesh: &Mesh) -> Self + { + let furthest_dir_points = mesh.find_furthest_vertex_positions(); + + Self { + min: Vec3 { + x: furthest_dir_points.left.x, + y: furthest_dir_points.down.y, + z: furthest_dir_points.back.z, + }, + max: Vec3 { + x: furthest_dir_points.right.x, + y: furthest_dir_points.up.y, + z: furthest_dir_points.front.z, + }, + } + } + + pub fn offset(self, offset: Vec3<f32>) -> Self + { + Self { + min: self.min + offset, + max: self.max + offset, + } + } +} + +impl Collider<BoxCollider> for BoxCollider +{ + fn intersects(&self, other: &BoxCollider) -> bool + { + self.min.x <= other.max.x + && self.max.x >= other.min.x + && self.min.y <= other.max.y + && self.max.y >= other.min.y + && self.min.z <= other.max.z + && self.max.z >= other.min.z + } +} + +impl Collider<SphereCollider> for BoxCollider +{ + fn intersects(&self, other: &SphereCollider) -> bool + { + other.intersects(self) + } +} + +impl Collider<Vec3<f32>> for BoxCollider +{ + fn intersects(&self, other: &Vec3<f32>) -> bool + { + other.x >= self.min.x + && other.y >= self.min.y + && other.z >= self.min.z + && other.x <= self.max.x + && other.y <= self.max.y + && other.z <= self.max.z + } +} + +#[derive(Debug, Default, Clone, Component)] +pub struct SphereCollider +{ + pub center: Vec3<f32>, + pub radius: f32, +} + +impl SphereCollider +{ + pub fn offset(self, offset: Vec3<f32>) -> Self + { + Self { + center: self.center + offset, + radius: self.radius, + } + } +} + +impl Collider<SphereCollider> for SphereCollider +{ + fn intersects(&self, other: &SphereCollider) -> bool + { + (&self.center - &other.center).length() <= self.radius + other.radius + } +} + +impl Collider<BoxCollider> for SphereCollider +{ + fn intersects(&self, other: &BoxCollider) -> bool + { + let mut min_distance = 0.0; + + if self.center.x < other.min.x { + min_distance += (self.center.x - other.min.x).powf(2.0); + } else if self.center.x > other.max.x { + min_distance += (self.center.x - other.max.x).powf(2.0); + } + + if self.center.y < other.min.y { + min_distance += (self.center.y - other.min.y).powf(2.0); + } else if self.center.y > other.max.y { + min_distance += (self.center.y - other.max.y).powf(2.0); + } + + if self.center.z < other.min.z { + min_distance += (self.center.z - other.min.z).powf(2.0); + } else if self.center.z > other.max.z { + min_distance += (self.center.z - other.max.z).powf(2.0); + } + + min_distance <= self.radius.powf(2.0) + } +} + +impl Collider<Vec3<f32>> for SphereCollider +{ + fn intersects(&self, other: &Vec3<f32>) -> bool + { + (&self.center - other).length() <= self.radius + } +} diff --git a/engine/src/data_types/dimens.rs b/engine/src/data_types/dimens.rs index b395627..d8d0247 100644 --- a/engine/src/data_types/dimens.rs +++ b/engine/src/data_types/dimens.rs @@ -1,7 +1,56 @@ -/// Dimensions. +/// 2D dimensions. #[derive(Debug, Clone, Copy)] pub struct Dimens<Value> { pub width: Value, pub height: Value, } + +impl<Value: Clone> From<Value> for Dimens<Value> +{ + fn from(value: Value) -> Self + { + Self { width: value.clone(), height: value } + } +} + +impl<Value> From<(Value, Value)> for Dimens<Value> +{ + fn from(value: (Value, Value)) -> Self + { + Self { width: value.0, height: value.1 } + } +} + +/// 3D dimensions. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub struct Dimens3<Value> +{ + pub width: Value, + pub height: Value, + pub depth: Value, +} + +impl<Value: Clone> From<Value> for Dimens3<Value> +{ + fn from(value: Value) -> Self + { + Self { + width: value.clone(), + height: value.clone(), + depth: value, + } + } +} + +impl<Value: Clone> From<(Value, Value, Value)> for Dimens3<Value> +{ + fn from(value: (Value, Value, Value)) -> Self + { + Self { + width: value.0, + height: value.1, + depth: value.2, + } + } +} diff --git a/engine/src/data_types/vector.rs b/engine/src/data_types/vector.rs index 17953f4..100c709 100644 --- a/engine/src/data_types/vector.rs +++ b/engine/src/data_types/vector.rs @@ -2,7 +2,8 @@ use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign}; use crate::color::Color; -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy, PartialEq)] +#[repr(C)] pub struct Vec2<Value> { pub x: Value, @@ -74,7 +75,7 @@ where } } -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy, PartialEq)] #[repr(C)] pub struct Vec3<Value> { @@ -85,6 +86,11 @@ pub struct Vec3<Value> impl Vec3<f32> { + pub const BACK: Self = Self { x: 0.0, y: 0.0, z: -1.0 }; + pub const DOWN: Self = Self { x: 0.0, y: -1.0, z: 0.0 }; + pub const FRONT: Self = Self { x: 0.0, y: 0.0, z: 1.0 }; + pub const LEFT: Self = Self { x: -1.0, y: 0.0, z: 0.0 }; + pub const RIGHT: Self = Self { x: 1.0, y: 0.0, z: 0.0 }; pub const UP: Self = Self { x: 0.0, y: 1.0, z: 0.0 }; /// Returns the length of the vector. @@ -209,6 +215,22 @@ where } } +impl<Value> Mul for Vec3<Value> +where + Value: Mul<Value, Output = Value>, +{ + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output + { + Self::Output { + x: self.x * rhs.x, + y: self.y * rhs.y, + z: self.z * rhs.z, + } + } +} + impl<Value> Neg for Vec3<Value> where Value: Neg<Output = Value>, diff --git a/engine/src/draw_flags.rs b/engine/src/draw_flags.rs index df5eed1..426f865 100644 --- a/engine/src/draw_flags.rs +++ b/engine/src/draw_flags.rs @@ -1,6 +1,6 @@ use ecs::Component; -use crate::util::builder; +use crate::builder; builder! { /// Flags for how a object should be drawn. diff --git a/engine/src/event.rs b/engine/src/event.rs deleted file mode 100644 index e5ae486..0000000 --- a/engine/src/event.rs +++ /dev/null @@ -1,27 +0,0 @@ -pub use ecs::event::start::Start; -use ecs::event::Event; - -#[derive(Debug)] -pub struct Update; - -impl Event for Update {} - -#[derive(Debug)] -pub struct PreUpdate; - -impl Event for PreUpdate {} - -#[derive(Debug)] -pub struct Present; - -impl Event for Present {} - -#[derive(Debug)] -pub struct PostPresent; - -impl Event for PostPresent {} - -#[derive(Debug)] -pub struct Conclude; - -impl Event for Conclude {} diff --git a/engine/src/file_format/wavefront/mtl.rs b/engine/src/file_format/wavefront/mtl.rs index ef6e894..f3c7a64 100644 --- a/engine/src/file_format/wavefront/mtl.rs +++ b/engine/src/file_format/wavefront/mtl.rs @@ -2,7 +2,7 @@ //! //! File format documentation: <https://paulbourke.net/dataformats/mtl> -use std::path::Path; +use std::path::{Path, PathBuf}; use crate::color::Color; use crate::file_format::wavefront::common::{ @@ -11,8 +11,6 @@ use crate::file_format::wavefront::common::{ ParsingError, Statement, }; -use crate::material::{Builder as MaterialBuilder, Material}; -use crate::texture::{Error as TextureError, Texture}; /// Parses the content of a Wavefront `.mtl`. /// @@ -44,25 +42,47 @@ pub fn parse(obj_content: &str) -> Result<Vec<NamedMaterial>, Error> .filter(|(_, statement)| matches!(statement.keyword, Keyword::Newmtl)) .count(); - #[cfg(feature = "debug")] tracing::debug!("Material count: {material_cnt}"); statements_to_materials(statements, material_cnt) } #[derive(Debug, Clone)] +#[non_exhaustive] pub struct NamedMaterial { pub name: String, - pub material: Material, + pub ambient: Color<f32>, + pub diffuse: Color<f32>, + pub specular: Color<f32>, + pub ambient_map: Option<TextureMap>, + pub diffuse_map: Option<TextureMap>, + pub specular_map: Option<TextureMap>, + pub shininess: f32, +} + +impl Default for NamedMaterial +{ + fn default() -> Self + { + Self { + name: String::new(), + ambient: Color::WHITE_F32, + diffuse: Color::WHITE_F32, + specular: Color::WHITE_F32, + ambient_map: None, + diffuse_map: None, + specular_map: None, + shininess: 0.0, + } + } } #[derive(Debug, Clone)] -pub struct UnfinishedNamedMaterial +#[non_exhaustive] +pub struct TextureMap { - name: String, - material_builder: MaterialBuilder, - ready: bool, + pub path: PathBuf, } #[derive(Debug, thiserror::Error)] @@ -71,8 +91,14 @@ pub enum Error #[error(transparent)] ParsingError(#[from] ParsingError), - #[error("Failed to open texture")] - TextureError(#[from] TextureError), + #[error( + "A material start statement (newmtl) is expected before statement at line {}", + line_no + )] + ExpectedMaterialStartStmtBeforeStmt + { + line_no: usize + }, #[error( "Unsupported number of arguments ({arg_count}) to {keyword} at line {line_no}" @@ -93,7 +119,7 @@ pub enum Error }, } -#[cfg_attr(feature = "debug", tracing::instrument(skip_all))] +#[tracing::instrument(skip_all)] fn statements_to_materials( statements: impl IntoIterator<Item = (usize, Statement<Keyword>)>, material_cnt: usize, @@ -101,63 +127,52 @@ fn statements_to_materials( { let mut materials = Vec::<NamedMaterial>::with_capacity(material_cnt); - let mut curr_material = UnfinishedNamedMaterial { - name: String::new(), - material_builder: MaterialBuilder::new(), - ready: false, - }; - for (line_no, statement) in statements { if statement.keyword == Keyword::Newmtl { - if curr_material.ready { - #[cfg(feature = "debug")] - tracing::debug!("Building material"); - - let material = curr_material.material_builder.clone().build(); - - materials.push(NamedMaterial { name: curr_material.name, material }); - } - let name = statement.get_text_arg(0, line_no)?; - curr_material.name = name.to_string(); - curr_material.ready = true; + materials.push(NamedMaterial { + name: name.to_string(), + ..Default::default() + }); continue; } - if !curr_material.ready { - // Discard statements not belonging to a material - continue; + let Some(curr_material) = materials.last_mut() else { + return Err(Error::ExpectedMaterialStartStmtBeforeStmt { line_no }); }; match statement.keyword { Keyword::Ka => { let color = get_color_from_statement(&statement, line_no)?; - #[cfg(feature = "debug")] - tracing::debug!("Adding ambient color"); + tracing::debug!( + "Adding ambient color {color:?} to material {}", + curr_material.name + ); - curr_material.material_builder = - curr_material.material_builder.ambient(color); + curr_material.ambient = color; } Keyword::Kd => { let color = get_color_from_statement(&statement, line_no)?; - #[cfg(feature = "debug")] - tracing::debug!("Adding diffuse color"); + tracing::debug!( + "Adding diffuse color {color:?} to material {}", + curr_material.name + ); - curr_material.material_builder = - curr_material.material_builder.diffuse(color); + curr_material.diffuse = color; } Keyword::Ks => { let color = get_color_from_statement(&statement, line_no)?; - #[cfg(feature = "debug")] - tracing::debug!("Adding specular color"); + tracing::debug!( + "Adding specular color {color:?} to material {}", + curr_material.name + ); - curr_material.material_builder = - curr_material.material_builder.specular(color); + curr_material.specular = color; } Keyword::MapKa => { if statement.arguments.len() > 1 { @@ -170,55 +185,75 @@ fn statements_to_materials( let texture_file_path = statement.get_text_arg(0, line_no)?; - let texture = Texture::open(Path::new(texture_file_path))?; - - #[cfg(feature = "debug")] - tracing::debug!("Adding ambient map"); + tracing::debug!( + "Adding ambient map {texture_file_path} to material {}", + curr_material.name + ); - let texture_id = texture.id(); - - curr_material.material_builder = curr_material - .material_builder - .texture(texture) - .ambient_map(texture_id); + curr_material.ambient_map = Some(TextureMap { + path: Path::new(texture_file_path).to_path_buf(), + }); } Keyword::MapKd => { - let texture = get_map_from_texture(&statement, line_no)?; + if statement.arguments.len() > 1 { + return Err(Error::UnsupportedArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no, + }); + } - #[cfg(feature = "debug")] - tracing::debug!("Adding diffuse map"); + let texture_file_path = statement.get_text_arg(0, line_no)?; - let texture_id = texture.id(); + tracing::debug!( + "Adding diffuse map {texture_file_path} to material {}", + curr_material.name + ); - curr_material.material_builder = curr_material - .material_builder - .texture(texture) - .diffuse_map(texture_id); + curr_material.diffuse_map = Some(TextureMap { + path: Path::new(texture_file_path).to_path_buf(), + }); } Keyword::MapKs => { - let texture = get_map_from_texture(&statement, line_no)?; + if statement.arguments.len() > 1 { + return Err(Error::UnsupportedArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no, + }); + } - #[cfg(feature = "debug")] - tracing::debug!("Adding specular map"); + let texture_file_path = statement.get_text_arg(0, line_no)?; - let texture_id = texture.id(); + tracing::debug!( + "Adding specular map {texture_file_path} to material {}", + curr_material.name + ); - curr_material.material_builder = curr_material - .material_builder - .texture(texture) - .specular_map(texture_id); + curr_material.specular_map = Some(TextureMap { + path: Path::new(texture_file_path).to_path_buf(), + }); } - Keyword::Newmtl => {} - } - } + Keyword::Ns => { + if statement.arguments.len() != 1 { + return Err(Error::UnsupportedArgumentCount { + keyword: statement.keyword.to_string(), + arg_count: statement.arguments.len(), + line_no, + }); + } - if curr_material.ready { - #[cfg(feature = "debug")] - tracing::debug!("Building last material"); + let shininess = statement.get_float_arg(0, line_no)?; - let material = curr_material.material_builder.build(); + tracing::debug!( + "Adding shininess {shininess} to material {}", + curr_material.name + ); - materials.push(NamedMaterial { name: curr_material.name, material }); + curr_material.shininess = shininess; + } + Keyword::Newmtl => {} + } } Ok(materials) @@ -244,24 +279,6 @@ fn get_color_from_statement( Ok(Color { red, green, blue }) } -fn get_map_from_texture( - statement: &Statement<Keyword>, - line_no: usize, -) -> Result<Texture, Error> -{ - if statement.arguments.len() > 1 { - return Err(Error::UnsupportedArgumentCount { - keyword: statement.keyword.to_string(), - arg_count: statement.arguments.len(), - line_no, - }); - } - - let texture_file_path = statement.get_text_arg(0, line_no)?; - - Ok(Texture::open(Path::new(texture_file_path))?) -} - keyword! { #[derive(Debug, PartialEq, Eq, Clone, Copy)] enum Keyword { @@ -280,5 +297,7 @@ keyword! { #[keyword(rename = "map_Ks")] MapKs, + + Ns, } } diff --git a/engine/src/file_format/wavefront/obj.rs b/engine/src/file_format/wavefront/obj.rs index 88e6580..88d566c 100644 --- a/engine/src/file_format/wavefront/obj.rs +++ b/engine/src/file_format/wavefront/obj.rs @@ -2,6 +2,7 @@ //! //! File format documentation: <https://paulbourke.net/dataformats/obj> +use std::collections::HashMap; use std::fs::read_to_string; use std::path::PathBuf; @@ -12,10 +13,9 @@ use crate::file_format::wavefront::common::{ Statement, Triplet, }; -use crate::mesh::Mesh; +use crate::mesh::{Mesh, Vertex}; use crate::util::try_option; use crate::vector::{Vec2, Vec3}; -use crate::vertex::{Builder as VertexBuilder, Vertex}; /// Parses the content of a Wavefront `.obj`. /// @@ -82,26 +82,32 @@ impl Obj /// - A face index does not fit in a [`u32`] pub fn to_mesh(&self) -> Result<Mesh, Error> { - let vertices = self - .faces - .iter() - .flat_map(|face| face.vertices.clone()) - .map(|face_vertex| face_vertex.to_vertex(self)) - .collect::<Result<Vec<_>, Error>>()?; - - Ok(Mesh::new( - vertices, - Some( - self.faces - .iter() - .flat_map(|face| face.vertices.clone()) - .enumerate() - .map(|(index, _)| { - u32::try_from(index).map_err(|_| Error::FaceIndexTooBig(index)) - }) - .collect::<Result<Vec<_>, _>>()?, - ), - )) + let mut vertices = Vec::<Vertex>::with_capacity(self.faces.len() * 3); + let mut indices = Vec::<u32>::with_capacity(self.faces.len() * 3); + + let mut added_face_vertices = + HashMap::<FaceVertex, u32>::with_capacity(self.faces.len() * 3); + + for face in &self.faces { + for face_vertex in &face.vertices { + if let Some(index) = added_face_vertices.get(&face_vertex) { + indices.push(*index); + + continue; + } + + vertices.push(face_vertex.to_vertex(self)?); + + let vertex_index = u32::try_from(vertices.len() - 1) + .map_err(|_| Error::FaceIndexTooBig(vertices.len() - 1))?; + + indices.push(vertex_index); + + added_face_vertices.insert(face_vertex.clone(), vertex_index); + } + } + + Ok(Mesh::new(vertices, Some(indices))) } /// Reads and parses the material libraries of this `Obj`. @@ -142,7 +148,7 @@ pub struct Face pub material_name: Option<String>, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct FaceVertex { pub position: u32, @@ -161,7 +167,7 @@ impl FaceVertex /// - The face's vertex normal cannot be found in the given [`Obj`] pub fn to_vertex(&self, obj: &Obj) -> Result<Vertex, Error> { - let mut vertex_builder = VertexBuilder::default(); + let mut vertex_builder = Vertex::builder(); let vertex_pos = *obj.vertex_positions.get(self.position as usize - 1).ok_or( Error::FaceVertexPositionNotFound { vertex_pos_index: self.position }, diff --git a/engine/src/image.rs b/engine/src/image.rs new file mode 100644 index 0000000..0e04412 --- /dev/null +++ b/engine/src/image.rs @@ -0,0 +1,184 @@ +use std::fs::File; +use std::io::BufReader; +use std::path::Path; + +use image_rs::GenericImageView as _; + +use crate::asset::{Assets, Submitter as AssetSubmitter}; +use crate::color::Color; +use crate::data_types::dimens::Dimens; +use crate::builder; + +#[derive(Debug)] +pub struct Image +{ + inner: image_rs::DynamicImage, +} + +impl Image +{ + pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> + { + let buffered_reader = + BufReader::new(File::open(&path).map_err(Error::ReadFailed)?); + + let image_reader = image_rs::io::Reader::with_format( + buffered_reader, + image_rs::ImageFormat::from_path(path) + .map_err(|_| Error::UnsupportedFormat)?, + ); + + Ok(Self { + inner: image_reader + .decode() + .map_err(|err| Error::DecodeFailed(DecodeError(err)))?, + }) + } + + pub fn from_color(dimens: impl Into<Dimens<u32>>, color: impl Into<Color<u8>>) + -> Self + { + let dimens: Dimens<u32> = dimens.into(); + + let color: Color<u8> = color.into(); + + Self { + inner: image_rs::RgbImage::from_pixel( + dimens.width, + dimens.height, + image_rs::Rgb([color.red, color.green, color.blue]), + ) + .into(), + } + } + + pub fn dimensions(&self) -> Dimens<u32> + { + self.inner.dimensions().into() + } + + pub fn color_type(&self) -> ColorType + { + self.inner.color().into() + } + + pub fn as_bytes(&self) -> &[u8] + { + self.inner.as_bytes() + } +} + +builder! { +#[builder(name = SettingsBuilder, derives=(Debug, Clone))] +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct Settings { +} +} + +impl Settings +{ + pub fn builder() -> SettingsBuilder + { + SettingsBuilder::default() + } +} + +impl Default for SettingsBuilder +{ + fn default() -> Self + { + Settings::default().into() + } +} + +/// An enumeration over supported color types and bit depths +#[derive(Copy, PartialEq, Eq, Debug, Clone, Hash)] +#[non_exhaustive] +pub enum ColorType +{ + /// Pixel is 8-bit luminance + L8, + + /// Pixel is 8-bit luminance with an alpha channel + La8, + + /// Pixel contains 8-bit R, G and B channels + Rgb8, + + /// Pixel is 8-bit RGB with an alpha channel + Rgba8, + + /// Pixel is 16-bit luminance + L16, + + /// Pixel is 16-bit luminance with an alpha channel + La16, + + /// Pixel is 16-bit RGB + Rgb16, + + /// Pixel is 16-bit RGBA + Rgba16, + + /// Pixel is 32-bit float RGB + Rgb32F, + + /// Pixel is 32-bit float RGBA + Rgba32F, +} + +impl From<image_rs::ColorType> for ColorType +{ + fn from(color_type: image_rs::ColorType) -> Self + { + match color_type { + image_rs::ColorType::L8 => Self::L8, + image_rs::ColorType::La8 => Self::La8, + image_rs::ColorType::Rgb8 => Self::Rgb8, + image_rs::ColorType::Rgba8 => Self::Rgba8, + image_rs::ColorType::L16 => Self::L16, + image_rs::ColorType::La16 => Self::La16, + image_rs::ColorType::Rgb16 => Self::Rgb16, + image_rs::ColorType::Rgba16 => Self::Rgba16, + image_rs::ColorType::Rgb32F => Self::Rgb32F, + image_rs::ColorType::Rgba32F => Self::Rgba32F, + _ => { + panic!("Unrecognized image_rs::ColorType variant"); + } + } + } +} + +pub fn set_asset_importers(assets: &mut Assets) +{ + assets.set_importer::<_, _>(["png", "jpg"], import_asset); +} + +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + #[error("Failed to read image file")] + ReadFailed(#[source] std::io::Error), + + #[error("Failed to decode image")] + DecodeFailed(DecodeError), + + #[error("Unsupported image format")] + UnsupportedFormat, +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct DecodeError(image_rs::ImageError); + +fn import_asset( + asset_submitter: &mut AssetSubmitter<'_>, + path: &Path, + _settings: Option<&'_ Settings>, +) -> Result<(), Error> +{ + asset_submitter.submit_store::<Image>(Image::open(path)?); + + Ok(()) +} diff --git a/engine/src/input.rs b/engine/src/input.rs index f4166f6..d6a82f6 100644 --- a/engine/src/input.rs +++ b/engine/src/input.rs @@ -1,16 +1,13 @@ use std::collections::HashMap; use ecs::extension::Collector as ExtensionCollector; +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::{Phase, PRE_UPDATE as PRE_UPDATE_PHASE, START as START_PHASE}; use ecs::sole::Single; -use ecs::Sole; +use ecs::{static_entity, Sole}; -use crate::event::{ - PostPresent as PostPresentEvent, - PreUpdate as PreUpdateEvent, - Start as StartEvent, -}; use crate::vector::Vec2; -use crate::window::Window; +use crate::window::{Window, UPDATE_PHASE as WINDOW_UPDATE_PHASE}; mod reexports { @@ -19,10 +16,16 @@ mod reexports pub use reexports::*; +static_entity!( + SET_PREV_KEY_STATE_PHASE, + (Phase, Pair::new::<ChildOf>(*WINDOW_UPDATE_PHASE)) +); + #[derive(Debug, Sole)] pub struct Keys { map: HashMap<Key, KeyData>, + pending: Vec<(Key, KeyState)>, } impl Keys @@ -43,6 +46,7 @@ impl Keys ) }) .collect(), + pending: Vec::with_capacity(Key::KEYS.len()), } } @@ -68,10 +72,6 @@ impl Keys pub fn set_key_state(&mut self, key: Key, new_key_state: KeyState) { - if matches!(new_key_state, KeyState::Repeat) { - return; - } - let Some(key_data) = self.map.get_mut(&key) else { unreachable!(); }; @@ -149,9 +149,9 @@ impl ecs::extension::Extension for Extension { fn collect(self, mut collector: ExtensionCollector<'_>) { - collector.add_system(StartEvent, initialize); - collector.add_system(PreUpdateEvent, maybe_clear_cursor_is_first_move); - collector.add_system(PostPresentEvent, set_keys_prev_tick_state); + collector.add_system(*START_PHASE, initialize); + collector.add_system(*PRE_UPDATE_PHASE, maybe_clear_cursor_is_first_move); + collector.add_system(*SET_PREV_KEY_STATE_PHASE, set_pending_key_states); collector.add_sole(Keys::default()).ok(); collector.add_sole(Cursor::default()).ok(); @@ -173,7 +173,7 @@ fn initialize( let mut keys = keys_ref.to_single(); - keys.set_key_state(key, key_state); + keys.pending.push((key, key_state)); }); let cursor_weak_ref = cursor.to_weak_ref(); @@ -194,7 +194,6 @@ fn initialize( let cursor_flags_weak_ref = cursor_flags.to_weak_ref(); window.set_focus_callback(move |is_focused| { - #[cfg(feature = "debug")] tracing::trace!("Window is focused: {is_focused}"); let cursor_flags_ref = cursor_flags_weak_ref.access().expect("No world"); @@ -209,7 +208,6 @@ fn maybe_clear_cursor_is_first_move( ) { if cursor_flags.is_first_move.pending_clear { - #[cfg(feature = "debug")] tracing::trace!("Clearing is_first_move"); // This flag was set for the whole previous tick so it can be cleared now @@ -219,7 +217,6 @@ fn maybe_clear_cursor_is_first_move( } if cursor.has_moved && cursor_flags.is_first_move.flag { - #[cfg(feature = "debug")] tracing::trace!("Setting flag to clear is_first_move next tick"); // Make this system clear is_first_move the next time it runs @@ -227,11 +224,21 @@ fn maybe_clear_cursor_is_first_move( } } -fn set_keys_prev_tick_state(mut keys: Single<Keys>) +fn set_pending_key_states(mut keys: Single<Keys>) { - for key_data in keys.map.values_mut() { + let Keys { map, pending } = &mut *keys; + + for key_data in map.values_mut() { key_data.prev_tick_state = key_data.state; } + + for (key, key_state) in pending { + let Some(key_data) = map.get_mut(key) else { + unreachable!(); + }; + + key_data.state = *key_state; + } } #[derive(Debug)] diff --git a/engine/src/lib.rs b/engine/src/lib.rs index abf26f5..6ccba53 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -2,46 +2,39 @@ #![allow(clippy::needless_pass_by_value)] use ecs::component::Sequence as ComponentSequence; -use ecs::event::component::TypeTransformComponentsToAddedEvents; -use ecs::event::{Event, Sequence as EventSequence}; use ecs::extension::Extension; +use ecs::pair::Pair; +use ecs::phase::PRE_UPDATE as PRE_UPDATE_PHASE; use ecs::sole::Sole; use ecs::system::{Into, System}; -use ecs::tuple::Reduce as TupleReduce; use ecs::uid::Uid; use ecs::{SoleAlreadyExistsError, World}; +use crate::asset::{Assets, Extension as AssetExtension}; use crate::delta_time::{update as update_delta_time, DeltaTime, LastUpdate}; -use crate::event::{ - Conclude as ConcludeEvent, - PostPresent as PostPresentEvent, - PreUpdate as PreUpdateEvent, - Present as PresentEvent, - Update as UpdateEvent, -}; mod opengl; -mod shader_preprocessor; mod util; +mod work_queue; +pub mod asset; pub mod camera; +pub mod collision; pub mod data_types; pub mod delta_time; pub mod draw_flags; -pub mod event; pub mod file_format; +pub mod image; pub mod input; pub mod lighting; pub mod material; pub mod math; pub mod mesh; -pub mod performance; +pub mod model; pub mod projection; pub mod renderer; -pub mod shader; pub mod texture; pub mod transform; -pub mod vertex; pub mod window; pub extern crate ecs; @@ -49,13 +42,7 @@ pub extern crate ecs; pub(crate) use crate::data_types::matrix; pub use crate::data_types::{color, vector}; -type EventOrder = ( - PreUpdateEvent, - UpdateEvent, - PresentEvent, - PostPresentEvent, - ConcludeEvent, -); +const INITIAL_ASSET_CAPACITY: usize = 128; #[derive(Debug)] pub struct Engine @@ -74,30 +61,45 @@ impl Engine world.add_sole(DeltaTime::default()).ok(); world.register_system( - PreUpdateEvent, + *PRE_UPDATE_PHASE, update_delta_time .into_system() .initialize((LastUpdate::default(),)), ); + let mut assets = Assets::with_capacity(INITIAL_ASSET_CAPACITY); + + crate::model::set_asset_importers(&mut assets); + crate::image::set_asset_importers(&mut assets); + + world.add_extension(AssetExtension { assets }); + Self { world } } pub fn spawn<Comps>(&mut self, components: Comps) -> Uid where - Comps: ComponentSequence + TupleReduce<TypeTransformComponentsToAddedEvents>, - Comps::Out: EventSequence, + Comps: ComponentSequence, { self.world.create_entity(components) } pub fn register_system<'this, SystemImpl>( &'this mut self, - event: impl Event, + phase_euid: Uid, + system: impl System<'this, SystemImpl>, + ) + { + self.world.register_system(phase_euid, system); + } + + pub fn register_observer_system<'this, SystemImpl>( + &'this mut self, system: impl System<'this, SystemImpl>, + event: Pair<Uid, Uid>, ) { - self.world.register_system(event, system); + self.world.register_observer_system(system, event); } /// Adds a globally shared singleton value. @@ -115,9 +117,9 @@ impl Engine } /// Runs the event loop. - pub fn start(&self) + pub fn start(&mut self) { - self.world.event_loop::<EventOrder>(); + self.world.start_loop(); } } diff --git a/engine/src/lighting.rs b/engine/src/lighting.rs index 48adb0e..4406ed5 100644 --- a/engine/src/lighting.rs +++ b/engine/src/lighting.rs @@ -2,7 +2,7 @@ use ecs::{Component, Sole}; use crate::color::Color; use crate::data_types::vector::Vec3; -use crate::util::builder; +use crate::builder; builder! { #[builder(name = PointLightBuilder, derives = (Debug, Clone))] @@ -10,7 +10,8 @@ builder! { #[non_exhaustive] pub struct PointLight { - pub position: Vec3<f32>, + /// Position in local space. + pub local_position: Vec3<f32>, pub diffuse: Color<f32>, pub specular: Color<f32>, pub attenuation_params: AttenuationParams, @@ -31,7 +32,7 @@ impl Default for PointLight fn default() -> Self { Self { - position: Vec3::default(), + local_position: Vec3::default(), diffuse: Color { red: 0.5, green: 0.5, blue: 0.5 }, specular: Color { red: 1.0, green: 1.0, blue: 1.0 }, attenuation_params: AttenuationParams::default(), diff --git a/engine/src/material.rs b/engine/src/material.rs index aae6003..56ff15f 100644 --- a/engine/src/material.rs +++ b/engine/src/material.rs @@ -1,35 +1,48 @@ use ecs::Component; use crate::color::Color; -use crate::data_types::dimens::Dimens; -use crate::texture::{Id as TextureId, Texture}; -use crate::util::builder; +use crate::texture::Texture; +use crate::builder; -#[derive(Debug, Clone, Component)] +#[derive(Debug, Clone)] #[non_exhaustive] pub struct Material { pub ambient: Color<f32>, pub diffuse: Color<f32>, pub specular: Color<f32>, - pub ambient_map: TextureId, - pub diffuse_map: TextureId, - pub specular_map: TextureId, - pub textures: Vec<Texture>, + pub ambient_map: Option<Texture>, + pub diffuse_map: Option<Texture>, + pub specular_map: Option<Texture>, pub shininess: f32, } +impl Material +{ + pub fn builder() -> Builder + { + Builder::default() + } +} + +impl Default for Material +{ + fn default() -> Self + { + Self::builder().build() + } +} + /// [`Material`] builder. #[derive(Debug, Clone)] pub struct Builder { - ambient: Option<Color<f32>>, - diffuse: Option<Color<f32>>, - specular: Option<Color<f32>>, - ambient_map: Option<TextureId>, - diffuse_map: Option<TextureId>, - specular_map: Option<TextureId>, - textures: Vec<Texture>, + ambient: Color<f32>, + diffuse: Color<f32>, + specular: Color<f32>, + ambient_map: Option<Texture>, + diffuse_map: Option<Texture>, + specular_map: Option<Texture>, shininess: f32, } @@ -39,13 +52,12 @@ impl Builder pub fn new() -> Self { Self { - ambient: None, - diffuse: None, - specular: None, + ambient: Color::WHITE_F32, + diffuse: Color::WHITE_F32, + specular: Color::WHITE_F32, ambient_map: None, diffuse_map: None, specular_map: None, - textures: Vec::new(), shininess: 32.0, } } @@ -53,7 +65,7 @@ impl Builder #[must_use] pub fn ambient(mut self, ambient: Color<f32>) -> Self { - self.ambient = Some(ambient); + self.ambient = ambient; self } @@ -61,7 +73,7 @@ impl Builder #[must_use] pub fn diffuse(mut self, diffuse: Color<f32>) -> Self { - self.diffuse = Some(diffuse); + self.diffuse = diffuse; self } @@ -69,13 +81,13 @@ impl Builder #[must_use] pub fn specular(mut self, specular: Color<f32>) -> Self { - self.specular = Some(specular); + self.specular = specular; self } #[must_use] - pub fn ambient_map(mut self, ambient_map: TextureId) -> Self + pub fn ambient_map(mut self, ambient_map: Texture) -> Self { self.ambient_map = Some(ambient_map); @@ -83,7 +95,7 @@ impl Builder } #[must_use] - pub fn diffuse_map(mut self, diffuse_map: TextureId) -> Self + pub fn diffuse_map(mut self, diffuse_map: Texture) -> Self { self.diffuse_map = Some(diffuse_map); @@ -91,7 +103,7 @@ impl Builder } #[must_use] - pub fn specular_map(mut self, specular_map: TextureId) -> Self + pub fn specular_map(mut self, specular_map: Texture) -> Self { self.specular_map = Some(specular_map); @@ -99,22 +111,6 @@ impl Builder } #[must_use] - pub fn textures(mut self, textures: impl IntoIterator<Item = Texture>) -> Self - { - self.textures = textures.into_iter().collect(); - - self - } - - #[must_use] - pub fn texture(mut self, texture: Texture) -> Self - { - self.textures.push(texture); - - self - } - - #[must_use] pub fn shininess(mut self, shininess: f32) -> Self { self.shininess = shininess; @@ -127,43 +123,15 @@ impl Builder /// # Panics /// Will panic if no ambient map, diffuse map or specular map is set. #[must_use] - pub fn build(mut self) -> Material + pub fn build(self) -> Material { - let ambient_map = self.ambient_map.unwrap_or_else(|| { - let texture = create_1x1_white_texture(); - let texture_id = texture.id(); - - self.textures.push(texture); - - texture_id - }); - - let diffuse_map = self.diffuse_map.unwrap_or_else(|| { - let texture = create_1x1_white_texture(); - let texture_id = texture.id(); - - self.textures.push(texture); - - texture_id - }); - - let specular_map = self.specular_map.unwrap_or_else(|| { - let texture = create_1x1_white_texture(); - let texture_id = texture.id(); - - self.textures.push(texture); - - texture_id - }); - Material { - ambient: self.ambient.unwrap_or(Color::WHITE_F32), - diffuse: self.diffuse.unwrap_or(Color::WHITE_F32), - specular: self.specular.unwrap_or(Color::WHITE_F32), - ambient_map, - diffuse_map, - specular_map, - textures: self.textures, + ambient: self.ambient, + diffuse: self.diffuse, + specular: self.specular, + ambient_map: self.ambient_map, + diffuse_map: self.diffuse_map, + specular_map: self.specular_map, shininess: self.shininess, } } @@ -198,8 +166,3 @@ impl Flags FlagsBuilder::default() } } - -fn create_1x1_white_texture() -> Texture -{ - Texture::new_from_color(&Dimens { width: 1, height: 1 }, &Color::WHITE_U8) -} diff --git a/engine/src/math.rs b/engine/src/math.rs index b86e760..0340de8 100644 --- a/engine/src/math.rs +++ b/engine/src/math.rs @@ -13,5 +13,5 @@ pub fn calc_triangle_surface_normal( let v1 = edge_b - egde_a; let v2 = edge_c - egde_a; - v1.cross(&v2) + v1.cross(&v2).normalize() } diff --git a/engine/src/mesh.rs b/engine/src/mesh.rs index de0af70..fb977af 100644 --- a/engine/src/mesh.rs +++ b/engine/src/mesh.rs @@ -1,10 +1,9 @@ -use ecs::Component; - -use crate::vertex::Vertex; +use crate::builder; +use crate::vector::{Vec2, Vec3}; pub mod cube; -#[derive(Debug, Clone, Component)] +#[derive(Debug, Clone, Default)] pub struct Mesh { vertices: Vec<Vertex>, @@ -26,8 +25,140 @@ impl Mesh } #[must_use] + pub fn vertices_mut(&mut self) -> &mut [Vertex] + { + &mut self.vertices + } + + #[must_use] pub fn indices(&self) -> Option<&[u32]> { self.indices.as_deref() } + + #[must_use] + pub fn indices_mut(&mut self) -> Option<&mut [u32]> + { + self.indices.as_deref_mut() + } + + /// Finds the vertex positions that are furthest in every 3D direction. Keep in mind + /// that this can be quite time-expensive if the mesh has many vertices. + pub fn find_furthest_vertex_positions(&self) -> DirectionPositions<'_> + { + let mut point_iter = self.vertices().iter().map(|vertex| &vertex.pos).into_iter(); + + let first_point = point_iter.next().unwrap(); + + point_iter + .fold( + FurthestPosAcc { + up: FurthestPos::new(&first_point, &Vec3::UP), + down: FurthestPos::new(&first_point, &Vec3::DOWN), + left: FurthestPos::new(&first_point, &Vec3::LEFT), + right: FurthestPos::new(&first_point, &Vec3::RIGHT), + back: FurthestPos::new(&first_point, &Vec3::BACK), + front: FurthestPos::new(&first_point, &Vec3::FRONT), + }, + |mut furthest_pos_acc, pos| { + furthest_pos_acc.up.update_if_further(pos); + furthest_pos_acc.down.update_if_further(pos); + furthest_pos_acc.left.update_if_further(pos); + furthest_pos_acc.right.update_if_further(pos); + furthest_pos_acc.back.update_if_further(pos); + furthest_pos_acc.front.update_if_further(pos); + + furthest_pos_acc + }, + ) + .into() + } +} + +builder! { +#[builder(name = VertexBuilder, derives = (Debug, Default, Clone))] +#[derive(Debug, Clone, Default, PartialEq)] +#[non_exhaustive] +pub struct Vertex +{ + pub pos: Vec3<f32>, + pub texture_coords: Vec2<f32>, + pub normal: Vec3<f32>, +} +} + +impl Vertex +{ + #[must_use] + pub fn builder() -> VertexBuilder + { + VertexBuilder::default() + } +} + +#[derive(Debug, Clone)] +pub struct DirectionPositions<'mesh> +{ + pub up: &'mesh Vec3<f32>, + pub down: &'mesh Vec3<f32>, + pub left: &'mesh Vec3<f32>, + pub right: &'mesh Vec3<f32>, + pub back: &'mesh Vec3<f32>, + pub front: &'mesh Vec3<f32>, +} + +impl<'mesh> From<FurthestPosAcc<'mesh>> for DirectionPositions<'mesh> +{ + fn from(acc: FurthestPosAcc<'mesh>) -> Self + { + Self { + up: acc.up.pos, + down: acc.down.pos, + left: acc.left.pos, + right: acc.right.pos, + back: acc.back.pos, + front: acc.front.pos, + } + } +} + +#[derive(Debug)] +struct FurthestPosAcc<'mesh> +{ + up: FurthestPos<'mesh>, + down: FurthestPos<'mesh>, + left: FurthestPos<'mesh>, + right: FurthestPos<'mesh>, + back: FurthestPos<'mesh>, + front: FurthestPos<'mesh>, +} + +#[derive(Debug)] +struct FurthestPos<'mesh> +{ + pos: &'mesh Vec3<f32>, + dot_prod: f32, + direction: &'mesh Vec3<f32>, +} + +impl<'mesh> FurthestPos<'mesh> +{ + fn new(pos: &'mesh Vec3<f32>, direction: &'mesh Vec3<f32>) -> Self + { + Self { + pos, + dot_prod: direction.dot(&pos), + direction, + } + } + + fn update_if_further(&mut self, point: &'mesh Vec3<f32>) + { + let point_dot_prod = self.direction.dot(point); + + if point_dot_prod > self.dot_prod { + self.pos = point; + self.dot_prod = point_dot_prod; + } + } } diff --git a/engine/src/mesh/cube.rs b/engine/src/mesh/cube.rs index 7cdf885..e91cf0e 100644 --- a/engine/src/mesh/cube.rs +++ b/engine/src/mesh/cube.rs @@ -1,8 +1,8 @@ +use crate::data_types::dimens::Dimens3; use crate::math::calc_triangle_surface_normal; -use crate::mesh::Mesh; -use crate::util::builder; -use crate::vector::Vec3; -use crate::vertex::{Builder as VertexBuilder, Vertex}; +use crate::mesh::{Mesh, Vertex}; +use crate::builder; +use crate::vector::{Vec2, Vec3}; builder! { /// Cube mesh creation specification. @@ -27,676 +27,467 @@ impl CreationSpec } } -#[derive(Debug)] +impl CreationSpecBuilder +{ + pub fn dimens(mut self, dimens: Dimens3<f32>) -> Self + { + self.width = dimens.width; + self.height = dimens.height; + self.depth = dimens.depth; + + self + } +} + +/// Describes a single side of a cube (obviously). +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Side { + /// +Z Front, + + /// -Z Back, + + /// -X Left, + + /// +X Right, + + /// +Y Top, + + /// -Y Bottom, } -#[derive(Debug)] -pub enum Corner +/// Describes what location on a side of a cube a face is. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum FaceLocation { - TopRight, - TopLeft, - BottomRight, - BottomLeft, + /// 🮝 + RightUp, + + /// 🮟 + LeftDown, } /// Creates a cube mesh. +/// +/// By default, the texture coordinates are arranged so that the full texture is visible +/// on every side. This can be changed inside of the `face_cb` function. pub fn create( creation_spec: CreationSpec, - vertex_builder_cb: impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, + face_cb: impl FnMut(FaceVertices, Side, FaceLocation) -> FaceVertices, ) -> Mesh { - let mut vertices = [const { None }; VertexIndex::VARIANT_CNT]; - - create_front(&creation_spec, &mut vertices, &vertex_builder_cb); - create_back(&creation_spec, &mut vertices, &vertex_builder_cb); - create_right(&creation_spec, &mut vertices, &vertex_builder_cb); - create_left(&creation_spec, &mut vertices, &vertex_builder_cb); - create_top(&creation_spec, &mut vertices, &vertex_builder_cb); - create_bottom(&creation_spec, &mut vertices, &vertex_builder_cb); - - Mesh::new( - vertices.map(Option::unwrap).to_vec(), - Some( - VERTEX_INDICES - .into_iter() - .flatten() - .map(|index| index as u32) - .collect(), - ), - ) -} + let mut data = Data::default(); -macro_rules! one { - ($tt: tt) => { - 1 - }; -} + create_side(&SidePositions::new_top(&creation_spec), &mut data); + create_side(&SidePositions::new_bottom(&creation_spec), &mut data); + create_side(&SidePositions::new_left(&creation_spec), &mut data); + create_side(&SidePositions::new_right(&creation_spec), &mut data); + create_side(&SidePositions::new_back(&creation_spec), &mut data); + create_side(&SidePositions::new_front(&creation_spec), &mut data); -macro_rules! enum_with_variant_cnt { - ( - $(#[$attr: meta])* - enum $name: ident { - $($variant: ident,)* - } - ) => { - $(#[$attr])* - enum $name { - $($variant,)* - } - - impl $name { - const VARIANT_CNT: usize = 0 $(+ one!($variant))*; - } - }; + data.into_mesh(face_cb) } -enum_with_variant_cnt! { -#[repr(u32)] -enum VertexIndex +#[derive(Debug, Default)] +struct Data { - FrontTopRight, - FrontBottomRight, - FrontBottomLeft, - FrontTopLeft, - - BackTopRight, - BackBottomRight, - BackBottomLeft, - BackTopLeft, - - RightBackTop, - RightBackBottom, - RightFrontTop, - RightFrontBottom, - - LeftBackTop, - LeftBackBottom, - LeftFrontTop, - LeftFrontBottom, - - TopBackRight, - TopBackLeft, - TopFrontRight, - TopFrontLeft, - - BottomBackRight, - BottomBackLeft, - BottomFrontRight, - BottomFrontLeft, -} + faces: Vec<Face>, + vertex_data: VertexData, } -fn create_front( - creation_spec: &CreationSpec, - vertices: &mut [Option<Vertex>], - vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) +#[derive(Debug, Default)] +struct VertexData { - let front_top_right_pos = Vec3 { - x: creation_spec.width / 2.0, - y: creation_spec.height / 2.0, - z: -(creation_spec.depth / 2.0), - }; - - let front_bottom_right_pos = Vec3 { - x: creation_spec.width / 2.0, - y: -(creation_spec.height / 2.0), - z: -(creation_spec.depth / 2.0), - }; + vertex_positions: Vec<Vec3<f32>>, + vertex_normals: Vec<Vec3<f32>>, +} - let front_bottom_left_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: -(creation_spec.height / 2.0), - z: -(creation_spec.depth / 2.0), - }; +impl Data +{ + fn into_mesh( + self, + mut face_cb: impl FnMut(FaceVertices, Side, FaceLocation) -> FaceVertices, + ) -> Mesh + { + let mut vertices = Vec::<Vertex>::with_capacity(self.faces.len() * 3); + let mut indices = Vec::<u32>::with_capacity(self.faces.len() * 3); - let front_top_left_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: creation_spec.height / 2.0, - z: -(creation_spec.depth / 2.0), - }; + let mut face_location = FaceLocation::RightUp; - let front_normal = calc_triangle_surface_normal( - &front_top_right_pos, - &front_bottom_right_pos, - &front_top_left_pos, - ); - - vertices[VertexIndex::FrontTopRight as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(front_top_right_pos) - .normal(front_normal), - Side::Front, - Corner::TopRight, - ) - .build(), - ); - - vertices[VertexIndex::FrontBottomRight as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(front_bottom_right_pos) - .normal(front_normal), - Side::Front, - Corner::BottomRight, - ) - .build(), - ); - - vertices[VertexIndex::FrontBottomLeft as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(front_bottom_left_pos) - .normal(front_normal), - Side::Front, - Corner::BottomLeft, - ) - .build(), - ); - - vertices[VertexIndex::FrontTopLeft as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(front_top_left_pos) - .normal(front_normal), - Side::Front, - Corner::TopLeft, - ) - .build(), - ); -} + let Self { faces, vertex_data } = self; -fn create_back( - creation_spec: &CreationSpec, - vertices: &mut [Option<Vertex>], - vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) -{ - let back_top_right_pos = Vec3 { - x: creation_spec.width / 2.0, - y: creation_spec.height / 2.0, - z: creation_spec.depth / 2.0, - }; + for face in faces { + let side = face.side; - let back_bottom_right_pos = Vec3 { - x: creation_spec.width / 2.0, - y: -(creation_spec.height / 2.0), - z: creation_spec.depth / 2.0, - }; + let face_vertices = face_cb( + FaceVertices::new(face, &vertex_data) + .with_full_per_side_tex_coords(face_location), + side, + face_location, + ); - let back_bottom_left_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: -(creation_spec.height / 2.0), - z: creation_spec.depth / 2.0, - }; + for vertex in face_vertices.vertices { + if let Some((prev_vertex_index, _)) = vertices + .iter() + .enumerate() + .find(|(_, prev_vertex)| *prev_vertex == &vertex) + { + indices + .push(u32::try_from(prev_vertex_index).expect( + "Vertex index does not fit into 32-bit unsigned int", + )); - let back_top_left_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: creation_spec.height / 2.0, - z: creation_spec.depth / 2.0, - }; + continue; + } - let back_normal = -calc_triangle_surface_normal( - &back_top_right_pos, - &back_bottom_right_pos, - &back_top_left_pos, - ); - - vertices[VertexIndex::BackTopRight as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(back_top_right_pos) - .normal(back_normal), - Side::Back, - Corner::TopRight, - ) - .build(), - ); - - vertices[VertexIndex::BackBottomRight as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(back_bottom_right_pos) - .normal(back_normal), - Side::Back, - Corner::BottomRight, - ) - .build(), - ); - - vertices[VertexIndex::BackBottomLeft as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(back_bottom_left_pos) - .normal(back_normal), - Side::Back, - Corner::BottomLeft, - ) - .build(), - ); - - vertices[VertexIndex::BackTopLeft as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(back_top_left_pos) - .normal(back_normal), - Side::Back, - Corner::TopLeft, - ) - .build(), - ); -} + vertices.push(vertex); -fn create_right( - creation_spec: &CreationSpec, - vertices: &mut [Option<Vertex>], - vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) -{ - let right_back_top_pos = Vec3 { - x: creation_spec.width / 2.0, - y: creation_spec.height / 2.0, - z: creation_spec.depth / 2.0, - }; + let vertex_index = u32::try_from(vertices.len() - 1) + .expect("Vertex index does not fit into 32-bit unsigned int"); - let right_back_bottom_pos = Vec3 { - x: creation_spec.width / 2.0, - y: -(creation_spec.height / 2.0), - z: creation_spec.depth / 2.0, - }; + indices.push(vertex_index); + } - let right_front_top_pos = Vec3 { - x: creation_spec.width / 2.0, - y: creation_spec.height / 2.0, - z: -(creation_spec.depth / 2.0), - }; + match face_location { + FaceLocation::RightUp => face_location = FaceLocation::LeftDown, + FaceLocation::LeftDown => face_location = FaceLocation::RightUp, + } + } - let right_front_bottom_pos = Vec3 { - x: creation_spec.width / 2.0, - y: -(creation_spec.height / 2.0), - z: -(creation_spec.depth / 2.0), - }; + Mesh::new(vertices, Some(indices)) + } +} - let right_normal = calc_triangle_surface_normal( - &right_back_top_pos, - &right_back_bottom_pos, - &right_front_top_pos, - ); - - vertices[VertexIndex::RightBackTop as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(right_back_top_pos) - .normal(right_normal), - Side::Right, - Corner::TopLeft, - ) - .build(), - ); - - vertices[VertexIndex::RightBackBottom as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(right_back_bottom_pos) - .normal(right_normal), - Side::Right, - Corner::BottomLeft, - ) - .build(), - ); - - vertices[VertexIndex::RightFrontTop as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(right_front_top_pos) - .normal(right_normal), - Side::Right, - Corner::TopRight, - ) - .build(), - ); - - vertices[VertexIndex::RightFrontBottom as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(right_front_bottom_pos) - .normal(right_normal), - Side::Right, - Corner::BottomRight, - ) - .build(), - ); +/// The vertices of a single face of a cube. +#[derive(Debug, Default, Clone)] +pub struct FaceVertices +{ + /// The three vertices of a face in counter-clockwise order. + /// + /// Order when [`FaceLocation::RightUp`]: + /// ```text + /// ₂ ₁ + /// 🮝 + /// ³ + /// ``` + /// + /// Order when [`FaceLocation::LeftDown`]: + /// ```text + /// ₁ + /// 🮟 + /// ² ³ + /// ``` + pub vertices: [Vertex; 3], } -fn create_left( - creation_spec: &CreationSpec, - vertices: &mut [Option<Vertex>], - vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) +impl FaceVertices { - let left_back_top_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: creation_spec.height / 2.0, - z: creation_spec.depth / 2.0, - }; + fn new(face: Face, vertex_data: &VertexData) -> Self + { + Self { + vertices: face.vertices.map(|face_vertex| { + let vertex_pos = vertex_data + .vertex_positions + .get(face_vertex.pos_index as usize) + .expect("Vertex position index is out of bounds") + .clone(); + + let vertex_normal = vertex_data + .vertex_normals + .get(face_vertex.normal_index as usize) + .expect("Vertex normal index is out of bounds") + .clone(); + + Vertex::builder() + .pos(vertex_pos) + .normal(vertex_normal) + .build() + }), + } + } - let left_back_bottom_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: -(creation_spec.height / 2.0), - z: creation_spec.depth / 2.0, - }; + fn with_full_per_side_tex_coords(mut self, face_location: FaceLocation) -> Self + { + match face_location { + FaceLocation::RightUp => { + self.vertices[0].texture_coords = Vec2 { x: 1.0, y: 1.0 }; + self.vertices[1].texture_coords = Vec2 { x: 0.0, y: 1.0 }; + self.vertices[2].texture_coords = Vec2 { x: 1.0, y: 0.0 }; + } + FaceLocation::LeftDown => { + self.vertices[0].texture_coords = Vec2 { x: 0.0, y: 1.0 }; + self.vertices[1].texture_coords = Vec2 { x: 0.0, y: 0.0 }; + self.vertices[2].texture_coords = Vec2 { x: 1.0, y: 0.0 }; + } + }; + + self + } +} - let left_front_top_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: creation_spec.height / 2.0, - z: -(creation_spec.depth / 2.0), - }; +#[derive(Debug)] +struct Face +{ + vertices: [FaceVertex; 3], + side: Side, +} - let left_front_bottom_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: -(creation_spec.height / 2.0), - z: -(creation_spec.depth / 2.0), - }; +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +struct FaceVertex +{ + pos_index: u32, + normal_index: u32, +} - let left_normal = -calc_triangle_surface_normal( - &left_back_top_pos, - &left_back_bottom_pos, - &left_front_top_pos, - ); - - vertices[VertexIndex::LeftBackTop as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(left_back_top_pos) - .normal(left_normal), - Side::Left, - Corner::TopRight, - ) - .build(), - ); - - vertices[VertexIndex::LeftBackBottom as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(left_back_bottom_pos) - .normal(left_normal), - Side::Left, - Corner::BottomRight, - ) - .build(), - ); - - vertices[VertexIndex::LeftFrontTop as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(left_front_top_pos) - .normal(left_normal), - Side::Left, - Corner::TopLeft, - ) - .build(), - ); - - vertices[VertexIndex::LeftFrontBottom as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(left_front_bottom_pos) - .normal(left_normal), - Side::Left, - Corner::BottomLeft, - ) - .build(), - ); +#[derive(Debug)] +struct SidePositions +{ + up_left: Vec3<f32>, + up_right: Vec3<f32>, + down_left: Vec3<f32>, + down_right: Vec3<f32>, + normal_calc_order: NormalCalcOrder, + side: Side, } -fn create_top( - creation_spec: &CreationSpec, - vertices: &mut [Option<Vertex>], - vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) +impl SidePositions { - let top_back_right_pos = Vec3 { - x: creation_spec.width / 2.0, - y: creation_spec.height / 2.0, - z: creation_spec.depth / 2.0, - }; + fn new_top(creation_spec: &CreationSpec) -> Self + { + let up_left = Vec3 { + x: -(creation_spec.width / 2.0), + y: creation_spec.height / 2.0, + z: creation_spec.depth / 2.0, + }; + + let down_right = Vec3 { + x: creation_spec.width / 2.0, + y: creation_spec.height / 2.0, + z: -(creation_spec.depth / 2.0), + }; + + Self { + up_left, + up_right: Vec3 { x: down_right.x, ..up_left.clone() }, + down_left: Vec3 { x: up_left.x, ..down_right.clone() }, + down_right, + normal_calc_order: NormalCalcOrder::Clockwise, + side: Side::Top, + } + } - let top_back_left_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: creation_spec.height / 2.0, - z: creation_spec.depth / 2.0, - }; + fn new_bottom(creation_spec: &CreationSpec) -> Self + { + let up_left = Vec3 { + x: -(creation_spec.width / 2.0), + y: -creation_spec.height / 2.0, + z: creation_spec.depth / 2.0, + }; + + let down_right = Vec3 { + x: creation_spec.width / 2.0, + y: -creation_spec.height / 2.0, + z: -(creation_spec.depth / 2.0), + }; + + Self { + up_left, + up_right: Vec3 { x: down_right.x, ..up_left.clone() }, + down_left: Vec3 { x: up_left.x, ..down_right.clone() }, + down_right, + normal_calc_order: NormalCalcOrder::CounterClockwise, + side: Side::Bottom, + } + } - let top_front_left_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: creation_spec.height / 2.0, - z: -(creation_spec.depth / 2.0), - }; + fn new_left(creation_spec: &CreationSpec) -> Self + { + let up_left = Vec3 { + x: -(creation_spec.width / 2.0), + y: creation_spec.height / 2.0, + z: -(creation_spec.depth / 2.0), + }; + + let down_right = Vec3 { + x: -(creation_spec.width / 2.0), + y: -(creation_spec.height / 2.0), + z: creation_spec.depth / 2.0, + }; + + Self { + up_left, + up_right: Vec3 { z: down_right.z, ..up_left.clone() }, + down_left: Vec3 { z: up_left.z, ..down_right.clone() }, + down_right, + normal_calc_order: NormalCalcOrder::CounterClockwise, + side: Side::Left, + } + } - let top_front_right_pos = Vec3 { - x: creation_spec.width / 2.0, - y: creation_spec.height / 2.0, - z: -(creation_spec.depth / 2.0), - }; + fn new_right(creation_spec: &CreationSpec) -> Self + { + let up_left = Vec3 { + x: (creation_spec.width / 2.0), + y: creation_spec.height / 2.0, + z: -(creation_spec.depth / 2.0), + }; + + let down_right = Vec3 { + x: (creation_spec.width / 2.0), + y: -(creation_spec.height / 2.0), + z: creation_spec.depth / 2.0, + }; + + Self { + up_left, + up_right: Vec3 { z: down_right.z, ..up_left.clone() }, + down_left: Vec3 { z: up_left.z, ..down_right.clone() }, + down_right, + normal_calc_order: NormalCalcOrder::Clockwise, + side: Side::Right, + } + } + + fn new_back(creation_spec: &CreationSpec) -> Self + { + let up_left = Vec3 { + x: -(creation_spec.width / 2.0), + y: creation_spec.height / 2.0, + z: -creation_spec.depth / 2.0, + }; + + let down_right = Vec3 { + x: creation_spec.width / 2.0, + y: -(creation_spec.height / 2.0), + z: -creation_spec.depth / 2.0, + }; + + Self { + up_left, + up_right: Vec3 { x: down_right.x, ..up_left.clone() }, + down_left: Vec3 { x: up_left.x, ..down_right.clone() }, + down_right, + normal_calc_order: NormalCalcOrder::Clockwise, + side: Side::Back, + } + } - let top_normal = -calc_triangle_surface_normal( - &top_back_right_pos, - &top_back_left_pos, - &top_front_right_pos, - ); - - vertices[VertexIndex::TopBackRight as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(top_back_right_pos) - .normal(top_normal), - Side::Top, - Corner::TopRight, - ) - .build(), - ); - - vertices[VertexIndex::TopBackLeft as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(top_back_left_pos) - .normal(top_normal), - Side::Top, - Corner::TopLeft, - ) - .build(), - ); - - vertices[VertexIndex::TopFrontLeft as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(top_front_left_pos) - .normal(top_normal), - Side::Top, - Corner::BottomLeft, - ) - .build(), - ); - - vertices[VertexIndex::TopFrontRight as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(top_front_right_pos) - .normal(top_normal), - Side::Top, - Corner::BottomRight, - ) - .build(), - ); + fn new_front(creation_spec: &CreationSpec) -> Self + { + let up_left = Vec3 { + x: -(creation_spec.width / 2.0), + y: creation_spec.height / 2.0, + z: creation_spec.depth / 2.0, + }; + + let down_right = Vec3 { + x: creation_spec.width / 2.0, + y: -(creation_spec.height / 2.0), + z: creation_spec.depth / 2.0, + }; + + Self { + up_left, + up_right: Vec3 { x: down_right.x, ..up_left.clone() }, + down_left: Vec3 { x: up_left.x, ..down_right.clone() }, + down_right, + normal_calc_order: NormalCalcOrder::CounterClockwise, + side: Side::Front, + } + } } -fn create_bottom( - creation_spec: &CreationSpec, - vertices: &mut [Option<Vertex>], - vertex_builder_cb: &impl Fn(VertexBuilder, Side, Corner) -> VertexBuilder, -) +#[derive(Debug)] +enum NormalCalcOrder { - let bottom_back_right_pos = Vec3 { - x: creation_spec.width / 2.0, - y: -(creation_spec.height / 2.0), - z: (creation_spec.depth / 2.0), - }; + Clockwise, + CounterClockwise, +} - let bottom_back_left_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: -(creation_spec.height / 2.0), - z: creation_spec.depth / 2.0, +fn create_side(side_positions: &SidePositions, data: &mut Data) +{ + let normal = match side_positions.normal_calc_order { + NormalCalcOrder::Clockwise => calc_triangle_surface_normal( + &side_positions.up_left, + &side_positions.up_right, + &side_positions.down_left, + ), + NormalCalcOrder::CounterClockwise => calc_triangle_surface_normal( + &side_positions.up_left, + &side_positions.down_left, + &side_positions.up_right, + ), }; - let bottom_front_right_pos = Vec3 { - x: creation_spec.width / 2.0, - y: -(creation_spec.height / 2.0), - z: -(creation_spec.depth / 2.0), - }; + data.vertex_data.vertex_normals.push(normal); - let bottom_front_left_pos = Vec3 { - x: -(creation_spec.width / 2.0), - y: -(creation_spec.height / 2.0), - z: -(creation_spec.depth / 2.0), - }; + let top_normal_index = data.vertex_data.vertex_normals.len() - 1; - let bottom_normal = calc_triangle_surface_normal( - &bottom_back_right_pos, - &bottom_back_left_pos, - &bottom_front_right_pos, - ); - - vertices[VertexIndex::BottomBackRight as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(bottom_back_right_pos) - .normal(bottom_normal), - Side::Bottom, - Corner::BottomRight, - ) - .build(), - ); - - vertices[VertexIndex::BottomBackLeft as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(bottom_back_left_pos) - .normal(bottom_normal), - Side::Bottom, - Corner::BottomLeft, - ) - .build(), - ); - - vertices[VertexIndex::BottomFrontRight as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(bottom_front_right_pos) - .normal(bottom_normal), - Side::Bottom, - Corner::TopRight, - ) - .build(), - ); - - vertices[VertexIndex::BottomFrontLeft as usize] = Some( - vertex_builder_cb( - VertexBuilder::default() - .pos(bottom_front_left_pos) - .normal(bottom_normal), - Side::Bottom, - Corner::TopLeft, - ) - .build(), - ); -} + data.vertex_data + .vertex_positions + .push(side_positions.up_right); -const VERTEX_INDICES_FRONT: [VertexIndex; 6] = [ - // 🮝 - VertexIndex::FrontTopRight, - VertexIndex::FrontBottomRight, - VertexIndex::FrontTopLeft, - // - // 🮟 - VertexIndex::FrontBottomRight, - VertexIndex::FrontBottomLeft, - VertexIndex::FrontTopLeft, -]; + let up_right_pos_index = data.vertex_data.vertex_positions.len() - 1; -const VERTEX_INDICES_BACK: [VertexIndex; 6] = [ - // 🮝 - VertexIndex::BackTopRight, - VertexIndex::BackBottomRight, - VertexIndex::BackTopLeft, - // - // 🮟 - VertexIndex::BackBottomRight, - VertexIndex::BackBottomLeft, - VertexIndex::BackTopLeft, -]; + data.vertex_data + .vertex_positions + .push(side_positions.up_left); -const VERTEX_INDICES_RIGHT: [VertexIndex; 6] = [ - // 🮝 - VertexIndex::RightBackTop, - VertexIndex::RightBackBottom, - VertexIndex::RightFrontTop, - // - // 🮟 - VertexIndex::RightBackBottom, - VertexIndex::RightFrontBottom, - VertexIndex::RightFrontTop, -]; + let up_left_pos_index = data.vertex_data.vertex_positions.len() - 1; -const VERTEX_INDICES_LEFT: [VertexIndex; 6] = [ - // 🮝 - VertexIndex::LeftBackTop, - VertexIndex::LeftBackBottom, - VertexIndex::LeftFrontTop, - // - // 🮟 - VertexIndex::LeftBackBottom, - VertexIndex::LeftFrontBottom, - VertexIndex::LeftFrontTop, -]; + data.vertex_data + .vertex_positions + .push(side_positions.down_left); -const VERTEX_INDICES_TOP: [VertexIndex; 6] = [ - // 🮝 - VertexIndex::TopBackRight, - VertexIndex::TopBackLeft, - VertexIndex::TopFrontRight, - // - // 🮟 - VertexIndex::TopBackLeft, - VertexIndex::TopFrontLeft, - VertexIndex::TopFrontRight, -]; + let down_left_pos_index = data.vertex_data.vertex_positions.len() - 1; + + data.vertex_data + .vertex_positions + .push(side_positions.down_right); + + let down_right_pos_index = data.vertex_data.vertex_positions.len() - 1; -const VERTEX_INDICES_BOTTOM: [VertexIndex; 6] = [ // 🮝 - VertexIndex::BottomBackRight, - VertexIndex::BottomBackLeft, - VertexIndex::BottomFrontRight, - // + data.faces.push(Face { + vertices: [ + FaceVertex { + pos_index: up_right_pos_index as u32, + normal_index: top_normal_index as u32, + }, + FaceVertex { + pos_index: up_left_pos_index as u32, + normal_index: top_normal_index as u32, + }, + FaceVertex { + pos_index: down_right_pos_index as u32, + normal_index: top_normal_index as u32, + }, + ], + side: side_positions.side, + }); + // 🮟 - VertexIndex::BottomBackLeft, - VertexIndex::BottomFrontLeft, - VertexIndex::BottomFrontRight, -]; - -const VERTEX_INDICES: [[VertexIndex; 6]; 6] = [ - VERTEX_INDICES_FRONT, - VERTEX_INDICES_BACK, - VERTEX_INDICES_RIGHT, - VERTEX_INDICES_LEFT, - VERTEX_INDICES_TOP, - VERTEX_INDICES_BOTTOM, -]; + data.faces.push(Face { + vertices: [ + FaceVertex { + pos_index: up_left_pos_index as u32, + normal_index: top_normal_index as u32, + }, + FaceVertex { + pos_index: down_left_pos_index as u32, + normal_index: top_normal_index as u32, + }, + FaceVertex { + pos_index: down_right_pos_index as u32, + normal_index: top_normal_index as u32, + }, + ], + side: side_positions.side, + }); +} diff --git a/engine/src/model.rs b/engine/src/model.rs new file mode 100644 index 0000000..9f5840c --- /dev/null +++ b/engine/src/model.rs @@ -0,0 +1,176 @@ +use std::borrow::Cow; +use std::collections::HashMap; +use std::fs::read_to_string; +use std::path::Path; + +use ecs::Component; + +use crate::asset::{Assets, Handle as AssetHandle, Submitter as AssetSubmitter}; +use crate::material::Material; +use crate::mesh::Mesh; +use crate::texture::Texture; + +#[derive(Debug, Clone, Component)] +#[non_exhaustive] +pub struct Model +{ + pub asset_handle: AssetHandle<Data>, +} + +impl Model +{ + pub fn new(asset_handle: AssetHandle<Data>) -> Self + { + Self { asset_handle } + } +} + +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct Data +{ + pub mesh: Mesh, + pub materials: HashMap<String, Material>, +} + +impl Data +{ + pub fn builder() -> DataBuilder + { + DataBuilder::default() + } +} + +#[derive(Debug, Default, Clone)] +pub struct DataBuilder +{ + mesh: Mesh, + materials: HashMap<String, Material>, +} + +impl DataBuilder +{ + pub fn mesh(mut self, mesh: Mesh) -> Self + { + self.mesh = mesh; + + self + } + + pub fn material<'name>( + mut self, + name: impl Into<Cow<'name, str>>, + material: Material, + ) -> Self + { + self.materials.insert(name.into().into_owned(), material); + + self + } + + pub fn build(self) -> Data + { + Data { + mesh: self.mesh, + materials: self.materials, + } + } +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct Settings {} + +#[derive(Debug, thiserror::Error)] +enum Error +{ + #[error("Failed to read model file")] + ReadModelFileFailed(#[source] std::io::Error), + + #[error("Failed to read material file")] + ReadMaterialFileFailed(#[source] std::io::Error), + + #[error("Failed to parse model file")] + ParsingFailed(#[from] ParsingError), +} + +pub fn set_asset_importers(assets: &mut Assets) +{ + assets.set_importer(["obj"], import_wavefront_obj_asset); +} + +#[derive(Debug, thiserror::Error)] +enum ParsingError +{ + #[error(transparent)] + Obj(#[from] crate::file_format::wavefront::obj::Error), + + #[error(transparent)] + Mtl(#[from] crate::file_format::wavefront::mtl::Error), +} + +fn import_wavefront_obj_asset( + asset_submitter: &mut AssetSubmitter<'_>, + path: &Path, + _settings: Option<&'_ Settings>, +) -> Result<(), Error> +{ + let obj = crate::file_format::wavefront::obj::parse( + &read_to_string(path).map_err(Error::ReadModelFileFailed)?, + ) + .map_err(|err| Error::ParsingFailed(ParsingError::Obj(err)))?; + + let mesh = obj + .to_mesh() + .map_err(|err| Error::ParsingFailed(ParsingError::Obj(err)))?; + + let mut materials = + HashMap::<String, Material>::with_capacity(obj.mtl_libs.iter().flatten().count()); + + for mtl_lib_path in obj.mtl_libs.iter().flatten() { + materials.extend(import_mtl(asset_submitter, &mtl_lib_path)?); + } + + asset_submitter.submit_store(Data { mesh, materials }); + + Ok(()) +} + +fn import_mtl<'a>( + asset_submitter: &'a AssetSubmitter<'_>, + path: &Path, +) -> Result<impl Iterator<Item = (String, Material)> + 'a, Error> +{ + let named_materials = crate::file_format::wavefront::mtl::parse( + &read_to_string(path).map_err(Error::ReadMaterialFileFailed)?, + ) + .map_err(|err| Error::ParsingFailed(ParsingError::Mtl(err)))?; + + Ok(named_materials.into_iter().map(|named_material| { + let mut material_builder = Material::builder() + .ambient(named_material.ambient) + .diffuse(named_material.diffuse) + .specular(named_material.specular) + .shininess(named_material.shininess); + + if let Some(ambient_map) = named_material.ambient_map { + material_builder = material_builder.ambient_map(Texture::new( + asset_submitter.submit_load_other(ambient_map.path.as_path()), + )); + } + + if let Some(diffuse_map) = named_material.diffuse_map { + material_builder = material_builder.diffuse_map(Texture::new( + asset_submitter.submit_load_other(diffuse_map.path.as_path()), + )); + } + + if let Some(specular_map) = named_material.specular_map { + material_builder = material_builder.specular_map(Texture::new( + asset_submitter.submit_load_other(specular_map.path.as_path()), + )); + } + + (named_material.name, material_builder.build()) + })) +} diff --git a/engine/src/opengl/buffer.rs b/engine/src/opengl/buffer.rs index 2be7f12..eded553 100644 --- a/engine/src/opengl/buffer.rs +++ b/engine/src/opengl/buffer.rs @@ -1,5 +1,6 @@ use std::marker::PhantomData; use std::mem::size_of_val; +use std::ptr::null; #[derive(Debug)] pub struct Buffer<Item> @@ -35,32 +36,40 @@ impl<Item> Buffer<Item> } } - pub fn object(&self) -> gl::types::GLuint + pub fn store_mapped<Value>( + &mut self, + values: &[Value], + usage: Usage, + mut map_func: impl FnMut(&Value) -> Item, + ) { - self.buf - } + unsafe { + #[allow(clippy::cast_possible_wrap)] + gl::NamedBufferData( + self.buf, + (size_of::<Item>() * values.len()) as gl::types::GLsizeiptr, + null(), + usage.into_gl(), + ); + } - /// Does a weak clone of this buffer. The buffer itself is NOT copied in any way this - /// function only copies the internal buffer ID. - /// - /// # Safety - /// The returned `Buffer` must not be dropped if another `Buffer` referencing the - /// same buffer ID is used later or if a [`VertexArray`] is used later. - /// - /// [`VertexArray`]: crate::opengl::vertex_array::VertexArray - pub unsafe fn clone_weak(&self) -> Self - { - Self { buf: self.buf, _pd: PhantomData } + for (index, value) in values.iter().enumerate() { + let item = map_func(value); + + unsafe { + gl::NamedBufferSubData( + self.buf, + (index * size_of::<Item>()) as gl::types::GLintptr, + size_of::<Item>() as gl::types::GLsizeiptr, + (&raw const item).cast(), + ); + } + } } -} -impl<Item> Drop for Buffer<Item> -{ - fn drop(&mut self) + pub fn object(&self) -> gl::types::GLuint { - unsafe { - gl::DeleteBuffers(1, &self.buf); - } + self.buf } } diff --git a/engine/src/shader_preprocessor.rs b/engine/src/opengl/glsl.rs index 70696ac..6fd5638 100644 --- a/engine/src/shader_preprocessor.rs +++ b/engine/src/opengl/glsl.rs @@ -1,183 +1,181 @@ +use std::borrow::Cow; +use std::fmt::{Display, Formatter}; use std::path::{Path, PathBuf}; use std::string::FromUtf8Error; const PREINCLUDE_DIRECTIVE: &str = "#preinclude"; -/// Preprocessor for shaders written in the OpenGL Shading Language. -pub struct ShaderPreprocessor +pub fn preprocess<'content>( + shader_content: impl Into<Cow<'content, str>>, + read_file: &impl Fn(&Path) -> Result<Vec<u8>, std::io::Error>, +) -> Result<Cow<'content, str>, PreprocessingError> { - read_file: fn(&Path) -> Result<Vec<u8>, std::io::Error>, - base_dir: PathBuf, + do_preprocess(shader_content, SpanPath::Original, read_file) } -impl ShaderPreprocessor +fn do_preprocess<'content>( + shader_content: impl Into<Cow<'content, str>>, + shader_path: SpanPath<'_>, + read_file: &impl Fn(&Path) -> Result<Vec<u8>, std::io::Error>, +) -> Result<Cow<'content, str>, PreprocessingError> { - pub fn new(base_dir: PathBuf) -> Self - { - Self { - read_file: |path| std::fs::read(path), - base_dir, - } - } + let shader_content = shader_content.into(); - pub fn preprocess( - &self, - shader_content: String, - shader_file_path: &Path, - ) -> Result<String, Error> - { - let mut preincludes = shader_content - .match_indices(PREINCLUDE_DIRECTIVE) - .peekable(); + let mut preincludes = shader_content + .match_indices(PREINCLUDE_DIRECTIVE) + .peekable(); - if preincludes.peek().is_none() { - // Shader content contains no preincludes - return Ok(shader_content); - }; + if preincludes.peek().is_none() { + // Shader content contains no preincludes + return Ok(shader_content.into()); + }; - let mut preprocessed = shader_content.clone(); + let mut preprocessed = shader_content.to_string(); - let mut curr = shader_content.find(PREINCLUDE_DIRECTIVE); + let mut curr = shader_content.find(PREINCLUDE_DIRECTIVE); - let mut last_start = 0; - let mut span_line_offset = 0; + let mut last_start = 0; + let mut span_line_offset = 0; - while let Some(preinclude_start) = curr { - let replacement_job = self.handle_preinclude( - &preprocessed, - shader_file_path, - preinclude_start, - span_line_offset, - )?; + while let Some(preinclude_start) = curr { + let replacement_job = handle_preinclude( + &preprocessed, + &shader_path, + preinclude_start, + span_line_offset, + )?; - let path = replacement_job.path.clone(); + let path = replacement_job.path.clone(); - let mut included = String::from_utf8( - (self.read_file)(&self.base_dir.join(replacement_job.path.clone())) - .map_err(|err| Error::ReadIncludedShaderFailed { - source: err, - path: replacement_job.path.clone(), - })?, - ) - .map_err(|err| Error::IncludedShaderInvalidUtf8 { - source: err, - path: path.clone(), + let mut included = + String::from_utf8(read_file(&replacement_job.path).map_err(|err| { + PreprocessingError::ReadIncludedShaderFailed { + source: err, + path: replacement_job.path.clone(), + } + })?) + .map_err(|err| { + PreprocessingError::IncludedShaderInvalidUtf8 { + source: err, + path: path.clone(), + } })?; - if let Some(first_line) = included.lines().next() { - if first_line.starts_with("#version") { - included = included - .chars() - .skip_while(|character| *character != '\n') - .collect(); - } + if let Some(first_line) = included.lines().next() { + if first_line.starts_with("#version") { + included = included + .chars() + .skip_while(|character| *character != '\n') + .collect(); } - - let included_preprocessed = - self.preprocess(included, &replacement_job.path)?; - - let start = replacement_job.start_index; - let end = replacement_job.end_index; - - preprocessed.replace_range(start..end, &included_preprocessed); - - curr = preprocessed[last_start + 1..] - .find(PREINCLUDE_DIRECTIVE) - .map(|index| index + 1); - - last_start = preinclude_start + included_preprocessed.len(); - - span_line_offset += included_preprocessed.lines().count(); } - Ok(preprocessed) - } + let included_preprocessed = do_preprocess( + &included, + SpanPath::Path(replacement_job.path.as_path().into()), + read_file, + )?; - fn handle_preinclude( - &self, - shader_content: &str, - shader_file_path: &Path, - preinclude_start_index: usize, - span_line_offset: usize, - ) -> Result<ReplacementJob, Error> - { - let expect_token = |token: char, index: usize| { - let token_found = shader_content.chars().nth(index).ok_or_else(|| { - Error::ExpectedToken { - expected: token, - span: Span::new( - shader_content, - &self.base_dir.join(shader_file_path), - index, - span_line_offset, - preinclude_start_index, - ), - } - })?; + let start = replacement_job.start_index; + let end = replacement_job.end_index; - if token_found != token { - return Err(Error::InvalidToken { - expected: token, - found: token_found, - span: Span::new( - shader_content, - &self.base_dir.join(shader_file_path), - index, - span_line_offset, - preinclude_start_index, - ), - }); - } + preprocessed.replace_range(start..end, &included_preprocessed); - Ok(()) - }; + curr = preprocessed[last_start + 1..] + .find(PREINCLUDE_DIRECTIVE) + .map(|index| index + 1); - let space_index = preinclude_start_index + PREINCLUDE_DIRECTIVE.len(); - let quote_open_index = space_index + 1; + last_start = preinclude_start + included_preprocessed.len(); - expect_token(' ', space_index)?; - expect_token('"', quote_open_index)?; + span_line_offset += included_preprocessed.lines().count(); + } - let buf = shader_content[quote_open_index + 1..] - .chars() - .take_while(|character| *character != '"') - .map(|character| character as u8) - .collect::<Vec<_>>(); + Ok(preprocessed.into()) +} - if buf.is_empty() { - return Err(Error::ExpectedToken { - expected: '"', +fn handle_preinclude( + shader_content: &str, + shader_path: &SpanPath<'_>, + preinclude_start_index: usize, + span_line_offset: usize, +) -> Result<ReplacementJob, PreprocessingError> +{ + let expect_token = |token: char, index: usize| { + let token_found = shader_content.chars().nth(index).ok_or_else(|| { + PreprocessingError::ExpectedToken { + expected: token, span: Span::new( shader_content, - &self.base_dir.join(shader_file_path), - shader_content.len() - 1, + shader_path.to_owned(), + index, span_line_offset, preinclude_start_index, ), - }); - } - - let path_len = buf.len(); + } + })?; - let path = PathBuf::from(String::from_utf8(buf).map_err(|err| { - Error::PreincludePathInvalidUtf8 { - source: err, + if token_found != token { + return Err(PreprocessingError::InvalidToken { + expected: token, + found: token_found, span: Span::new( shader_content, - &self.base_dir.join(shader_file_path), - quote_open_index + 1, + shader_path.to_owned(), + index, span_line_offset, preinclude_start_index, ), - } - })?); + }); + } - Ok(ReplacementJob { - start_index: preinclude_start_index, - end_index: quote_open_index + 1 + path_len + 1, - path, - }) + Ok(()) + }; + + let space_index = preinclude_start_index + PREINCLUDE_DIRECTIVE.len(); + let quote_open_index = space_index + 1; + + expect_token(' ', space_index)?; + expect_token('"', quote_open_index)?; + + let buf = shader_content[quote_open_index + 1..] + .chars() + .take_while(|character| *character != '"') + .map(|character| character as u8) + .collect::<Vec<_>>(); + + if buf.is_empty() { + return Err(PreprocessingError::ExpectedToken { + expected: '"', + span: Span::new( + shader_content, + shader_path.to_owned(), + shader_content.len() - 1, + span_line_offset, + preinclude_start_index, + ), + }); } + + let path_len = buf.len(); + + let path = PathBuf::from(String::from_utf8(buf).map_err(|err| { + PreprocessingError::PreincludePathInvalidUtf8 { + source: err, + span: Span::new( + shader_content, + shader_path.to_owned(), + quote_open_index + 1, + span_line_offset, + preinclude_start_index, + ), + } + })?); + + Ok(ReplacementJob { + start_index: preinclude_start_index, + end_index: quote_open_index + 1 + path_len + 1, + path, + }) } struct ReplacementJob @@ -187,15 +185,14 @@ struct ReplacementJob path: PathBuf, } -/// Shader preprocessing error. #[derive(Debug, thiserror::Error)] -pub enum Error +pub enum PreprocessingError { #[error( "Invalid token at line {}, column {} of {}. Expected '{}', found '{}'", span.line, span.column, - span.file.display(), + span.path, expected, found )] @@ -211,7 +208,7 @@ pub enum Error expected, span.line, span.column, - span.file.display(), + span.path )] ExpectedToken { @@ -222,7 +219,7 @@ pub enum Error "Preinclude path at line {}, column {} of {} is invalid UTF-8", span.line, span.column, - span.file.display(), + span.path )] PreincludePathInvalidUtf8 { @@ -249,18 +246,19 @@ pub enum Error } #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub struct Span { - line: usize, - column: usize, - file: PathBuf, + pub line: usize, + pub column: usize, + pub path: SpanPath<'static>, } impl Span { fn new( file_content: &str, - file_path: &Path, + path: SpanPath<'static>, char_index: usize, line_offset: usize, line_start_index: usize, @@ -272,7 +270,49 @@ impl Span Self { line, column: char_index - line_start_index + 1, - file: file_path.to_path_buf(), + path, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SpanPath<'a> +{ + Original, + Path(Cow<'a, Path>), +} + +impl<'a> SpanPath<'a> +{ + fn to_owned(&self) -> SpanPath<'static> + { + match self { + Self::Original => SpanPath::Original, + Self::Path(path) => SpanPath::Path(Cow::Owned(path.to_path_buf().into())), + } + } +} + +impl<'a> Display for SpanPath<'a> +{ + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result + { + match self { + Self::Original => write!(formatter, "original file"), + Self::Path(path) => write!(formatter, "file {}", path.display()), + } + } +} + +impl<'a, PathLike> PartialEq<PathLike> for SpanPath<'a> +where + PathLike: AsRef<Path>, +{ + fn eq(&self, other: &PathLike) -> bool + { + match self { + Self::Original => false, + Self::Path(path) => path == other.as_ref(), } } } @@ -290,21 +330,17 @@ fn find_line_of_index(text: &str, index: usize) -> usize mod tests { use std::ffi::OsStr; - use std::path::{Path, PathBuf}; + use std::path::Path; - use super::{Error, ShaderPreprocessor}; + use super::{preprocess, PreprocessingError}; + use crate::opengl::glsl::SpanPath; #[test] fn preprocess_no_directives_is_same() { assert_eq!( - ShaderPreprocessor { - read_file: |_| { unreachable!() }, - base_dir: PathBuf::new() - } - .preprocess("#version 330 core\n".to_string(), Path::new("foo.glsl"),) - .unwrap(), - "#version 330 core\n".to_string() + preprocess("#version 330 core\n", &|_| { unreachable!() }).unwrap(), + "#version 330 core\n" ); } @@ -312,20 +348,15 @@ mod tests fn preprocess_with_directives_works() { assert_eq!( - ShaderPreprocessor { - read_file: |_| { Ok(b"out vec4 FragColor;".to_vec()) }, - base_dir: PathBuf::new() - } - .preprocess( + preprocess( concat!( "#version 330 core\n", "\n", "#preinclude \"foo.glsl\"\n", "\n", "void main() {}", - ) - .to_string(), - Path::new("foo.glsl"), + ), + &|_| { Ok(b"out vec4 FragColor;".to_vec()) } ) .unwrap(), concat!( @@ -338,11 +369,7 @@ mod tests ); assert_eq!( - ShaderPreprocessor { - read_file: |_| { Ok(b"out vec4 FragColor;".to_vec()) }, - base_dir: PathBuf::new() - } - .preprocess( + preprocess( concat!( "#version 330 core\n", "\n", @@ -351,9 +378,8 @@ mod tests "in vec3 in_frag_color;\n", "\n", "void main() {}", - ) - .to_string(), - Path::new("foo.glsl"), + ), + &|_| { Ok(b"out vec4 FragColor;".to_vec()) } ) .unwrap(), concat!( @@ -368,8 +394,19 @@ mod tests ); assert_eq!( - ShaderPreprocessor { - read_file: |path| { + preprocess( + concat!( + "#version 330 core\n", + "\n", + "#preinclude \"bar.glsl\"\n", + "\n", + "in vec3 in_frag_color;\n", + "\n", + "#preinclude \"foo.glsl\"\n", + "\n", + "void main() {}", + ), + &|path| { if path == OsStr::new("bar.glsl") { Ok(b"out vec4 FragColor;".to_vec()) } else { @@ -381,22 +418,6 @@ mod tests .to_vec()) } }, - base_dir: PathBuf::new() - } - .preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "#preinclude \"foo.glsl\"\n", - "\n", - "void main() {}", - ) - .to_string(), - Path::new("foo.glsl"), ) .unwrap(), concat!( @@ -415,15 +436,9 @@ mod tests } #[test] - fn preprocess_invalid_shader_does_not_work() + fn preprocess_invalid_directive_does_not_work() { - let path = Path::new("foo.glsl"); - - let res = ShaderPreprocessor { - read_file: |_| Ok(b"out vec4 FragColor;".to_vec()), - base_dir: PathBuf::new(), - } - .preprocess( + let res = preprocess( concat!( "#version 330 core\n", "\n", @@ -431,12 +446,11 @@ mod tests "#preinclude foo.glsl\"\n", "\n", "void main() {}", - ) - .to_string(), - path, + ), + &|_| Ok(b"out vec4 FragColor;".to_vec()), ); - let Err(Error::InvalidToken { expected, found, span }) = res else { + let Err(PreprocessingError::InvalidToken { expected, found, span }) = res else { panic!( "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}" ); @@ -446,16 +460,25 @@ mod tests assert_eq!(found, 'f'); assert_eq!(span.line, 3); assert_eq!(span.column, 13); - assert_eq!(span.file, path); + assert_eq!(span.path, SpanPath::Original); } #[test] fn preprocess_error_has_correct_span() { - let path = Path::new("foo.glsl"); - - let res = ShaderPreprocessor { - read_file: |path| { + let res = preprocess( + concat!( + "#version 330 core\n", + "\n", + "#preinclude \"bar.glsl\"\n", + "\n", + "#preinclude \"foo.glsl\"\n", + "\n", + "in vec3 in_frag_color;\n", + "\n", + "void main() {}", + ), + &|path| { if path == OsStr::new("bar.glsl") { Ok(concat!( "out vec4 FragColor;\n", @@ -464,29 +487,25 @@ mod tests ) .as_bytes() .to_vec()) + } else if path == OsStr::new("foo.glsl") { + Ok(concat!( + "uniform sampler2D input_texture;\n", + "\n", + // Missing space before first " + "#preinclude\"shared_types.glsl\"\n", + ) + .as_bytes() + .to_vec()) } else { - Ok(b"uniform sampler2D input_texture;".to_vec()) + panic!(concat!( + "Expected read function to be called with ", + "either path bar.glsl or foo.glsl" + )); } }, - base_dir: PathBuf::new(), - } - .preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "#preinclude\"foo.glsl\"\n", - "\n", - "void main() {}", - ) - .to_string(), - Path::new("foo.glsl"), ); - let Err(Error::InvalidToken { expected, found, span }) = res else { + let Err(PreprocessingError::InvalidToken { expected, found, span }) = res else { panic!( "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}" ); @@ -494,17 +513,26 @@ mod tests assert_eq!(expected, ' '); assert_eq!(found, '"'); - assert_eq!(span.line, 7); + assert_eq!(span.line, 3); assert_eq!(span.column, 12); - assert_eq!(span.file, path); + assert_eq!(span.path, SpanPath::Path(Path::new("foo.glsl").into())); } #[test] fn preprocess_included_shader_with_include_works() { assert_eq!( - ShaderPreprocessor { - read_file: |path| { + preprocess( + concat!( + "#version 330 core\n", + "\n", + "#preinclude \"bar.glsl\"\n", + "\n", + "in vec3 in_frag_color;\n", + "\n", + "void main() {}", + ), + &|path| { if path == OsStr::new("bar.glsl") { Ok(concat!( "#preinclude \"foo.glsl\"\n", @@ -521,21 +549,7 @@ mod tests .as_bytes() .to_vec()) } - }, - base_dir: PathBuf::new() - } - .preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ) - .to_string(), - Path::new("foo.glsl"), + } ) .unwrap(), concat!( @@ -556,8 +570,17 @@ mod tests #[test] fn preprocess_included_shader_with_include_error_span_is_correct() { - let res = ShaderPreprocessor { - read_file: |path| { + let res = preprocess( + concat!( + "#version 330 core\n", + "\n", + "#preinclude \"bar.glsl\"\n", + "\n", + "in vec3 in_frag_color;\n", + "\n", + "void main() {}", + ), + &|path| { if path == OsStr::new("bar.glsl") { Ok(concat!( // ' instead of " @@ -576,23 +599,9 @@ mod tests .to_vec()) } }, - base_dir: PathBuf::new(), - } - .preprocess( - concat!( - "#version 330 core\n", - "\n", - "#preinclude \"bar.glsl\"\n", - "\n", - "in vec3 in_frag_color;\n", - "\n", - "void main() {}", - ) - .to_string(), - Path::new("foo.glsl"), ); - let Err(Error::InvalidToken { expected, found, span }) = res else { + let Err(PreprocessingError::InvalidToken { expected, found, span }) = res else { panic!( "Expected result to be Err(Error::InvalidToken {{ ... }}), is {res:?}" ); @@ -602,6 +611,6 @@ mod tests assert_eq!(found, '\''); assert_eq!(span.line, 1); assert_eq!(span.column, 13); - assert_eq!(span.file, Path::new("bar.glsl")); + assert_eq!(span.path, Path::new("bar.glsl")); } } diff --git a/engine/src/opengl/mod.rs b/engine/src/opengl/mod.rs index 0b1bb8a..53e0120 100644 --- a/engine/src/opengl/mod.rs +++ b/engine/src/opengl/mod.rs @@ -1,16 +1,17 @@ use bitflags::bitflags; +use gl::types::GLint; use crate::data_types::dimens::Dimens; use crate::vector::Vec2; pub mod buffer; +pub mod glsl; pub mod shader; pub mod texture; pub mod vertex_array; mod util; -#[cfg(feature = "debug")] pub mod debug; pub fn set_viewport(position: Vec2<u32>, size: Dimens<u32>) @@ -47,6 +48,17 @@ pub fn enable(capacity: Capability) } } +pub fn get_context_flags() -> ContextFlags +{ + let mut context_flags: GLint = 0; + + unsafe { + gl::GetIntegerv(gl::CONTEXT_FLAGS as u32, &mut context_flags); + } + + ContextFlags::from_bits_truncate(context_flags as u32) +} + bitflags! { #[derive(Debug, Clone, Copy)] pub struct BufferClearMask: u32 { @@ -105,3 +117,12 @@ impl From<crate::draw_flags::PolygonModeFace> for PolygonModeFace } } } + +bitflags! { +#[derive(Debug, Clone, Copy)] +pub struct ContextFlags: u32 { + const FORWARD_COMPATIBLE = gl::CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT; + const DEBUG = gl::CONTEXT_FLAG_DEBUG_BIT; + const ROBUST_ACCESS = gl::CONTEXT_FLAG_ROBUST_ACCESS_BIT; +} +} diff --git a/engine/src/opengl/shader.rs b/engine/src/opengl/shader.rs index 070897e..a626fc7 100644 --- a/engine/src/opengl/shader.rs +++ b/engine/src/opengl/shader.rs @@ -2,7 +2,6 @@ use std::ffi::CStr; use std::ptr::null_mut; use crate::matrix::Matrix; -use crate::shader::Kind; use crate::vector::Vec3; #[derive(Debug)] @@ -20,7 +19,7 @@ impl Shader Self { shader } } - pub fn set_source(&self, source: &str) -> Result<(), Error> + pub fn set_source(&mut self, source: &str) -> Result<(), Error> { if !source.is_ascii() { return Err(Error::SourceNotAscii); @@ -39,7 +38,7 @@ impl Shader Ok(()) } - pub fn compile(&self) -> Result<(), Error> + pub fn compile(&mut self) -> Result<(), Error> { unsafe { gl::CompileShader(self.shader); @@ -90,6 +89,14 @@ impl Drop for Shader } } +/// Shader kind. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Kind +{ + Vertex, + Fragment, +} + impl Kind { fn into_gl(self) -> gl::types::GLenum @@ -117,14 +124,14 @@ impl Program Self { program } } - pub fn attach(&self, shader: &Shader) + pub fn attach(&mut self, shader: &Shader) { unsafe { gl::AttachShader(self.program, shader.shader); } } - pub fn link(&self) -> Result<(), Error> + pub fn link(&mut self) -> Result<(), Error> { unsafe { gl::LinkProgram(self.program); @@ -152,82 +159,105 @@ impl Program } } - pub fn set_uniform_matrix_4fv(&mut self, name: &CStr, matrix: &Matrix<f32, 4, 4>) + pub fn set_uniform(&mut self, name: &CStr, var: &impl UniformVariable) { - let uniform_location = - unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) }; + let location = UniformLocation(unsafe { + gl::GetUniformLocation(self.program, name.as_ptr().cast()) + }); + + var.set(self, location); + } + + fn get_info_log(&self) -> String + { + let mut buf = vec![gl::types::GLchar::default(); 512]; unsafe { - gl::ProgramUniformMatrix4fv( + #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] + gl::GetProgramInfoLog( self.program, - uniform_location, - 1, - gl::FALSE, - matrix.as_ptr(), + buf.len() as gl::types::GLsizei, + null_mut(), + buf.as_mut_ptr(), ); } + + let info_log = unsafe { CStr::from_ptr(buf.as_ptr()) }; + + unsafe { String::from_utf8_unchecked(info_log.to_bytes().to_vec()) } } +} - pub fn set_uniform_vec_3fv(&mut self, name: &CStr, vec: &Vec3<f32>) +impl Drop for Program +{ + fn drop(&mut self) { - let uniform_location = - unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) }; - unsafe { - gl::ProgramUniform3fv(self.program, uniform_location, 1, vec.as_ptr()); + gl::DeleteProgram(self.program); } } +} - pub fn set_uniform_1fv(&mut self, name: &CStr, num: f32) - { - let uniform_location = - unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) }; +pub trait UniformVariable +{ + fn set(&self, program: &mut Program, uniform_location: UniformLocation); +} +impl UniformVariable for f32 +{ + fn set(&self, program: &mut Program, uniform_location: UniformLocation) + { unsafe { - gl::ProgramUniform1fv(self.program, uniform_location, 1, &num); + gl::ProgramUniform1f(program.program, uniform_location.0, *self); } } +} - pub fn set_uniform_1i(&mut self, name: &CStr, num: i32) +impl UniformVariable for i32 +{ + fn set(&self, program: &mut Program, uniform_location: UniformLocation) { - let uniform_location = - unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) }; - unsafe { - gl::ProgramUniform1i(self.program, uniform_location, num); + gl::ProgramUniform1i(program.program, uniform_location.0, *self); } } +} - fn get_info_log(&self) -> String +impl UniformVariable for Vec3<f32> +{ + fn set(&self, program: &mut Program, uniform_location: UniformLocation) { - let mut buf = vec![gl::types::GLchar::default(); 512]; - unsafe { - #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] - gl::GetProgramInfoLog( - self.program, - buf.len() as gl::types::GLsizei, - null_mut(), - buf.as_mut_ptr(), + gl::ProgramUniform3f( + program.program, + uniform_location.0, + self.x, + self.y, + self.z, ); } - - let info_log = unsafe { CStr::from_ptr(buf.as_ptr()) }; - - unsafe { String::from_utf8_unchecked(info_log.to_bytes().to_vec()) } } } -impl Drop for Program +impl UniformVariable for Matrix<f32, 4, 4> { - fn drop(&mut self) + fn set(&self, program: &mut Program, uniform_location: UniformLocation) { unsafe { - gl::DeleteProgram(self.program); + gl::ProgramUniformMatrix4fv( + program.program, + uniform_location.0, + 1, + gl::FALSE, + self.as_ptr(), + ); } } } +#[derive(Debug)] +pub struct UniformLocation(gl::types::GLint); + /// Shader error. #[derive(Debug, thiserror::Error)] pub enum Error diff --git a/engine/src/opengl/texture.rs b/engine/src/opengl/texture.rs index 074ade7..80a5f37 100644 --- a/engine/src/opengl/texture.rs +++ b/engine/src/opengl/texture.rs @@ -1,5 +1,4 @@ use crate::data_types::dimens::Dimens; -use crate::texture::{Id, Properties}; #[derive(Debug)] pub struct Texture @@ -20,10 +19,10 @@ impl Texture Self { texture } } - pub fn bind(&self) + pub fn bind_to_texture_unit(&self, texture_unit: u32) { unsafe { - gl::BindTexture(gl::TEXTURE_2D, self.texture); + gl::BindTextureUnit(texture_unit, self.texture); } } @@ -41,16 +40,9 @@ impl Texture } } - pub fn apply_properties(&mut self, properties: &Properties) - { - self.set_wrap(properties.wrap); - self.set_magnifying_filter(properties.magnifying_filter); - self.set_minifying_filter(properties.minifying_filter); - } - pub fn set_wrap(&mut self, wrapping: Wrapping) { - let wrapping_gl = wrapping.to_gl(); + let wrapping_gl = wrapping as gl::types::GLenum; #[allow(clippy::cast_possible_wrap)] unsafe { @@ -61,7 +53,7 @@ impl Texture pub fn set_magnifying_filter(&mut self, filtering: Filtering) { - let filtering_gl = filtering.to_gl(); + let filtering_gl = filtering as gl::types::GLenum; #[allow(clippy::cast_possible_wrap)] unsafe { @@ -75,7 +67,7 @@ impl Texture pub fn set_minifying_filter(&mut self, filtering: Filtering) { - let filtering_gl = filtering.to_gl(); + let filtering_gl = filtering as gl::types::GLenum; #[allow(clippy::cast_possible_wrap)] unsafe { @@ -132,43 +124,21 @@ impl Drop for Texture /// Texture wrapping. #[derive(Debug, Clone, Copy)] +#[repr(u32)] pub enum Wrapping { - Repeat, - MirroredRepeat, - ClampToEdge, - ClampToBorder, -} - -impl Wrapping -{ - fn to_gl(self) -> gl::types::GLenum - { - match self { - Self::Repeat => gl::REPEAT, - Self::MirroredRepeat => gl::MIRRORED_REPEAT, - Self::ClampToEdge => gl::CLAMP_TO_EDGE, - Self::ClampToBorder => gl::CLAMP_TO_BORDER, - } - } + Repeat = gl::REPEAT, + MirroredRepeat = gl::MIRRORED_REPEAT, + ClampToEdge = gl::CLAMP_TO_EDGE, + ClampToBorder = gl::CLAMP_TO_BORDER, } #[derive(Debug, Clone, Copy)] +#[repr(u32)] pub enum Filtering { - Nearest, - Linear, -} - -impl Filtering -{ - fn to_gl(self) -> gl::types::GLenum - { - match self { - Self::Linear => gl::LINEAR, - Self::Nearest => gl::NEAREST, - } - } + Nearest = gl::NEAREST, + Linear = gl::LINEAR, } /// Texture pixel data format. @@ -197,44 +167,3 @@ impl PixelDataFormat } } } - -pub fn set_active_texture_unit(texture_unit: TextureUnit) -{ - unsafe { - gl::ActiveTexture(texture_unit.into_gl()); - } -} - -macro_rules! texture_unit_enum { - (cnt=$cnt: literal) => { - seq_macro::seq!(N in 0..$cnt { - #[derive(Debug, Clone, Copy)] - pub enum TextureUnit { - #( - No~N, - )* - } - - impl TextureUnit { - fn into_gl(self) -> gl::types::GLenum { - match self { - #( - Self::No~N => gl::TEXTURE~N, - )* - } - } - - pub fn from_texture_id(texture_id: Id) -> Option<Self> { - match texture_id.into_inner() { - #( - N => Some(Self::No~N), - )* - _ => None - } - } - } - }); - }; -} - -texture_unit_enum!(cnt = 31); diff --git a/engine/src/opengl/vertex_array.rs b/engine/src/opengl/vertex_array.rs index da5d91e..1f8a870 100644 --- a/engine/src/opengl/vertex_array.rs +++ b/engine/src/opengl/vertex_array.rs @@ -1,10 +1,6 @@ use std::mem::size_of; use crate::opengl::buffer::Buffer; -use crate::vertex::Vertex; - -#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] -const VERTEX_STRIDE: i32 = size_of::<Vertex>() as i32; #[derive(Debug)] pub struct VertexArray @@ -59,10 +55,10 @@ impl VertexArray } } - pub fn bind_vertex_buffer( + pub fn bind_vertex_buffer<VertexT>( &mut self, binding_index: u32, - vertex_buffer: &Buffer<Vertex>, + vertex_buffer: &Buffer<VertexT>, offset: isize, ) { @@ -72,7 +68,7 @@ impl VertexArray binding_index, vertex_buffer.object(), offset, - VERTEX_STRIDE, + size_of::<VertexT>() as i32, ); } } @@ -125,27 +121,6 @@ impl VertexArray { unsafe { gl::BindVertexArray(self.array) } } - - /// Does a weak clone of this vertex array. The vertex array itself is NOT copied in - /// any way this function only copies the internal vertex array ID. - /// - /// # Safety - /// The returned `VertexArray` must not be dropped if another `VertexArray` - /// referencing the same vertex array ID is used later. - pub unsafe fn clone_unsafe(&self) -> Self - { - Self { array: self.array } - } -} - -impl Drop for VertexArray -{ - fn drop(&mut self) - { - unsafe { - gl::DeleteVertexArrays(1, &self.array); - } - } } #[derive(Debug)] diff --git a/engine/src/performance.rs b/engine/src/performance.rs deleted file mode 100644 index ffc5c27..0000000 --- a/engine/src/performance.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::time::Instant; - -use ecs::component::local::Local; -use ecs::system::{Into, System}; -use ecs::Component; - -use crate::event::PostPresent as PostPresentEvent; - -#[derive(Debug, Default)] -#[non_exhaustive] -pub struct Extension {} - -impl ecs::extension::Extension for Extension -{ - fn collect(self, mut collector: ecs::extension::Collector<'_>) - { - collector.add_system( - PostPresentEvent, - log_perf.into_system().initialize((State::default(),)), - ); - } -} - -#[cfg(feature = "debug")] -macro_rules! log_perf { - ($($tt: tt)*) => { - tracing::info!($($tt)*); - }; -} - -#[cfg(not(feature = "debug"))] -macro_rules! log_perf { - ($($tt: tt)*) => { - println!($($tt)*); - }; -} - -fn log_perf(mut state: Local<State>) -{ - let Some(last_time) = state.last_time else { - state.last_time = Some(Instant::now()); - return; - }; - - let time_now = Instant::now(); - - state.last_time = Some(time_now); - - log_perf!( - "Frame time: {}us", - time_now.duration_since(last_time).as_micros() - ); -} - -#[derive(Debug, Default, Component)] -struct State -{ - last_time: Option<Instant>, -} diff --git a/engine/src/projection.rs b/engine/src/projection.rs index aa84a9f..115ca39 100644 --- a/engine/src/projection.rs +++ b/engine/src/projection.rs @@ -1,10 +1,14 @@ +use crate::data_types::dimens::Dimens3; use crate::matrix::Matrix; +use crate::builder; +use crate::vector::Vec3; #[derive(Debug)] #[non_exhaustive] pub enum Projection { Perspective(Perspective), + Orthographic(Orthographic), } /// Perspective projection parameters. @@ -16,6 +20,29 @@ pub struct Perspective pub near: f32, } +impl Perspective +{ + /// Creates a perspective projection matrix using right-handed coordinates. + #[inline] + pub fn to_matrix_rh(&self, aspect: f32, clip_volume: ClipVolume) + -> Matrix<f32, 4, 4> + { + let mut out = Matrix::new(); + + match clip_volume { + ClipVolume::NegOneToOne => { + out.set_cell(0, 0, (1.0 / (self.fov_radians / 2.0).tan()) / aspect); + out.set_cell(1, 1, 1.0 / (self.fov_radians / 2.0).tan()); + out.set_cell(2, 2, (self.near + self.far) / (self.near - self.far)); + out.set_cell(2, 3, (2.0 * self.near * self.far) / (self.near - self.far)); + out.set_cell(3, 2, -1.0); + } + } + + out + } +} + impl Default for Perspective { fn default() -> Self @@ -28,30 +55,83 @@ impl Default for Perspective } } -pub(crate) fn new_perspective_matrix( - perspective: &Perspective, - aspect: f32, -) -> Matrix<f32, 4, 4> +builder! { +#[builder(name = OrthographicBuilder, derives=(Debug, Clone))] +#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[non_exhaustive] +pub struct Orthographic { - let mut out = Matrix::new(); + pub size: Dimens3<f32>, +} +} - out.set_cell(0, 0, (1.0 / (perspective.fov_radians / 2.0).tan()) / aspect); +impl Orthographic +{ + pub fn builder() -> OrthographicBuilder + { + OrthographicBuilder::default() + } - out.set_cell(1, 1, 1.0 / (perspective.fov_radians / 2.0).tan()); + /// Creates a orthographic projection matrix using right-handed coordinates. + pub fn to_matrix_rh( + &self, + center_pos: &Vec3<f32>, + clip_volume: ClipVolume, + ) -> Matrix<f32, 4, 4> + { + let mut result = Matrix::<f32, 4, 4>::new(); - out.set_cell( - 2, - 2, - (perspective.near + perspective.far) / (perspective.near - perspective.far), - ); + let left = center_pos.x - (self.size.width / 2.0); + let right = center_pos.x + (self.size.width / 2.0); + let bottom = center_pos.y - (self.size.height / 2.0); + let top = center_pos.y + (self.size.height / 2.0); + let near = center_pos.z - (self.size.depth / 2.0); + let far = center_pos.z + (self.size.depth / 2.0); - out.set_cell( - 2, - 3, - (2.0 * perspective.near * perspective.far) / (perspective.near - perspective.far), - ); + match clip_volume { + ClipVolume::NegOneToOne => { + result.set_cell(0, 0, 2.0 / (right - left)); + result.set_cell(1, 1, 2.0 / (top - bottom)); + result.set_cell(2, 2, -2.0 / (far - near)); + result.set_cell(0, 3, -(right + left) / (right - left)); + result.set_cell(1, 3, -(top + bottom) / (top - bottom)); + result.set_cell(2, 3, -(far + near) / (far - near)); + result.set_cell(3, 3, 1.0); + } + } - out.set_cell(3, 2, -1.0); + result + } +} - out +impl Default for Orthographic +{ + fn default() -> Self + { + Self { + size: Dimens3 { + width: 10.0, + height: 7.0, + depth: 10.0, + }, + } + } +} + +impl Default for OrthographicBuilder +{ + fn default() -> Self + { + let orthographic = Orthographic::default(); + + OrthographicBuilder { size: orthographic.size } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum ClipVolume +{ + /// -1 to +1. This is the OpenGL clip volume definition. + NegOneToOne, } diff --git a/engine/src/renderer.rs b/engine/src/renderer.rs index 2544919..17bc925 100644 --- a/engine/src/renderer.rs +++ b/engine/src/renderer.rs @@ -1 +1,7 @@ +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::{Phase, UPDATE as UPDATE_PHASE}; +use ecs::static_entity; + pub mod opengl; + +static_entity!(pub RENDER_PHASE, (Phase, Pair::new::<ChildOf>(*UPDATE_PHASE))); diff --git a/engine/src/renderer/opengl.rs b/engine/src/renderer/opengl.rs index a353c6a..cfd046f 100644 --- a/engine/src/renderer/opengl.rs +++ b/engine/src/renderer/opengl.rs @@ -2,75 +2,109 @@ use std::collections::HashMap; use std::ffi::{c_void, CString}; -use std::ops::Deref; +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; +use std::path::Path; use std::process::abort; use ecs::actions::Actions; use ecs::component::local::Local; -use ecs::query::options::{Not, With}; +use ecs::component::Handle as ComponentHandle; +use ecs::phase::START as START_PHASE; +use ecs::query::term::Without; use ecs::sole::Single; use ecs::system::{Into as _, System}; use ecs::{Component, Query}; +use crate::asset::{Assets, Id as AssetId}; use crate::camera::{Active as ActiveCamera, Camera}; use crate::color::Color; use crate::data_types::dimens::Dimens; use crate::draw_flags::{DrawFlags, NoDraw, PolygonModeConfig}; -use crate::event::{Present as PresentEvent, Start as StartEvent}; +use crate::image::{ColorType as ImageColorType, Image}; use crate::lighting::{DirectionalLight, GlobalLight, PointLight}; use crate::material::{Flags as MaterialFlags, Material}; use crate::matrix::Matrix; use crate::mesh::Mesh; +use crate::model::Model; use crate::opengl::buffer::{Buffer, Usage as BufferUsage}; -#[cfg(feature = "debug")] -use crate::opengl::debug::{MessageSeverity, MessageSource, MessageType}; +use crate::opengl::debug::{ + enable_debug_output, + set_debug_message_callback, + set_debug_message_control, + MessageIdsAction, + MessageSeverity, + MessageSource, + MessageType, +}; +use crate::opengl::glsl::{ + preprocess as glsl_preprocess, + PreprocessingError as GlslPreprocessingError, +}; use crate::opengl::shader::{ Error as GlShaderError, + Kind as ShaderKind, Program as GlShaderProgram, Shader as GlShader, }; use crate::opengl::texture::{ - set_active_texture_unit, + Filtering as GlTextureFiltering, + PixelDataFormat as GlTexturePixelDataFormat, Texture as GlTexture, - TextureUnit, + Wrapping as GlTextureWrapping, }; use crate::opengl::vertex_array::{ DataType as VertexArrayDataType, PrimitiveKind, VertexArray, }; -use crate::opengl::{clear_buffers, enable, BufferClearMask, Capability}; -use crate::projection::{new_perspective_matrix, Projection}; -use crate::shader::Program as ShaderProgram; -use crate::texture::{Id as TextureId, Texture}; -use crate::transform::{Position, Scale}; -use crate::util::NeverDrop; +use crate::opengl::{ + clear_buffers, + enable, + get_context_flags as get_opengl_context_flags, + BufferClearMask, + Capability, + ContextFlags, +}; +use crate::projection::{ClipVolume, Projection}; +use crate::renderer::opengl::vertex::{AttributeComponentType, Vertex}; +use crate::renderer::RENDER_PHASE; +use crate::texture::{ + Filtering as TextureFiltering, + Properties as TextureProperties, + Wrapping as TextureWrapping, +}; +use crate::transform::{Scale, WorldPosition}; +use crate::util::{defer, Defer, RefOrValue}; use crate::vector::{Vec2, Vec3}; -use crate::vertex::{AttributeComponentType, Vertex}; use crate::window::Window; -type RenderableEntity = ( - Mesh, - ShaderProgram, - Material, - Option<MaterialFlags>, - Option<Position>, - Option<Scale>, - Option<DrawFlags>, - Option<GlObjects>, +mod vertex; + +const AMBIENT_MAP_TEXTURE_UNIT: u32 = 0; +const DIFFUSE_MAP_TEXTURE_UNIT: u32 = 1; +const SPECULAR_MAP_TEXTURE_UNIT: u32 = 2; + +type RenderableEntity<'a> = ( + &'a Model, + Option<&'a MaterialFlags>, + Option<&'a WorldPosition>, + Option<&'a Scale>, + Option<&'a DrawFlags>, + Option<&'a GlObjects>, ); #[derive(Debug, Default)] +#[non_exhaustive] pub struct Extension {} impl ecs::extension::Extension for Extension { fn collect(self, mut collector: ecs::extension::Collector<'_>) { - collector.add_system(StartEvent, initialize); + collector.add_system(*START_PHASE, initialize); collector.add_system( - PresentEvent, + *RENDER_PHASE, render .into_system() .initialize((GlobalGlObjects::default(),)), @@ -95,8 +129,9 @@ fn initialize(window: Single<Window>) } }); - #[cfg(feature = "debug")] - initialize_debug(); + if get_opengl_context_flags().contains(ContextFlags::DEBUG) { + initialize_debug(); + } let window_size = window.size().expect("Failed to get window size"); @@ -110,78 +145,63 @@ fn initialize(window: Single<Window>) enable(Capability::MultiSample); } +#[tracing::instrument(skip_all)] #[allow(clippy::too_many_arguments)] fn render( - query: Query<RenderableEntity, Not<With<NoDraw>>>, - point_light_query: Query<(PointLight,)>, - directional_lights: Query<(DirectionalLight,)>, - camera_query: Query<(Camera, Position, ActiveCamera)>, + query: Query<RenderableEntity<'_>, (Without<NoDraw>,)>, + point_light_query: Query<(&PointLight, &WorldPosition)>, + directional_lights: Query<(&DirectionalLight,)>, + camera_query: Query<(&Camera, &WorldPosition, &ActiveCamera)>, window: Single<Window>, global_light: Single<GlobalLight>, + assets: Single<Assets>, mut gl_objects: Local<GlobalGlObjects>, mut actions: Actions, ) { - let Some((camera, camera_pos, _)) = camera_query.iter().next() else { - #[cfg(feature = "debug")] + let Some((camera, camera_world_pos, _)) = camera_query.iter().next() else { tracing::warn!("No current camera. Nothing will be rendered"); return; }; - let point_lights = point_light_query - .iter() - .map(|(point_light,)| point_light) - .collect::<Vec<_>>(); - let directional_lights = directional_lights.iter().collect::<Vec<_>>(); let GlobalGlObjects { - shader_programs: gl_shader_programs, + shader_program, textures: gl_textures, + default_1x1_texture: default_1x1_gl_texture, } = &mut *gl_objects; + let shader_program = + shader_program.get_or_insert_with(|| create_default_shader_program().unwrap()); + clear_buffers(BufferClearMask::COLOR | BufferClearMask::DEPTH); - for ( - entity_index, - ( - mesh, - shader_program, - material, - material_flags, - position, - scale, - draw_flags, - gl_objects, - ), - ) in query.iter().enumerate() + 'subject_loop: for ( + euid, + (model, material_flags, position, scale, draw_flags, gl_objects), + ) in query.iter_with_euids() { + let Some(model_data) = assets.get(&model.asset_handle) else { + tracing::trace!("Missing model asset"); + continue; + }; + let material_flags = material_flags .map(|material_flags| material_flags.clone()) .unwrap_or_default(); - let shader_program = gl_shader_programs - .entry(shader_program.u64_hash()) - .or_insert_with(|| create_gl_shader_program(&shader_program).unwrap()); - - let new_gl_objects; - - let gl_objects = if let Some(gl_objects) = gl_objects.as_deref() { - gl_objects - } else { - // TODO: Account for when meshes are changed - let gl_objects = GlObjects::new(&mesh); - - new_gl_objects = Some(gl_objects.clone()); - - actions.add_components( - query.get_entity_uid(entity_index).unwrap(), - (gl_objects,), - ); - - &*new_gl_objects.unwrap() + let gl_objs = match gl_objects.as_deref() { + Some(gl_objs) => RefOrValue::Ref(gl_objs), + None => RefOrValue::Value(Some(GlObjects::new(&model_data.mesh))), }; + defer!(|gl_objs| { + if let RefOrValue::Value(opt_gl_objs) = gl_objs { + actions.add_components(euid, (opt_gl_objs.take().unwrap(),)); + }; + }); + apply_transformation_matrices( Transformation { position: position.map(|pos| *pos).unwrap_or_default().position, @@ -189,37 +209,82 @@ fn render( }, shader_program, &camera, - &camera_pos, + &camera_world_pos, window.size().expect("Failed to get window size"), ); + if model_data.materials.len() > 1 { + tracing::warn!(concat!( + "Multiple model materials are not supported ", + "so only the first material will be used" + )); + } + + let material = match model_data.materials.values().next() { + Some(material) => material, + None => { + tracing::warn!("Model has no materials. Using default material"); + + &Material::default() + } + }; + apply_light( &material, &material_flags, &global_light, shader_program, - point_lights.as_slice(), + (point_light_query.iter(), point_light_query.iter().count()), directional_lights .iter() .map(|(dir_light,)| &**dir_light) .collect::<Vec<_>>() .as_slice(), - &camera_pos, + &camera_world_pos, ); - for texture in &material.textures { - let gl_texture = gl_textures - .entry(texture.id()) - .or_insert_with(|| create_gl_texture(texture)); - - let texture_unit = - TextureUnit::from_texture_id(texture.id()).unwrap_or_else(|| { - panic!("Texture id {} is a invalid texture unit", texture.id()); + let material_texture_maps = [ + (&material.ambient_map, AMBIENT_MAP_TEXTURE_UNIT), + (&material.diffuse_map, DIFFUSE_MAP_TEXTURE_UNIT), + (&material.specular_map, SPECULAR_MAP_TEXTURE_UNIT), + ]; + + for (texture, texture_unit) in material_texture_maps { + let Some(texture) = texture else { + let gl_texture = default_1x1_gl_texture.get_or_insert_with(|| { + create_gl_texture( + &Image::from_color(1, Color::WHITE_U8), + &TextureProperties::default(), + ) }); - set_active_texture_unit(texture_unit); + gl_texture.bind_to_texture_unit(texture_unit); - gl_texture.bind(); + continue; + }; + + let texture_image_asset_id = texture.asset_handle.id(); + + let gl_texture = match gl_textures.get(&texture_image_asset_id) { + Some(gl_texture) => gl_texture, + None => { + let Some(image) = assets.get::<Image>(&texture.asset_handle) else { + tracing::trace!("Missing texture asset"); + continue 'subject_loop; + }; + + gl_textures.insert( + texture_image_asset_id, + create_gl_texture(image, &texture.properties), + ); + + gl_textures + .get(&texture.asset_handle.id()) + .expect("Not possible") + } + }; + + gl_texture.bind_to_texture_unit(texture_unit); } shader_program.activate(); @@ -231,7 +296,7 @@ fn render( ); } - draw_mesh(gl_objects); + draw_mesh(gl_objs.get().unwrap()); if draw_flags.is_some() { let default_polygon_mode_config = PolygonModeConfig::default(); @@ -247,8 +312,9 @@ fn render( #[derive(Debug, Default, Component)] struct GlobalGlObjects { - shader_programs: HashMap<u64, GlShaderProgram>, - textures: HashMap<TextureId, GlTexture>, + shader_program: Option<GlShaderProgram>, + textures: HashMap<AssetId, GlTexture>, + default_1x1_texture: Option<GlTexture>, } fn set_viewport(position: Vec2<u32>, size: Dimens<u32>) @@ -256,20 +322,11 @@ fn set_viewport(position: Vec2<u32>, size: Dimens<u32>) crate::opengl::set_viewport(position, size); } -#[cfg(feature = "debug")] fn initialize_debug() { - use crate::opengl::debug::{ - enable_debug_output, - set_debug_message_callback, - set_debug_message_control, - MessageIdsAction, - }; - enable_debug_output(); set_debug_message_callback(opengl_debug_message_cb); - set_debug_message_control(None, None, None, &[], MessageIdsAction::Disable); } @@ -278,72 +335,119 @@ fn draw_mesh(gl_objects: &GlObjects) gl_objects.vertex_arr.bind(); if gl_objects.index_buffer.is_some() { - VertexArray::draw_elements(PrimitiveKind::Triangles, 0, gl_objects.index_cnt); + VertexArray::draw_elements(PrimitiveKind::Triangles, 0, gl_objects.element_cnt); } else { - VertexArray::draw_arrays(PrimitiveKind::Triangles, 0, 3); + VertexArray::draw_arrays(PrimitiveKind::Triangles, 0, gl_objects.element_cnt); } } -fn create_gl_texture(texture: &Texture) -> GlTexture +fn create_gl_texture(image: &Image, texture_properties: &TextureProperties) -> GlTexture { let mut gl_texture = GlTexture::new(); gl_texture.generate( - *texture.dimensions(), - texture.image().as_bytes(), - texture.pixel_data_format(), + image.dimensions(), + image.as_bytes(), + match image.color_type() { + ImageColorType::Rgb8 => GlTexturePixelDataFormat::Rgb8, + ImageColorType::Rgba8 => GlTexturePixelDataFormat::Rgba8, + _ => { + unimplemented!(); + } + }, ); - gl_texture.apply_properties(texture.properties()); + gl_texture.set_wrap(texture_wrapping_to_gl(texture_properties.wrap)); + + gl_texture.set_magnifying_filter(texture_filtering_to_gl( + texture_properties.magnifying_filter, + )); + + gl_texture.set_minifying_filter(texture_filtering_to_gl( + texture_properties.minifying_filter, + )); gl_texture } -fn create_gl_shader_program( - shader_program: &ShaderProgram, -) -> Result<GlShaderProgram, GlShaderError> +const VERTEX_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/vertex.glsl"); +const FRAGMENT_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/fragment.glsl"); + +const VERTEX_DATA_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/vertex_data.glsl"); +const LIGHT_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/light.glsl"); + +fn create_default_shader_program() -> Result<GlShaderProgram, CreateShaderError> { - let gl_shaders = shader_program - .shaders() - .iter() - .map(|shader| { - let gl_shader = GlShader::new(shader.kind()); + let mut vertex_shader = GlShader::new(ShaderKind::Vertex); - gl_shader.set_source(shader.source())?; - gl_shader.compile()?; + vertex_shader.set_source(&*glsl_preprocess( + VERTEX_GLSL_SHADER_SRC, + &get_glsl_shader_content, + )?)?; - Ok(gl_shader) - }) - .collect::<Result<Vec<_>, _>>()?; + vertex_shader.compile()?; - let gl_shader_program = GlShaderProgram::new(); + let mut fragment_shader = GlShader::new(ShaderKind::Fragment); - for gl_shader in &gl_shaders { - gl_shader_program.attach(gl_shader); - } + fragment_shader.set_source(&*glsl_preprocess( + FRAGMENT_GLSL_SHADER_SRC, + &get_glsl_shader_content, + )?)?; + + fragment_shader.compile()?; + + let mut gl_shader_program = GlShaderProgram::new(); + + gl_shader_program.attach(&vertex_shader); + gl_shader_program.attach(&fragment_shader); gl_shader_program.link()?; Ok(gl_shader_program) } +#[derive(Debug, thiserror::Error)] +enum CreateShaderError +{ + #[error(transparent)] + ShaderError(#[from] GlShaderError), + + #[error(transparent)] + PreprocessingError(#[from] GlslPreprocessingError), +} + +fn get_glsl_shader_content(path: &Path) -> Result<Vec<u8>, std::io::Error> +{ + if path == Path::new("vertex_data.glsl") { + return Ok(VERTEX_DATA_GLSL_SHADER_SRC.as_bytes().to_vec()); + } + + if path == Path::new("light.glsl") { + return Ok(LIGHT_GLSL_SHADER_SRC.as_bytes().to_vec()); + } + + Err(IoError::new( + IoErrorKind::NotFound, + format!("Content for shader file {} not found", path.display()), + )) +} + #[derive(Debug, Component)] struct GlObjects { /// Vertex and index buffer has to live as long as the vertex array - vertex_buffer: Buffer<Vertex>, + _vertex_buffer: Buffer<Vertex>, index_buffer: Option<Buffer<u32>>, - index_cnt: u32, + element_cnt: u32, vertex_arr: VertexArray, } impl GlObjects { - #[cfg_attr(feature = "debug", tracing::instrument(skip_all))] + #[tracing::instrument(skip_all)] fn new(mesh: &Mesh) -> Self { - #[cfg(feature = "debug")] tracing::trace!( "Creating vertex array, vertex buffer{}", if mesh.indices().is_some() { @@ -356,7 +460,13 @@ impl GlObjects let mut vertex_arr = VertexArray::new(); let mut vertex_buffer = Buffer::new(); - vertex_buffer.store(mesh.vertices(), BufferUsage::Static); + vertex_buffer.store_mapped(mesh.vertices(), BufferUsage::Static, |vertex| { + Vertex { + pos: vertex.pos, + texture_coords: vertex.texture_coords, + normal: vertex.normal, + } + }); vertex_arr.bind_vertex_buffer(0, &vertex_buffer, 0); @@ -387,78 +497,77 @@ impl GlObjects vertex_arr.bind_element_buffer(&index_buffer); return Self { - vertex_buffer, + _vertex_buffer: vertex_buffer, index_buffer: Some(index_buffer), - index_cnt: indices.len().try_into().unwrap(), + element_cnt: indices + .len() + .try_into() + .expect("Mesh index count does not fit into a 32-bit unsigned int"), vertex_arr, }; } Self { - vertex_buffer, + _vertex_buffer: vertex_buffer, index_buffer: None, - index_cnt: 0, + element_cnt: mesh + .vertices() + .len() + .try_into() + .expect("Mesh vertex count does not fit into a 32-bit unsigned int"), vertex_arr, } } - - pub fn clone(&self) -> NeverDrop<Self> - { - NeverDrop::new(Self { - // SAFETY: The vertex buffer will never become dropped (NeverDrop ensures it) - vertex_buffer: unsafe { self.vertex_buffer.clone_weak() }, - index_buffer: self - .index_buffer - .as_ref() - // SAFETY: The index buffer will never become dropped (NeverDrop ensures - // it) - .map(|index_buffer| unsafe { index_buffer.clone_weak() }), - index_cnt: self.index_cnt, - // SAFETY: The vertex array will never become dropped (NeverDrop ensures it) - vertex_arr: unsafe { self.vertex_arr.clone_unsafe() }, - }) - } } fn apply_transformation_matrices( transformation: Transformation, gl_shader_program: &mut GlShaderProgram, camera: &Camera, - camera_pos: &Position, + camera_world_pos: &WorldPosition, window_size: Dimens<u32>, ) { gl_shader_program - .set_uniform_matrix_4fv(c"model", &create_transformation_matrix(transformation)); + .set_uniform(c"model", &create_transformation_matrix(transformation)); - let view = create_view(camera, camera_pos); + let view_matrix = create_view_matrix(camera, &camera_world_pos.position); - gl_shader_program.set_uniform_matrix_4fv(c"view", &view); + gl_shader_program.set_uniform(c"view", &view_matrix); #[allow(clippy::cast_precision_loss)] - let projection = match &camera.projection { - Projection::Perspective(perspective) => new_perspective_matrix( - perspective, + let proj_matrix = match &camera.projection { + Projection::Perspective(perspective_proj) => perspective_proj.to_matrix_rh( window_size.width as f32 / window_size.height as f32, + ClipVolume::NegOneToOne, ), + Projection::Orthographic(orthographic_proj) => orthographic_proj + .to_matrix_rh(&camera_world_pos.position, ClipVolume::NegOneToOne), }; - gl_shader_program.set_uniform_matrix_4fv(c"projection", &projection); + gl_shader_program.set_uniform(c"projection", &proj_matrix); } -fn apply_light<PointLightHolder>( +fn apply_light<'point_light>( material: &Material, material_flags: &MaterialFlags, global_light: &GlobalLight, gl_shader_program: &mut GlShaderProgram, - point_lights: &[PointLightHolder], + (point_light_iter, point_light_cnt): ( + impl Iterator< + Item = ( + ComponentHandle<'point_light, PointLight>, + ComponentHandle<'point_light, WorldPosition>, + ), + >, + usize, + ), directional_lights: &[&DirectionalLight], - camera_pos: &Position, -) where - PointLightHolder: Deref<Target = PointLight>, + camera_world_pos: &WorldPosition, +) { debug_assert!( - point_lights.len() < 64, + point_light_cnt < 64, "Shader cannot handle more than 64 point lights" ); @@ -468,7 +577,7 @@ fn apply_light<PointLightHolder>( ); for (dir_light_index, dir_light) in directional_lights.iter().enumerate() { - gl_shader_program.set_uniform_vec_3fv( + gl_shader_program.set_uniform( &create_light_uniform_name( "directional_lights", dir_light_index, @@ -488,71 +597,68 @@ fn apply_light<PointLightHolder>( // There probably won't be more than 2147483648 directional lights #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] gl_shader_program - .set_uniform_1i(c"directional_light_cnt", directional_lights.len() as i32); + .set_uniform(c"directional_light_cnt", &(directional_lights.len() as i32)); - for (point_light_index, point_light) in point_lights.iter().enumerate() { - gl_shader_program.set_uniform_vec_3fv( + for (point_light_index, (point_light, point_light_world_pos)) in + point_light_iter.enumerate() + { + gl_shader_program.set_uniform( &create_light_uniform_name("point_lights", point_light_index, "position"), - &point_light.position, + &(point_light_world_pos.position + point_light.local_position), ); set_light_phong_uniforms( gl_shader_program, "point_lights", point_light_index, - &**point_light, + &*point_light, ); set_light_attenuation_uniforms( gl_shader_program, "point_lights", point_light_index, - point_light, + &*point_light, ); } // There probably won't be more than 2147483648 point lights #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] - gl_shader_program.set_uniform_1i(c"point_light_cnt", point_lights.len() as i32); + gl_shader_program.set_uniform(c"point_light_cnt", &(point_light_cnt as i32)); - gl_shader_program.set_uniform_vec_3fv( + gl_shader_program.set_uniform( c"material.ambient", - &if material_flags.use_ambient_color { + &Vec3::from(if material_flags.use_ambient_color { material.ambient.clone() } else { global_light.ambient.clone() - } - .into(), + }), ); gl_shader_program - .set_uniform_vec_3fv(c"material.diffuse", &material.diffuse.clone().into()); + .set_uniform(c"material.diffuse", &Vec3::from(material.diffuse.clone())); #[allow(clippy::cast_possible_wrap)] gl_shader_program - .set_uniform_vec_3fv(c"material.specular", &material.specular.clone().into()); + .set_uniform(c"material.specular", &Vec3::from(material.specular.clone())); #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform_1i( - c"material.ambient_map", - material.ambient_map.into_inner() as i32, - ); + gl_shader_program + .set_uniform(c"material.ambient_map", &(AMBIENT_MAP_TEXTURE_UNIT as i32)); #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform_1i( - c"material.diffuse_map", - material.diffuse_map.into_inner() as i32, - ); + gl_shader_program + .set_uniform(c"material.diffuse_map", &(DIFFUSE_MAP_TEXTURE_UNIT as i32)); #[allow(clippy::cast_possible_wrap)] - gl_shader_program.set_uniform_1i( + gl_shader_program.set_uniform( c"material.specular_map", - material.specular_map.into_inner() as i32, + &(SPECULAR_MAP_TEXTURE_UNIT as i32), ); - gl_shader_program.set_uniform_1fv(c"material.shininess", material.shininess); + gl_shader_program.set_uniform(c"material.shininess", &material.shininess); - gl_shader_program.set_uniform_vec_3fv(c"view_pos", &camera_pos.position); + gl_shader_program.set_uniform(c"view_pos", &camera_world_pos.position); } fn set_light_attenuation_uniforms( @@ -562,27 +668,27 @@ fn set_light_attenuation_uniforms( light: &PointLight, ) { - gl_shader_program.set_uniform_1fv( + gl_shader_program.set_uniform( &create_light_uniform_name( light_array, light_index, "attenuation_props.constant", ), - light.attenuation_params.constant, + &light.attenuation_params.constant, ); - gl_shader_program.set_uniform_1fv( + gl_shader_program.set_uniform( &create_light_uniform_name(light_array, light_index, "attenuation_props.linear"), - light.attenuation_params.linear, + &light.attenuation_params.linear, ); - gl_shader_program.set_uniform_1fv( + gl_shader_program.set_uniform( &create_light_uniform_name( light_array, light_index, "attenuation_props.quadratic", ), - light.attenuation_params.quadratic, + &light.attenuation_params.quadratic, ); } @@ -593,14 +699,14 @@ fn set_light_phong_uniforms( light: &impl Light, ) { - gl_shader_program.set_uniform_vec_3fv( + gl_shader_program.set_uniform( &create_light_uniform_name(light_array, light_index, "phong.diffuse"), - &light.diffuse().clone().into(), + &Vec3::from(light.diffuse().clone()), ); - gl_shader_program.set_uniform_vec_3fv( + gl_shader_program.set_uniform( &create_light_uniform_name(light_array, light_index, "phong.specular"), - &light.specular().clone().into(), + &Vec3::from(light.specular().clone()), ); } @@ -649,16 +755,15 @@ fn create_light_uniform_name( } } -fn create_view(camera: &Camera, camera_pos: &Position) -> Matrix<f32, 4, 4> +fn create_view_matrix(camera: &Camera, camera_pos: &Vec3<f32>) -> Matrix<f32, 4, 4> { let mut view = Matrix::new(); - view.look_at(&camera_pos.position, &camera.target, &camera.global_up); + view.look_at(&camera_pos, &camera.target, &camera.global_up); view } -#[cfg(feature = "debug")] #[tracing::instrument(skip_all)] fn opengl_debug_message_cb( source: MessageSource, @@ -717,3 +822,23 @@ fn create_transformation_matrix(transformation: Transformation) -> Matrix<f32, 4 matrix } + +#[inline] +fn texture_wrapping_to_gl(texture_wrapping: TextureWrapping) -> GlTextureWrapping +{ + match texture_wrapping { + TextureWrapping::Repeat => GlTextureWrapping::Repeat, + TextureWrapping::MirroredRepeat => GlTextureWrapping::MirroredRepeat, + TextureWrapping::ClampToEdge => GlTextureWrapping::ClampToEdge, + TextureWrapping::ClampToBorder => GlTextureWrapping::ClampToBorder, + } +} + +#[inline] +fn texture_filtering_to_gl(texture_filtering: TextureFiltering) -> GlTextureFiltering +{ + match texture_filtering { + TextureFiltering::Linear => GlTextureFiltering::Linear, + TextureFiltering::Nearest => GlTextureFiltering::Nearest, + } +} diff --git a/engine/fragment.glsl b/engine/src/renderer/opengl/glsl/fragment.glsl index 5bf5ff2..5bf5ff2 100644 --- a/engine/fragment.glsl +++ b/engine/src/renderer/opengl/glsl/fragment.glsl diff --git a/engine/light.glsl b/engine/src/renderer/opengl/glsl/light.glsl index 1bc23a4..f12b5fe 100644 --- a/engine/light.glsl +++ b/engine/src/renderer/opengl/glsl/light.glsl @@ -80,10 +80,10 @@ vec3 calc_specular_light( { vec3 view_direction = normalize(view_pos - frag_pos); - vec3 reflect_direction = reflect(-light_dir, norm); + vec3 halfway_direction = normalize(light_dir + view_direction); float spec = - pow(max(dot(view_direction, reflect_direction), 0.0), material.shininess); + pow(max(dot(norm, halfway_direction), 0.0), material.shininess); return light_phong.specular * ( spec * (vec3(texture(material.specular_map, texture_coords)) * material.specular) diff --git a/engine/vertex.glsl b/engine/src/renderer/opengl/glsl/vertex.glsl index b57caa6..b57caa6 100644 --- a/engine/vertex.glsl +++ b/engine/src/renderer/opengl/glsl/vertex.glsl diff --git a/engine/vertex_data.glsl b/engine/src/renderer/opengl/glsl/vertex_data.glsl index 486d445..486d445 100644 --- a/engine/vertex_data.glsl +++ b/engine/src/renderer/opengl/glsl/vertex_data.glsl diff --git a/engine/src/vertex.rs b/engine/src/renderer/opengl/vertex.rs index 897ee97..499b94b 100644 --- a/engine/src/vertex.rs +++ b/engine/src/renderer/opengl/vertex.rs @@ -1,23 +1,17 @@ -use std::mem::size_of; - -use crate::util::builder; use crate::vector::{Vec2, Vec3}; -builder! { -#[builder(name = Builder, derives = (Debug, Default))] -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] #[repr(C)] pub struct Vertex { - pos: Vec3<f32>, - texture_coords: Vec2<f32>, - normal: Vec3<f32>, -} + pub pos: Vec3<f32>, + pub texture_coords: Vec2<f32>, + pub normal: Vec3<f32>, } impl Vertex { - pub(crate) fn attrs() -> &'static [Attribute] + pub fn attrs() -> &'static [Attribute] { #[allow(clippy::cast_possible_truncation)] &[ @@ -43,15 +37,17 @@ impl Vertex } } -pub(crate) struct Attribute +#[derive(Debug)] +pub struct Attribute { - pub(crate) index: u32, - pub(crate) component_type: AttributeComponentType, - pub(crate) component_cnt: AttributeComponentCnt, - pub(crate) component_size: u32, + pub index: u32, + pub component_type: AttributeComponentType, + pub component_cnt: AttributeComponentCnt, + pub component_size: u32, } -pub(crate) enum AttributeComponentType +#[derive(Debug)] +pub enum AttributeComponentType { Float, } @@ -59,7 +55,7 @@ pub(crate) enum AttributeComponentType #[derive(Debug, Clone, Copy)] #[repr(u32)] #[allow(dead_code)] -pub(crate) enum AttributeComponentCnt +pub enum AttributeComponentCnt { One = 1, Two = 2, diff --git a/engine/src/shader.rs b/engine/src/shader.rs deleted file mode 100644 index 89f7b7c..0000000 --- a/engine/src/shader.rs +++ /dev/null @@ -1,186 +0,0 @@ -use std::collections::hash_map::DefaultHasher; -use std::fs::read_to_string; -use std::hash::{Hash, Hasher}; -use std::path::{Path, PathBuf}; - -use ecs::Component; - -use crate::shader_preprocessor::{Error as ShaderPreprocessorError, ShaderPreprocessor}; - -const VERTEX_SHADER_FILE: &str = "vertex.glsl"; -const FRAGMENT_SHADER_FILE: &str = "fragment.glsl"; - -const SHADER_DIR: &str = "engine"; - -/// Shader program -#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Component)] -pub struct Program -{ - shaders: Vec<Shader>, -} - -impl Program -{ - /// Creates a new shader program with the default shaders. - /// - /// # Errors - /// Returns `Err` if: - /// - Reading a default shader file Fails - /// - Preprocessing a shader fails. - pub fn new() -> Result<Self, Error> - { - let mut program = Self { shaders: Vec::new() }; - - program.push_shader( - Shader::read_shader_file( - Kind::Vertex, - &Path::new(SHADER_DIR).join(VERTEX_SHADER_FILE), - )? - .preprocess()?, - ); - - program.push_shader( - Shader::read_shader_file( - Kind::Fragment, - &Path::new(SHADER_DIR).join(FRAGMENT_SHADER_FILE), - )? - .preprocess()?, - ); - - Ok(program) - } - - pub fn push_shader(&mut self, shader: Shader) - { - self.shaders.push(shader); - } - - pub fn append_shaders(&mut self, shaders: impl IntoIterator<Item = Shader>) - { - self.shaders.extend(shaders); - } - - #[must_use] - pub fn shaders(&self) -> &[Shader] - { - &self.shaders - } - - pub(crate) fn u64_hash(&self) -> u64 - { - let mut hasher = DefaultHasher::new(); - - self.hash(&mut hasher); - - hasher.finish() - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub struct Shader -{ - kind: Kind, - source: String, - file: PathBuf, -} - -impl Shader -{ - /// Reads a shader from the specified source file. - /// - /// # Errors - /// Will return `Err` if: - /// - Reading the file fails - /// - The shader source is not ASCII - pub fn read_shader_file(kind: Kind, shader_file: &Path) -> Result<Self, Error> - { - let source = read_to_string(shader_file).map_err(|err| Error::ReadFailed { - source: err, - shader_file: shader_file.to_path_buf(), - })?; - - if !source.is_ascii() { - return Err(Error::SourceNotAscii); - } - - Ok(Self { - kind, - source, - file: shader_file.to_path_buf(), - }) - } - - /// Preprocesses the shaders. - /// - /// # Errors - /// Returns `Err` if preprocessing fails. - pub fn preprocess(self) -> Result<Self, Error> - { - let shader_preprocessor = ShaderPreprocessor::new( - self.file - .parent() - .ok_or(Error::SourcePathHasNoParent)? - .to_path_buf(), - ); - - let source_preprocessed = shader_preprocessor - .preprocess(self.source, &self.file) - .map_err(|err| Error::PreprocessFailed { - source: err, - shader_file: self.file.clone(), - })?; - - Ok(Self { - kind: self.kind, - source: source_preprocessed, - file: self.file.clone(), - }) - } - - #[must_use] - pub fn kind(&self) -> Kind - { - self.kind - } - - #[must_use] - pub fn source(&self) -> &str - { - &self.source - } -} - -/// Shader kind. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Kind -{ - Vertex, - Fragment, -} - -/// Shader error -#[derive(Debug, thiserror::Error)] -pub enum Error -{ - #[error("Failed to read shader {}", shader_file.display())] - ReadFailed - { - #[source] - source: std::io::Error, - shader_file: PathBuf, - }, - - #[error("Shader source is not ASCII")] - SourceNotAscii, - - #[error("Failed to preprocess shader {}", shader_file.display())] - PreprocessFailed - { - #[source] - source: ShaderPreprocessorError, - shader_file: PathBuf, - }, - - #[error("Shader source path has no parent")] - SourcePathHasNoParent, -} diff --git a/engine/src/texture.rs b/engine/src/texture.rs index f82b59d..d02b9ff 100644 --- a/engine/src/texture.rs +++ b/engine/src/texture.rs @@ -1,152 +1,37 @@ -use std::fmt::Display; -use std::path::Path; -use std::sync::atomic::{AtomicU32, Ordering}; - -use image::io::Reader as ImageReader; -use image::{DynamicImage, ImageError, Rgb, RgbImage}; - -use crate::color::Color; -use crate::data_types::dimens::Dimens; -use crate::opengl::texture::PixelDataFormat; - -static NEXT_ID: AtomicU32 = AtomicU32::new(0); - -mod reexports -{ - pub use crate::opengl::texture::{Filtering, Wrapping}; -} - -pub use reexports::*; +use crate::asset::Handle as AssetHandle; +use crate::image::Image; +use crate::builder; #[derive(Debug, Clone)] +#[non_exhaustive] pub struct Texture { - id: Id, - image: DynamicImage, - pixel_data_format: PixelDataFormat, - dimensions: Dimens<u32>, - properties: Properties, + pub asset_handle: AssetHandle<Image>, + pub properties: Properties, } impl Texture { - /// Opens a texture image. - /// - /// # Errors - /// Will return `Err` if: - /// - Opening the image fails - /// - The image data is not 8-bit/color RGB - #[allow(clippy::new_without_default)] - pub fn open(path: &Path) -> Result<Self, Error> - { - let image = ImageReader::open(path) - .map_err(Error::OpenImageFailed)? - .decode() - .map_err(Error::DecodeImageFailed)?; - - let pixel_data_format = match &image { - DynamicImage::ImageRgb8(_) => PixelDataFormat::Rgb8, - DynamicImage::ImageRgba8(_) => PixelDataFormat::Rgba8, - _ => { - return Err(Error::UnsupportedImageDataKind); - } - }; - - let dimensions = Dimens { - width: image.width(), - height: image.height(), - }; - - let id = NEXT_ID.fetch_add(1, Ordering::Relaxed); - - Ok(Self { - id: Id::new(id), - image, - pixel_data_format, - dimensions, - properties: Properties::default(), - }) - } - - #[must_use] - pub fn new_from_color(dimensions: &Dimens<u32>, color: &Color<u8>) -> Self + pub fn new(asset_handle: AssetHandle<Image>) -> Self { - let image = RgbImage::from_pixel( - dimensions.width, - dimensions.height, - Rgb([color.red, color.green, color.blue]), - ); - - let id = NEXT_ID.fetch_add(1, Ordering::Relaxed); - Self { - id: Id::new(id), - image: image.into(), - pixel_data_format: PixelDataFormat::Rgb8, - dimensions: *dimensions, + asset_handle, properties: Properties::default(), } } - #[must_use] - pub fn id(&self) -> Id - { - self.id - } - - #[must_use] - pub fn properties(&self) -> &Properties - { - &self.properties - } - - pub fn properties_mut(&mut self) -> &mut Properties - { - &mut self.properties - } - - #[must_use] - pub fn dimensions(&self) -> &Dimens<u32> - { - &self.dimensions - } - - #[must_use] - pub fn pixel_data_format(&self) -> PixelDataFormat - { - self.pixel_data_format - } - - #[must_use] - pub fn image(&self) -> &DynamicImage - { - &self.image - } -} - -impl Drop for Texture -{ - fn drop(&mut self) + pub fn with_properties( + asset_handle: AssetHandle<Image>, + properties: Properties, + ) -> Self { - NEXT_ID.fetch_sub(1, Ordering::Relaxed); + Self { asset_handle, properties } } } -/// Texture error. -#[derive(Debug, thiserror::Error)] -pub enum Error -{ - #[error("Failed to open texture image")] - OpenImageFailed(#[source] std::io::Error), - - #[error("Failed to decode texture image")] - DecodeImageFailed(#[source] ImageError), - - #[error("Unsupported image data kind")] - UnsupportedImageDataKind, -} - +builder! { /// Texture properties +#[builder(name = PropertiesBuilder, derives=(Debug, Clone))] #[derive(Debug, Clone)] #[non_exhaustive] pub struct Properties @@ -155,6 +40,15 @@ pub struct Properties pub magnifying_filter: Filtering, pub minifying_filter: Filtering, } +} + +impl Properties +{ + pub fn builder() -> PropertiesBuilder + { + PropertiesBuilder::default() + } +} impl Default for Properties { @@ -168,30 +62,29 @@ impl Default for Properties } } -/// Texture ID. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Id -{ - id: u32, -} - -impl Id +impl Default for PropertiesBuilder { - fn new(id: u32) -> Self + fn default() -> Self { - Self { id } + Properties::default().into() } +} - pub(crate) fn into_inner(self) -> u32 - { - self.id - } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum Filtering +{ + Nearest, + Linear, } -impl Display for Id +/// Texture wrapping. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum Wrapping { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result - { - self.id.fmt(formatter) - } + Repeat, + MirroredRepeat, + ClampToEdge, + ClampToBorder, } diff --git a/engine/src/transform.rs b/engine/src/transform.rs index 5e5e296..7c0c941 100644 --- a/engine/src/transform.rs +++ b/engine/src/transform.rs @@ -2,14 +2,14 @@ use ecs::Component; use crate::vector::Vec3; -/// A position in 3D space. +/// A position in world space. #[derive(Debug, Default, Clone, Copy, Component)] -pub struct Position +pub struct WorldPosition { pub position: Vec3<f32>, } -impl From<Vec3<f32>> for Position +impl From<Vec3<f32>> for WorldPosition { fn from(position: Vec3<f32>) -> Self { diff --git a/engine/src/util.rs b/engine/src/util.rs index a505a38..cc4677d 100644 --- a/engine/src/util.rs +++ b/engine/src/util.rs @@ -1,3 +1,5 @@ +use std::marker::PhantomData; + macro_rules! try_option { ($expr: expr) => { match $expr { @@ -9,9 +11,6 @@ macro_rules! try_option { }; } -use std::mem::ManuallyDrop; -use std::ops::{Deref, DerefMut}; - pub(crate) use try_option; macro_rules! or { @@ -26,6 +25,18 @@ macro_rules! or { pub(crate) use or; +#[macro_export] +macro_rules! expand_map_opt { + ($in: tt, no_occurance=($($no_occurance: tt)*), occurance=($($occurance: tt)*)) => { + $($occurance)* + }; + + (, no_occurance=($($no_occurance: tt)*), occurance=($($occurance: tt)*)) => { + $($no_occurance)* + }; +} + +#[macro_export] macro_rules! builder { ( $(#[doc = $doc: literal])* @@ -37,7 +48,8 @@ macro_rules! builder { $visibility: vis struct $name: ident { $( - $(#[$field_attr: meta])* + $(#[doc = $field_doc: literal])* + $(#[builder(skip_generate_fn$($field_skip_generate_fn: tt)?)])? $field_visibility: vis $field: ident: $field_type: ty, )* } @@ -47,7 +59,7 @@ macro_rules! builder { $visibility struct $name { $( - $(#[$field_attr])* + $(#[doc = $field_doc])* $field_visibility $field: $field_type, )* } @@ -63,12 +75,18 @@ macro_rules! builder { impl $builder_name { $( - #[must_use] - $visibility fn $field(mut self, $field: $field_type) -> Self - { - self.$field = $field; - self - } + $crate::expand_map_opt!( + $(true $($field_skip_generate_fn)?)?, + no_occurance=( + #[must_use] + $visibility fn $field(mut self, $field: $field_type) -> Self + { + self.$field = $field; + self + } + ), + occurance=() + ); )* #[must_use] @@ -83,6 +101,7 @@ macro_rules! builder { impl From<$name> for $builder_name { + #[allow(unused_variables)] fn from(built: $name) -> Self { Self { @@ -95,38 +114,74 @@ macro_rules! builder { }; } -pub(crate) use builder; - -/// Wrapper that ensures the contained value will never be dropped. -#[derive(Debug)] -pub struct NeverDrop<Value> +pub enum RefOrValue<'a, T> { - value: ManuallyDrop<Value>, + Ref(&'a T), + Value(Option<T>), } -impl<Value> NeverDrop<Value> +impl<'a, T> RefOrValue<'a, T> { - #[must_use] - pub fn new(value: Value) -> Self + pub fn get(&self) -> Option<&T> { - Self { value: ManuallyDrop::new(value) } + match self { + Self::Ref(val_ref) => Some(val_ref), + Self::Value(val_cell) => val_cell.as_ref(), + } } } -impl<Value> Deref for NeverDrop<Value> +#[derive(Debug)] +pub struct Defer<'func, Func, Data> +where + Func: FnMut(&mut Data) + 'func, { - type Target = Value; + func: Func, + pub data: Data, + _pd: PhantomData<&'func ()>, +} - fn deref(&self) -> &Self::Target +impl<'func, Func, Data> Defer<'func, Func, Data> +where + Func: FnMut(&mut Data) + 'func, +{ + pub fn new(data: Data, func: Func) -> Self { - &self.value + Self { func, data, _pd: PhantomData } } } -impl<Value> DerefMut for NeverDrop<Value> +impl<'func, Func, Data> Drop for Defer<'func, Func, Data> +where + Func: FnMut(&mut Data) + 'func, { - fn deref_mut(&mut self) -> &mut Self::Target + fn drop(&mut self) { - &mut self.value + (self.func)(&mut self.data) } } + +/// Defines a function that will be called at the end of the current scope. +/// +/// Only captured variables that are later mutably borrowed needs to specified as +/// captures. +macro_rules! defer { + (|$capture: ident| {$($tt: tt)*}) => { + // This uses the automatic temporary lifetime extension behaviour introduced + // in Rust 1.79.0 (https://blog.rust-lang.org/2024/06/13/Rust-1.79.0.html) to + // create a unnamable variable for the Defer struct. The variable should be + // unnamable so that it cannot be missused and so that this macro can be used + // multiple times without having to give it a identifier for the Defer struct + // variable + let Defer { data: $capture, .. } = if true { + &Defer::new($capture, |$capture| { + $($tt)* + }) + } + else { + unreachable!(); + }; + }; +} + +pub(crate) use defer; diff --git a/engine/src/window.rs b/engine/src/window.rs index ccc1b8d..d342341 100644 --- a/engine/src/window.rs +++ b/engine/src/window.rs @@ -1,30 +1,25 @@ use std::borrow::Cow; use std::ffi::{CStr, CString}; +use bitflags::bitflags; use ecs::actions::Actions; use ecs::extension::Collector as ExtensionCollector; +use ecs::pair::{ChildOf, Pair}; +use ecs::phase::{Phase, START as START_PHASE}; use ecs::sole::Single; -use ecs::Sole; +use ecs::{static_entity, Sole}; +use glfw::window::{Hint as WindowCreationHint, HintValue as WindowCreationHintValue}; use glfw::WindowSize; +use util_macros::VariantArr; use crate::data_types::dimens::Dimens; -use crate::event::{Conclude as ConcludeEvent, Start as StartEvent}; +use crate::renderer::RENDER_PHASE; use crate::vector::Vec2; -mod reexports -{ - pub use glfw::window::{ - CursorMode, - Hint as CreationHint, - HintValue as CreationHintValue, - InputMode, - Key, - KeyModifiers, - KeyState, - }; -} - -pub use reexports::*; +static_entity!( + pub UPDATE_PHASE, + (Phase, Pair::new::<ChildOf>(*RENDER_PHASE)) +); #[derive(Debug, Sole)] /// Has to be dropped last since it holds the OpenGL context. @@ -53,7 +48,9 @@ impl Window enabled: bool, ) -> Result<(), Error> { - Ok(self.inner.set_input_mode(input_mode, enabled)?) + Ok(self + .inner + .set_input_mode(input_mode.to_glfw_input_mode(), enabled)?) } /// Sets the cursor mode. @@ -62,7 +59,9 @@ impl Window /// If a platform error occurs. pub fn set_cursor_mode(&self, cursor_mode: CursorMode) -> Result<(), Error> { - Ok(self.inner.set_cursor_mode(cursor_mode)?) + Ok(self + .inner + .set_cursor_mode(cursor_mode.to_glfw_cursor_mode())?) } /// Returns whether or not the window should close. Will return true when the user has @@ -155,7 +154,19 @@ impl Window callback: impl Fn(Key, i32, KeyState, KeyModifiers) + 'static, ) { - self.inner.set_key_callback(callback); + self.inner + .set_key_callback(move |key, scancode, key_state, key_modifiers| { + let Some(key_state) = KeyState::from_glfw_key_state(key_state) else { + return; + }; + + callback( + Key::from_glfw_key(key), + scancode, + key_state, + KeyModifiers::from_bits_truncate(key_modifiers.bits()), + ) + }); } /// Sets the window's cursor position callback. @@ -165,6 +176,24 @@ impl Window .set_cursor_pos_callback(move |pos| callback(Vec2 { x: pos.x, y: pos.y })); } + /// Sets the window's mouse button callback. The given function is called when a mouse + /// button enters a new state. + pub fn set_mouse_button_callback( + &self, + callback: impl Fn(MouseButton, MouseButtonState, KeyModifiers) + 'static, + ) + { + self.inner.set_mouse_button_callback( + move |mouse_button, mouse_button_state, key_modifiers| { + callback( + MouseButton::from_glfw_mouse_button(mouse_button), + MouseButtonState::from_glfw_mouse_button_state(mouse_button_state), + KeyModifiers::from_bits_truncate(key_modifiers.bits()), + ) + }, + ); + } + /// Sets the window's close callback. pub fn set_close_callback(&self, callback: impl Fn() + 'static) { @@ -188,10 +217,26 @@ pub struct Builder impl Builder { - #[must_use] - pub fn creation_hint(mut self, hint: CreationHint, value: CreationHintValue) -> Self + /// Sets whether the OpenGL context should be created in debug mode, which may + /// provide additional error and diagnostic reporting functionality. + pub fn opengl_debug_context(mut self, enabled: bool) -> Self + { + self.inner = self.inner.hint( + WindowCreationHint::OpenGLDebugContext, + WindowCreationHintValue::Bool(enabled), + ); + + self + } + + /// Set the desired number of samples to use for multisampling. Zero disables + /// multisampling. + pub fn multisampling_sample_count(mut self, sample_count: u16) -> Self { - self.inner = self.inner.hint(hint, value); + self.inner = self.inner.hint( + WindowCreationHint::Samples, + WindowCreationHintValue::Number(sample_count as i32), + ); self } @@ -204,8 +249,8 @@ impl Builder pub fn create(&self, size: Dimens<u32>, title: &str) -> Result<Window, Error> { let builder = self.inner.clone().hint( - CreationHint::OpenGLDebugContext, - CreationHintValue::Bool(cfg!(feature = "debug")), + WindowCreationHint::OpenGLDebugContext, + WindowCreationHintValue::Bool(true), ); let window = builder.create( @@ -220,6 +265,396 @@ impl Builder } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, VariantArr)] +#[variant_arr(name = KEYS)] +pub enum Key +{ + Space, + Apostrophe, + Comma, + Minus, + Period, + Slash, + Digit0, + Digit1, + Digit2, + Digit3, + Digit4, + Digit5, + Digit6, + Digit7, + Digit8, + Digit9, + Semicolon, + Equal, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T, + U, + V, + W, + X, + Y, + Z, + LeftBracket, + Backslash, + RightBracket, + GraveAccent, + World1, + World2, + Escape, + Enter, + Tab, + Backspace, + Insert, + Delete, + Right, + Left, + Down, + Up, + PageUp, + PageDown, + Home, + End, + CapsLock, + ScrollLock, + NumLock, + PrintScreen, + Pause, + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + F13, + F14, + F15, + F16, + F17, + F18, + F19, + F20, + F21, + F22, + F23, + F24, + F25, + Kp0, + Kp1, + Kp2, + Kp3, + Kp4, + Kp5, + Kp6, + Kp7, + Kp8, + Kp9, + KpDecimal, + KpDivide, + KpMultiply, + KpSubtract, + KpAdd, + KpEnter, + KpEqual, + LeftShift, + LeftControl, + LeftAlt, + LeftSuper, + RightShift, + RightControl, + RightAlt, + RightSuper, + Menu, +} + +impl Key +{ + fn from_glfw_key(glfw_key: glfw::window::Key) -> Self + { + match glfw_key { + glfw::window::Key::Space => Self::Space, + glfw::window::Key::Apostrophe => Self::Apostrophe, + glfw::window::Key::Comma => Self::Comma, + glfw::window::Key::Minus => Self::Minus, + glfw::window::Key::Period => Self::Period, + glfw::window::Key::Slash => Self::Slash, + glfw::window::Key::Digit0 => Self::Digit0, + glfw::window::Key::Digit1 => Self::Digit1, + glfw::window::Key::Digit2 => Self::Digit2, + glfw::window::Key::Digit3 => Self::Digit3, + glfw::window::Key::Digit4 => Self::Digit4, + glfw::window::Key::Digit5 => Self::Digit5, + glfw::window::Key::Digit6 => Self::Digit6, + glfw::window::Key::Digit7 => Self::Digit7, + glfw::window::Key::Digit8 => Self::Digit8, + glfw::window::Key::Digit9 => Self::Digit9, + glfw::window::Key::Semicolon => Self::Semicolon, + glfw::window::Key::Equal => Self::Equal, + glfw::window::Key::A => Self::A, + glfw::window::Key::B => Self::B, + glfw::window::Key::C => Self::C, + glfw::window::Key::D => Self::D, + glfw::window::Key::E => Self::E, + glfw::window::Key::F => Self::F, + glfw::window::Key::G => Self::G, + glfw::window::Key::H => Self::H, + glfw::window::Key::I => Self::I, + glfw::window::Key::J => Self::J, + glfw::window::Key::K => Self::K, + glfw::window::Key::L => Self::L, + glfw::window::Key::M => Self::M, + glfw::window::Key::N => Self::N, + glfw::window::Key::O => Self::O, + glfw::window::Key::P => Self::P, + glfw::window::Key::Q => Self::Q, + glfw::window::Key::R => Self::R, + glfw::window::Key::S => Self::S, + glfw::window::Key::T => Self::T, + glfw::window::Key::U => Self::U, + glfw::window::Key::V => Self::V, + glfw::window::Key::W => Self::W, + glfw::window::Key::X => Self::X, + glfw::window::Key::Y => Self::Y, + glfw::window::Key::Z => Self::Z, + glfw::window::Key::LeftBracket => Self::LeftBracket, + glfw::window::Key::Backslash => Self::Backslash, + glfw::window::Key::RightBracket => Self::RightBracket, + glfw::window::Key::GraveAccent => Self::GraveAccent, + glfw::window::Key::World1 => Self::World1, + glfw::window::Key::World2 => Self::World2, + glfw::window::Key::Escape => Self::Escape, + glfw::window::Key::Enter => Self::Enter, + glfw::window::Key::Tab => Self::Tab, + glfw::window::Key::Backspace => Self::Backspace, + glfw::window::Key::Insert => Self::Insert, + glfw::window::Key::Delete => Self::Delete, + glfw::window::Key::Right => Self::Right, + glfw::window::Key::Left => Self::Left, + glfw::window::Key::Down => Self::Down, + glfw::window::Key::Up => Self::Up, + glfw::window::Key::PageUp => Self::PageUp, + glfw::window::Key::PageDown => Self::PageDown, + glfw::window::Key::Home => Self::Home, + glfw::window::Key::End => Self::End, + glfw::window::Key::CapsLock => Self::CapsLock, + glfw::window::Key::ScrollLock => Self::ScrollLock, + glfw::window::Key::NumLock => Self::NumLock, + glfw::window::Key::PrintScreen => Self::PrintScreen, + glfw::window::Key::Pause => Self::Pause, + glfw::window::Key::F1 => Self::F1, + glfw::window::Key::F2 => Self::F2, + glfw::window::Key::F3 => Self::F3, + glfw::window::Key::F4 => Self::F4, + glfw::window::Key::F5 => Self::F5, + glfw::window::Key::F6 => Self::F6, + glfw::window::Key::F7 => Self::F7, + glfw::window::Key::F8 => Self::F8, + glfw::window::Key::F9 => Self::F9, + glfw::window::Key::F10 => Self::F10, + glfw::window::Key::F11 => Self::F11, + glfw::window::Key::F12 => Self::F12, + glfw::window::Key::F13 => Self::F13, + glfw::window::Key::F14 => Self::F14, + glfw::window::Key::F15 => Self::F15, + glfw::window::Key::F16 => Self::F16, + glfw::window::Key::F17 => Self::F17, + glfw::window::Key::F18 => Self::F18, + glfw::window::Key::F19 => Self::F19, + glfw::window::Key::F20 => Self::F20, + glfw::window::Key::F21 => Self::F21, + glfw::window::Key::F22 => Self::F22, + glfw::window::Key::F23 => Self::F23, + glfw::window::Key::F24 => Self::F24, + glfw::window::Key::F25 => Self::F25, + glfw::window::Key::Kp0 => Self::Kp0, + glfw::window::Key::Kp1 => Self::Kp1, + glfw::window::Key::Kp2 => Self::Kp2, + glfw::window::Key::Kp3 => Self::Kp3, + glfw::window::Key::Kp4 => Self::Kp4, + glfw::window::Key::Kp5 => Self::Kp5, + glfw::window::Key::Kp6 => Self::Kp6, + glfw::window::Key::Kp7 => Self::Kp7, + glfw::window::Key::Kp8 => Self::Kp8, + glfw::window::Key::Kp9 => Self::Kp9, + glfw::window::Key::KpDecimal => Self::KpDecimal, + glfw::window::Key::KpDivide => Self::KpDivide, + glfw::window::Key::KpMultiply => Self::KpMultiply, + glfw::window::Key::KpSubtract => Self::KpSubtract, + glfw::window::Key::KpAdd => Self::KpAdd, + glfw::window::Key::KpEnter => Self::KpEnter, + glfw::window::Key::KpEqual => Self::KpEqual, + glfw::window::Key::LeftShift => Self::LeftShift, + glfw::window::Key::LeftControl => Self::LeftControl, + glfw::window::Key::LeftAlt => Self::LeftAlt, + glfw::window::Key::LeftSuper => Self::LeftSuper, + glfw::window::Key::RightShift => Self::RightShift, + glfw::window::Key::RightControl => Self::RightControl, + glfw::window::Key::RightAlt => Self::RightAlt, + glfw::window::Key::RightSuper => Self::RightSuper, + glfw::window::Key::Menu => Self::Menu, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum KeyState +{ + Pressed, + Released, +} + +impl KeyState +{ + fn from_glfw_key_state(glfw_key_state: glfw::window::KeyState) -> Option<Self> + { + match glfw_key_state { + glfw::window::KeyState::Pressed => Some(Self::Pressed), + glfw::window::KeyState::Released => Some(Self::Released), + glfw::window::KeyState::Repeat => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum MouseButton +{ + One, + Two, + Three, + Four, + Five, + Six, + Seven, + Eight, +} + +impl MouseButton +{ + pub const LEFT: Self = Self::One; + pub const MIDDLE: Self = Self::Three; + pub const RIGHT: Self = Self::Two; + + fn from_glfw_mouse_button(mouse_button: glfw::window::MouseButton) -> Self + { + match mouse_button { + glfw::window::MouseButton::One => Self::One, + glfw::window::MouseButton::Two => Self::Two, + glfw::window::MouseButton::Three => Self::Three, + glfw::window::MouseButton::Four => Self::Four, + glfw::window::MouseButton::Five => Self::Five, + glfw::window::MouseButton::Six => Self::Six, + glfw::window::MouseButton::Seven => Self::Seven, + glfw::window::MouseButton::Eight => Self::Eight, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum MouseButtonState +{ + Pressed, + Released, +} + +impl MouseButtonState +{ + fn from_glfw_mouse_button_state( + mouse_button_state: glfw::window::MouseButtonState, + ) -> Self + { + match mouse_button_state { + glfw::window::MouseButtonState::Pressed => Self::Pressed, + glfw::window::MouseButtonState::Released => Self::Released, + } + } +} + +bitflags! { + #[derive(Debug, Clone, Copy)] + pub struct KeyModifiers: i32 { + const SHIFT = glfw::window::KeyModifiers::SHIFT.bits(); + const CONTROL = glfw::window::KeyModifiers::CONTROL.bits(); + const ALT = glfw::window::KeyModifiers::ALT.bits(); + const SUPER = glfw::window::KeyModifiers::SUPER.bits(); + const CAPS_LOCK = glfw::window::KeyModifiers::CAPS_LOCK.bits(); + const NUM_LOCK = glfw::window::KeyModifiers::NUM_LOCK.bits(); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CursorMode +{ + /// Hides and grabs the cursor, providing virtual and unlimited cursor movement. + Disabled, + + /// Makes the cursor invisible when it is over the content area of the window but + /// does not restrict the cursor from leaving. + Hidden, + + /// Makes the cursor visible and behaving normally. + Normal, +} + +impl CursorMode +{ + fn to_glfw_cursor_mode(self) -> glfw::window::CursorMode + { + match self { + Self::Disabled => glfw::window::CursorMode::Disabled, + Self::Hidden => glfw::window::CursorMode::Hidden, + Self::Normal => glfw::window::CursorMode::Normal, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum InputMode +{ + /// When the cursor is disabled, raw (unscaled and unaccelerated) mouse motion can be + /// enabled if available. + /// + /// Raw mouse motion is closer to the actual motion of the mouse across a surface. It + /// is not affected by the scaling and acceleration applied to the motion of the + /// desktop cursor. That processing is suitable for a cursor while raw motion is + /// better for controlling for example a 3D camera. Because of this, raw mouse motion + /// is only provided when the cursor is disabled. + RawMouseMotion, +} + +impl InputMode +{ + fn to_glfw_input_mode(self) -> glfw::window::InputMode + { + match self { + Self::RawMouseMotion => glfw::window::InputMode::RawMouseMotion, + } + } +} + #[derive(Debug)] pub struct Extension { @@ -257,8 +692,8 @@ impl ecs::extension::Extension for Extension { fn collect(self, mut collector: ExtensionCollector<'_>) { - collector.add_system(StartEvent, initialize); - collector.add_system(ConcludeEvent, update); + collector.add_system(*START_PHASE, initialize); + collector.add_system(*UPDATE_PHASE, update); let window = self .window_builder diff --git a/engine/src/work_queue.rs b/engine/src/work_queue.rs new file mode 100644 index 0000000..7226c7d --- /dev/null +++ b/engine/src/work_queue.rs @@ -0,0 +1,44 @@ +use std::marker::PhantomData; +use std::sync::mpsc::{channel as mpsc_channel, Sender as MpscSender}; +use std::thread::JoinHandle as ThreadHandle; + +pub struct Work<UserData: Send + Sync + 'static> +{ + pub func: fn(UserData), + pub user_data: UserData, +} + +#[derive(Debug)] +pub struct WorkQueue<UserData: Send + Sync + 'static> +{ + work_sender: MpscSender<Work<UserData>>, + _thread: ThreadHandle<()>, + _pd: PhantomData<UserData>, +} + +impl<UserData: Send + Sync + 'static> WorkQueue<UserData> +{ + pub fn new() -> Self + { + let (work_sender, work_receiver) = mpsc_channel::<Work<UserData>>(); + + Self { + work_sender, + _thread: std::thread::spawn(move || { + let work_receiver = work_receiver; + + while let Ok(work) = work_receiver.recv() { + (work.func)(work.user_data); + } + }), + _pd: PhantomData, + } + } + + pub fn add_work(&self, work: Work<UserData>) + { + if self.work_sender.send(work).is_err() { + tracing::error!("Cannot add work to work queue. Work queue thread is dead"); + } + } +} diff --git a/glfw/src/window.rs b/glfw/src/window.rs index 5c30c16..1e7e777 100644 --- a/glfw/src/window.rs +++ b/glfw/src/window.rs @@ -180,8 +180,6 @@ impl Window } /// Sets the mouse button callback. - /// - /// The callback is called when a mouse button is pressed. pub fn set_mouse_button_callback( &self, callback: impl Fn(MouseButton, MouseButtonState, KeyModifiers) + 'static, diff --git a/organize_todo.py b/organize_todo.py new file mode 100644 index 0000000..95460ea --- /dev/null +++ b/organize_todo.py @@ -0,0 +1,57 @@ +from enum import Enum +from typing import List + + +TODO_FILE_NAME = "TODO.md" + +TODO_LINE_START_COMPLETED = "- [x] " +TODO_LINE_START_UNCOMPLETED = "- [ ] " + + +class PrevItemState(Enum): + COMPLETED = 0 + UNCOMPLETED = 1 + UNDETERMINED = 2 + + +def main(): + with open(TODO_FILE_NAME) as todo_file: + todo_lines = todo_file.readlines() + + completed: List[str] = [] + uncompleted: List[str] = [] + + prev_item_state = PrevItemState.UNDETERMINED + + for [index, line] in enumerate(todo_lines): + if len(line) == 0: + continue + + if line.startswith(TODO_LINE_START_COMPLETED): + completed.append(line) + prev_item_state = PrevItemState.COMPLETED + elif line.startswith(TODO_LINE_START_UNCOMPLETED): + uncompleted.append(line) + prev_item_state = PrevItemState.UNCOMPLETED + elif line[0].isspace(): + if prev_item_state == PrevItemState.COMPLETED: + completed.append(line) + elif prev_item_state == PrevItemState.UNCOMPLETED: + uncompleted.append(line) + elif prev_item_state == PrevItemState.UNDETERMINED: + print( + f"Error: todo line {index + 1} starts with whitespace but there is " + "no previous item" + ) + exit(1) + else: + print(f"Error: todo line {index + 1} does not have correct syntax") + exit(1) + + with open(TODO_FILE_NAME, "w") as todo_file: + todo_file.writelines(uncompleted) + todo_file.writelines(completed) + + +if __name__ == "__main__": + main() diff --git a/res/cube.obj b/res/cube.obj deleted file mode 100644 index 59c3fd8..0000000 --- a/res/cube.obj +++ /dev/null @@ -1,43 +0,0 @@ -o Cube -v 1.000000 1.000000 -1.000000 -v 1.000000 -1.000000 -1.000000 -v 1.000000 1.000000 1.000000 -v 1.000000 -1.000000 1.000000 -v -1.000000 1.000000 -1.000000 -v -1.000000 -1.000000 -1.000000 -v -1.000000 1.000000 1.000000 -v -1.000000 -1.000000 1.000000 -vn -0.0000 1.0000 -0.0000 -vn -0.0000 -0.0000 1.0000 -vn -1.0000 -0.0000 -0.0000 -vn -0.0000 -1.0000 -0.0000 -vn 1.0000 -0.0000 -0.0000 -vn -0.0000 -0.0000 -1.0000 -vt 0.875000 0.500000 -vt 0.625000 0.750000 -vt 0.625000 0.500000 -vt 0.375000 1.000000 -vt 0.375000 0.750000 -vt 0.625000 0.000000 -vt 0.375000 0.250000 -vt 0.375000 0.000000 -vt 0.375000 0.500000 -vt 0.125000 0.750000 -vt 0.125000 0.500000 -vt 0.625000 0.250000 -vt 0.875000 0.750000 -vt 0.625000 1.000000 -s 0 -f 5//1 3//1 1//1 -f 5/1/1 3/2/1 1/3/1 -f 3/2/2 8/4/2 4/5/2 -f 7/6/3 6/7/3 8/8/3 -f 2/9/4 8/10/4 6/11/4 -f 1/3/5 4/5/5 2/9/5 -f 5/12/6 2/9/6 6/7/6 -f 5/1/1 7/13/1 3/2/1 -f 3/2/2 7/14/2 8/4/2 -f 7/6/3 5/12/3 6/7/3 -f 2/9/4 4/5/4 8/10/4 -f 1/3/5 3/2/5 4/5/5 -f 5/12/6 1/3/6 2/9/6 diff --git a/src/main.rs b/src/main.rs index 8710a21..d5154ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::error::Error; -use std::fs::read_to_string; use std::path::Path; +use engine::asset::Assets; use engine::camera::fly::{ Extension as FlyCameraExtension, Fly as FlyCamera, @@ -9,29 +9,32 @@ use engine::camera::fly::{ }; use engine::camera::{Active as ActiveCamera, Camera}; use engine::color::Color; -use engine::data_types::dimens::Dimens; +use engine::data_types::dimens::{Dimens, Dimens3}; +use engine::ecs::actions::Actions; +use engine::ecs::phase::START as START_PHASE; use engine::ecs::sole::Single; -use engine::event::Start as StartEvent; -use engine::file_format::wavefront::mtl::parse as parse_mtl; -use engine::file_format::wavefront::obj::parse as parse_obj; use engine::input::Extension as InputExtension; use engine::lighting::{AttenuationParams, GlobalLight, PointLight}; -use engine::material::{Builder as MaterialBuilder, Flags as MaterialFlags}; +use engine::material::{Flags as MaterialFlags, Material}; +use engine::mesh::cube::{ + create as cube_mesh_create, + CreationSpec as CubeMeshCreationSpec, +}; +use engine::model::{Data as ModelData, Model}; use engine::renderer::opengl::Extension as OpenglRendererExtension; -use engine::shader::Program as ShaderProgram; -use engine::transform::Position; +use engine::transform::WorldPosition; use engine::vector::Vec3; use engine::window::{ Builder as WindowBuilder, - CreationHint as WindowCreationHint, - CreationHintValue as WindowCreationHintValue, CursorMode, Extension as WindowExtension, Window, }; use engine::Engine; -use tracing::Level; -use tracing_subscriber::FmtSubscriber; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::EnvFilter; const WINDOW_SIZE: Dimens<u32> = Dimens { width: 1920, height: 1080 }; @@ -45,57 +48,20 @@ const RESOURCE_DIR: &str = "res"; fn main() -> Result<(), Box<dyn Error>> { - let subscriber = FmtSubscriber::builder() - .with_max_level(Level::TRACE) - .finish(); - - tracing::subscriber::set_global_default(subscriber)?; + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .with( + EnvFilter::builder() + .with_default_directive(LevelFilter::DEBUG.into()) + .from_env()?, + ) + .init(); let mut engine = Engine::new(); - let teapot_obj = - parse_obj(&read_to_string(Path::new(RESOURCE_DIR).join("teapot.obj"))?)?; - - let teapot_mat_name = teapot_obj - .faces - .first() - .and_then(|face| face.material_name.as_ref()); - - let teapot_mats = teapot_obj.read_and_parse_material_libs(parse_mtl)?; - - let teapot_mat = teapot_mats - .into_iter() - .find(|mat| Some(&mat.name) == teapot_mat_name) - .ok_or("Teapot material was not found")?; - - engine.spawn(( - teapot_obj.to_mesh()?, - teapot_mat.material, - Position::from(Vec3 { x: 1.6, y: 0.0, z: 0.0 }), - ShaderProgram::new()?, - )); - - engine.spawn(( - PointLight::builder() - .position(Vec3 { x: -6.0, y: 3.0, z: 3.0 }) - .diffuse(YELLOW) - .attenuation_params(AttenuationParams { - linear: 0.045, - quadratic: 0.0075, - ..Default::default() - }) - .build(), - Position::from(Vec3 { x: -6.0, y: 3.0, z: 3.0 }), - parse_obj(&read_to_string(Path::new(RESOURCE_DIR).join("cube.obj"))?)? - .to_mesh()?, - MaterialBuilder::new().ambient(YELLOW * 5.0).build(), - ShaderProgram::new()?, - MaterialFlags::builder().use_ambient_color(true).build(), - )); - engine.spawn(( Camera::default(), - Position { + WorldPosition { position: Vec3 { x: 0.0, y: 0.0, z: 3.0 }, }, ActiveCamera, @@ -104,17 +70,14 @@ fn main() -> Result<(), Box<dyn Error>> engine.add_sole(GlobalLight::default())?; - engine.register_system(StartEvent, prepare_window); + engine.register_system(*START_PHASE, init); engine.add_extension(OpenglRendererExtension::default()); engine.add_extension( - WindowExtension::new(WindowBuilder::default().creation_hint( - WindowCreationHint::Samples, - WindowCreationHintValue::Number(8), - )) - .window_title("Game") - .window_size(WINDOW_SIZE), + WindowExtension::new(WindowBuilder::default().multisampling_sample_count(8)) + .window_title("Game") + .window_size(WINDOW_SIZE), ); engine.add_extension(FlyCameraExtension(FlyCameraOptions { @@ -128,7 +91,42 @@ fn main() -> Result<(), Box<dyn Error>> Ok(()) } -fn prepare_window(window: Single<Window>) +fn init(window: Single<Window>, mut assets: Single<Assets>, mut actions: Actions) { window.set_cursor_mode(CursorMode::Disabled).unwrap(); + + actions.spawn(( + PointLight::builder() + .diffuse(YELLOW) + .attenuation_params(AttenuationParams { + linear: 0.045, + quadratic: 0.0075, + ..Default::default() + }) + .build(), + WorldPosition::from(Vec3 { x: -6.0, y: 3.0, z: 3.0 }), + Model::new( + assets.store_with_name( + "light_cube", + ModelData::builder() + .mesh(cube_mesh_create( + CubeMeshCreationSpec::builder() + .dimens(Dimens3::from(2.0)) + .build(), + |face_verts, _, _| face_verts, + )) + .material( + "surface", + Material::builder().ambient(YELLOW * 5.0).build(), + ) + .build(), + ), + ), + MaterialFlags::builder().use_ambient_color(true).build(), + )); + + actions.spawn(( + Model::new(assets.load::<ModelData>(Path::new(RESOURCE_DIR).join("teapot.obj"))), + WorldPosition::from(Vec3 { x: 1.6, y: 0.0, z: 0.0 }), + )); } |