summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock2131
-rw-r--r--Cargo.toml10
-rw-r--r--TODO.md12
-rw-r--r--ecs-macros/src/lib.rs188
-rw-r--r--ecs/Cargo.toml7
-rw-r--r--ecs/examples/component_changed_event.rs78
-rw-r--r--ecs/examples/component_events.rs64
-rw-r--r--ecs/examples/component_relationship.rs65
-rw-r--r--ecs/examples/component_removed_event.rs46
-rw-r--r--ecs/examples/event_loop.rs38
-rw-r--r--ecs/examples/optional_component.rs2
-rw-r--r--ecs/examples/relationship.rs20
-rw-r--r--ecs/examples/with_local.rs3
-rw-r--r--ecs/examples/with_sole.rs16
-rw-r--r--ecs/src/actions.rs183
-rw-r--r--ecs/src/archetype.rs59
-rw-r--r--ecs/src/component.rs483
-rw-r--r--ecs/src/component/local.rs81
-rw-r--r--ecs/src/component/storage.rs1146
-rw-r--r--ecs/src/component/storage/archetype.rs374
-rw-r--r--ecs/src/component/storage/graph.rs432
-rw-r--r--ecs/src/entity.rs311
-rw-r--r--ecs/src/entity/obtainer.rs29
-rw-r--r--ecs/src/event.rs104
-rw-r--r--ecs/src/event/component.rs133
-rw-r--r--ecs/src/extension.rs17
-rw-r--r--ecs/src/lib.rs737
-rw-r--r--ecs/src/lock.rs220
-rw-r--r--ecs/src/pair.rs687
-rw-r--r--ecs/src/phase.rs20
-rw-r--r--ecs/src/private.rs2
-rw-r--r--ecs/src/query.rs534
-rw-r--r--ecs/src/query/flexible.rs139
-rw-r--r--ecs/src/query/options.rs59
-rw-r--r--ecs/src/query/term.rs116
-rw-r--r--ecs/src/relationship.rs471
-rw-r--r--ecs/src/sole.rs33
-rw-r--r--ecs/src/system.rs327
-rw-r--r--ecs/src/system/initializable.rs131
-rw-r--r--ecs/src/system/observer.rs310
-rw-r--r--ecs/src/system/stateful.rs293
-rw-r--r--ecs/src/type_name.rs15
-rw-r--r--ecs/src/uid.rs228
-rw-r--r--ecs/src/util.rs263
-rw-r--r--ecs/src/util/array_vec.rs131
-rw-r--r--ecs/tests/query.rs413
-rw-r--r--engine/Cargo.toml20
-rw-r--r--engine/build.rs63
-rw-r--r--engine/src/asset.rs777
-rw-r--r--engine/src/camera/fly.rs76
-rw-r--r--engine/src/data_types/color.rs1
-rw-r--r--engine/src/data_types/dimens.rs65
-rw-r--r--engine/src/data_types/matrix.rs2
-rw-r--r--engine/src/data_types/vector.rs24
-rw-r--r--engine/src/draw_flags.rs2
-rw-r--r--engine/src/file_format/wavefront/mtl.rs200
-rw-r--r--engine/src/file_format/wavefront/obj.rs5
-rw-r--r--engine/src/image.rs184
-rw-r--r--engine/src/input.rs248
-rw-r--r--engine/src/input/keyboard.rs6
-rw-r--r--engine/src/input/mouse.rs6
-rw-r--r--engine/src/lib.rs36
-rw-r--r--engine/src/lighting.rs8
-rw-r--r--engine/src/material.rs117
-rw-r--r--engine/src/mesh.rs29
-rw-r--r--engine/src/mesh/cube.rs20
-rw-r--r--engine/src/model.rs176
-rw-r--r--engine/src/opengl/buffer.rs92
-rw-r--r--engine/src/opengl/debug.rs145
-rw-r--r--engine/src/opengl/mod.rs127
-rw-r--r--engine/src/opengl/shader.rs247
-rw-r--r--engine/src/opengl/texture.rs240
-rw-r--r--engine/src/opengl/util.rs30
-rw-r--r--engine/src/opengl/vertex_array.rs183
-rw-r--r--engine/src/projection.rs118
-rw-r--r--engine/src/renderer.rs79
-rw-r--r--engine/src/renderer/opengl.rs1542
-rw-r--r--engine/src/renderer/opengl/glsl/light.glsl4
-rw-r--r--engine/src/renderer/opengl/glutin_compat.rs268
-rw-r--r--engine/src/renderer/opengl/graphics_mesh.rs120
-rw-r--r--engine/src/renderer/opengl/vertex.rs (renamed from engine/src/vertex.rs)36
-rw-r--r--engine/src/texture.rs186
-rw-r--r--engine/src/transform.rs6
-rw-r--r--engine/src/util.rs145
-rw-r--r--engine/src/window.rs752
-rw-r--r--engine/src/windowing.rs669
-rw-r--r--engine/src/windowing/keyboard.rs763
-rw-r--r--engine/src/windowing/mouse.rs136
-rw-r--r--engine/src/windowing/window.rs171
-rw-r--r--engine/src/windowing/window/platform.rs12
-rw-r--r--engine/src/work_queue.rs44
-rw-r--r--glfw/Cargo.toml18
-rw-r--r--glfw/build.rs42
-rw-r--r--glfw/glfw.h6
-rw-r--r--glfw/src/ffi.rs9
-rw-r--r--glfw/src/init.rs59
-rw-r--r--glfw/src/lib.rs53
-rw-r--r--glfw/src/util.rs10
-rw-r--r--glfw/src/window.rs863
-rw-r--r--opengl-bindings/Cargo.toml65
-rw-r--r--opengl-bindings/build.rs107
-rw-r--r--opengl-bindings/src/buffer.rs167
-rw-r--r--opengl-bindings/src/data_types.rs37
-rw-r--r--opengl-bindings/src/debug.rs161
-rw-r--r--opengl-bindings/src/lib.rs119
-rw-r--r--opengl-bindings/src/misc.rs190
-rw-r--r--opengl-bindings/src/shader.rs366
-rw-r--r--opengl-bindings/src/texture.rs236
-rw-r--r--opengl-bindings/src/vertex_array.rs260
-rw-r--r--src/main.rs156
110 files changed, 15242 insertions, 7023 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 3952e63..1b57c31 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,18 +1,31 @@
# 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 = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "getrandom 0.3.3",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
[[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",
]
@@ -24,6 +37,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
+name = "android-activity"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046"
+dependencies = [
+ "android-properties",
+ "bitflags 2.9.0",
+ "cc",
+ "cesu8",
+ "jni",
+ "jni-sys",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-context",
+ "ndk-sys",
+ "num_enum",
+ "thiserror",
+]
+
+[[package]]
+name = "android-properties"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
+
+[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -36,29 +76,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
+name = "anyhow"
+version = "1.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
+[[package]]
+name = "as-raw-xcb-connection"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[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"
-version = "0.68.1"
+version = "0.71.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078"
+checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
dependencies = [
- "bitflags 2.4.0",
+ "bitflags 2.9.0",
"cexpr",
"clang-sys",
- "lazy_static",
- "lazycell",
- "peeking_take_while",
+ "itertools",
+ "log",
+ "prettyplease 0.2.31",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
- "syn",
+ "syn 2.0.100",
]
[[package]]
@@ -69,15 +133,30 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
-version = "2.4.0"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
+
+[[package]]
+name = "block2"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
+dependencies = [
+ "objc2 0.5.2",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
+checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[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"
@@ -86,12 +165,61 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "calloop"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
+dependencies = [
+ "bitflags 2.9.0",
+ "log",
+ "polling",
+ "rustix 0.38.44",
+ "slab",
+ "thiserror",
+]
+
+[[package]]
+name = "calloop-wayland-source"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
+dependencies = [
+ "calloop",
+ "rustix 0.38.44",
+ "wayland-backend",
+ "wayland-client",
+]
+
+[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
+name = "cc"
+version = "1.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951"
+dependencies = [
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -107,6 +235,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "cgl"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff"
+dependencies = [
+ "libc",
+]
+
+[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -135,9 +278,9 @@ dependencies = [
[[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",
@@ -146,18 +289,18 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.5.23"
+version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
+checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
-version = "4.5.23"
+version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
+checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8"
dependencies = [
"anstyle",
"clap_lex",
@@ -176,10 +319,69 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "core-graphics"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "libc",
+]
+
+[[package]]
name = "crc32fast"
-version = "1.3.2"
+version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
@@ -219,10 +421,68 @@ dependencies = [
]
[[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
name = "crunchy"
-version = "0.2.2"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
+
+[[package]]
+name = "cursor-icon"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
+
+[[package]]
+name = "dispatch"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
+
+[[package]]
+name = "dispatch2"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
+dependencies = [
+ "bitflags 2.9.0",
+ "objc2 0.6.1",
+]
+
+[[package]]
+name = "dlib"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
+dependencies = [
+ "libloading",
+]
+
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
+[[package]]
+name = "dpi"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
+checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
[[package]]
name = "ecs"
@@ -230,13 +490,14 @@ version = "0.1.0"
dependencies = [
"criterion",
"ecs-macros",
- "hashbrown 0.15.2",
- "linkme",
+ "hashbrown",
+ "parking_lot",
"paste",
"seq-macro",
"thiserror",
"tracing",
"util-macros",
+ "vizoxide",
]
[[package]]
@@ -245,52 +506,104 @@ version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.100",
"toml",
]
[[package]]
name = "either"
-version = "1.13.0"
+version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "engine"
version = "0.1.0"
dependencies = [
- "bitflags 2.4.0",
+ "bitflags 2.9.0",
+ "cfg_aliases",
+ "crossbeam-channel",
"ecs",
- "gl",
- "glfw",
+ "glutin",
"image",
+ "nu-ansi-term",
+ "opengl-bindings",
+ "parking_lot",
"paste",
+ "raw-window-handle",
+ "safer-ffi",
"seq-macro",
"thiserror",
"tracing",
"util-macros",
+ "winit",
]
[[package]]
name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "ext-trait"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d772df1c1a777963712fb68e014235e80863d6a91a85c4e06ba2d16243a310e5"
+dependencies = [
+ "ext-trait-proc_macros",
+]
+
+[[package]]
+name = "ext-trait-proc_macros"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ab7934152eaf26aa5aa9f7371408ad5af4c31357073c9e84c3b9d7f11ad639a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "extension-traits"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+checksum = "a296e5a895621edf9fa8329c83aa1cb69a964643e36cf54d8d7a69b789089537"
+dependencies = [
+ "ext-trait",
+]
+
+[[package]]
+name = "extern-c"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320bea982e85d42441eb25c49b41218e7eaa2657e8f90bc4eca7437376751e23"
[[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",
@@ -298,9 +611,36 @@ dependencies = [
[[package]]
name = "foldhash"
-version = "0.1.4"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "game-newest"
@@ -312,12 +652,36 @@ dependencies = [
]
[[package]]
-name = "gl"
-version = "0.14.0"
+name = "gethostname"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404"
+checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818"
dependencies = [
- "gl_generator",
+ "libc",
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
]
[[package]]
@@ -332,37 +696,83 @@ dependencies = [
]
[[package]]
-name = "glfw"
-version = "0.1.0"
+name = "glob"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
+
+[[package]]
+name = "glutin"
+version = "0.32.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325"
dependencies = [
- "bindgen",
- "bitflags 2.4.0",
- "libc",
- "thiserror",
- "util-macros",
+ "bitflags 2.9.0",
+ "cfg_aliases",
+ "cgl",
+ "dispatch2",
+ "glutin_egl_sys",
+ "glutin_glx_sys",
+ "glutin_wgl_sys",
+ "libloading",
+ "objc2 0.6.1",
+ "objc2-app-kit 0.3.1",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.1",
+ "once_cell",
+ "raw-window-handle",
+ "wayland-sys",
+ "windows-sys 0.52.0",
+ "x11-dl",
]
[[package]]
-name = "glob"
-version = "0.3.1"
+name = "glutin_egl_sys"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2"
+dependencies = [
+ "gl_generator",
+ "windows-sys 0.52.0",
+]
[[package]]
-name = "half"
-version = "2.4.1"
+name = "glutin_glx_sys"
+version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
+checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185"
dependencies = [
- "cfg-if",
- "crunchy",
+ "gl_generator",
+ "x11-dl",
]
[[package]]
-name = "hashbrown"
-version = "0.14.3"
+name = "glutin_wgl_sys"
+version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
+checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e"
+dependencies = [
+ "gl_generator",
+]
+
+[[package]]
+name = "graphviz-sys"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97dbac5b269c7160b78812a9873f548668aa34b62f28f7a64328a6cd94feb47d"
+dependencies = [
+ "bindgen",
+]
+
+[[package]]
+name = "half"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+]
[[package]]
name = "hashbrown"
@@ -377,44 +787,43 @@ dependencies = [
[[package]]
name = "hermit-abi"
-version = "0.4.0"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
+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 0.14.3",
+ "hashbrown",
]
[[package]]
name = "is-terminal"
-version = "0.4.13"
+version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b"
+checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi",
"libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -428,15 +837,57 @@ dependencies = [
[[package]]
name = "itoa"
-version = "1.0.14"
+version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
-name = "jpeg-decoder"
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
+[[package]]
+name = "jobserver"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
+dependencies = [
+ "getrandom 0.3.3",
+ "libc",
+]
+
+[[package]]
+name = "jpeg-decoder"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
[[package]]
name = "khronos_api"
@@ -446,63 +897,104 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "lazy_static"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
-
-[[package]]
-name = "lazycell"
-version = "1.3.0"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[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 0.52.6",
]
[[package]]
-name = "linkme"
-version = "0.3.29"
+name = "libredox"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70fe496a7af8c406f877635cbf3cd6a9fac9d6f443f58691cd8afe6ce0971af4"
+checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
- "linkme-impl",
+ "bitflags 2.9.0",
+ "libc",
+ "redox_syscall 0.5.10",
]
[[package]]
-name = "linkme-impl"
-version = "0.3.29"
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b01f197a15988fb5b2ec0a5a9800c97e70771499c456ad757d63b3c5e9b96e75"
+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+
+[[package]]
+name = "lock_api"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "autocfg",
+ "scopeguard",
]
[[package]]
name = "log"
-version = "0.4.20"
+version = "0.4.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
+
+[[package]]
+name = "macro_rules_attribute"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf0c9b980bf4f3a37fd7b1c066941dd1b1d0152ce6ee6e8fe8c49b9f6810d862"
+dependencies = [
+ "macro_rules_attribute-proc_macro",
+ "paste",
+]
+
+[[package]]
+name = "macro_rules_attribute-proc_macro"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58093314a45e00c77d5c508f76e77c3396afbbc0d01506e7fae47b018bac2b1d"
+
+[[package]]
+name = "matchers"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
+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 = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "memmap2"
+version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
+checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
+dependencies = [
+ "libc",
+]
[[package]]
name = "minimal-lexical"
@@ -512,15 +1004,45 @@ 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",
]
[[package]]
+name = "ndk"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+dependencies = [
+ "bitflags 2.9.0",
+ "jni-sys",
+ "log",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
+name = "ndk-sys"
+version = "0.6.0+11769913"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+dependencies = [
+ "jni-sys",
+]
+
+[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -541,46 +1063,315 @@ dependencies = [
]
[[package]]
-name = "num-integer"
-version = "0.1.45"
+name = "num-traits"
+version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
- "num-traits",
]
[[package]]
-name = "num-rational"
-version = "0.4.1"
+name = "num_enum"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
+checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179"
dependencies = [
- "autocfg",
- "num-integer",
- "num-traits",
+ "num_enum_derive",
]
[[package]]
-name = "num-traits"
-version = "0.2.17"
+name = "num_enum_derive"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
+checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
dependencies = [
- "autocfg",
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
+[[package]]
+name = "objc-sys"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
+
+[[package]]
+name = "objc2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
+dependencies = [
+ "objc-sys",
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551"
+dependencies = [
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
+dependencies = [
+ "bitflags 2.9.0",
+ "block2",
+ "libc",
+ "objc2 0.5.2",
+ "objc2-core-data",
+ "objc2-core-image",
+ "objc2-foundation 0.2.2",
+ "objc2-quartz-core",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc"
+dependencies = [
+ "bitflags 2.9.0",
+ "objc2 0.6.1",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.1",
+]
+
+[[package]]
+name = "objc2-cloud-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
+dependencies = [
+ "bitflags 2.9.0",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-core-location",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-contacts"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
+dependencies = [
+ "block2",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
+dependencies = [
+ "bitflags 2.9.0",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
+dependencies = [
+ "bitflags 2.9.0",
+ "dispatch2",
+ "objc2 0.6.1",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
+dependencies = [
+ "block2",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-metal",
+]
+
+[[package]]
+name = "objc2-core-location"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
+dependencies = [
+ "block2",
+ "objc2 0.5.2",
+ "objc2-contacts",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-foundation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
+dependencies = [
+ "bitflags 2.9.0",
+ "block2",
+ "dispatch",
+ "libc",
+ "objc2 0.5.2",
+]
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
+dependencies = [
+ "bitflags 2.9.0",
+ "objc2 0.6.1",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-link-presentation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
+dependencies = [
+ "block2",
+ "objc2 0.5.2",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-metal"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
+dependencies = [
+ "bitflags 2.9.0",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
+dependencies = [
+ "bitflags 2.9.0",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-metal",
+]
+
+[[package]]
+name = "objc2-symbols"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
+dependencies = [
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-ui-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
+dependencies = [
+ "bitflags 2.9.0",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-cloud-kit",
+ "objc2-core-data",
+ "objc2-core-image",
+ "objc2-core-location",
+ "objc2-foundation 0.2.2",
+ "objc2-link-presentation",
+ "objc2-quartz-core",
+ "objc2-symbols",
+ "objc2-uniform-type-identifiers",
+ "objc2-user-notifications",
+]
+
+[[package]]
+name = "objc2-uniform-type-identifiers"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
+dependencies = [
+ "block2",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-user-notifications"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
+dependencies = [
+ "bitflags 2.9.0",
+ "block2",
+ "objc2 0.5.2",
+ "objc2-core-location",
+ "objc2-foundation 0.2.2",
]
[[package]]
name = "once_cell"
-version = "1.18.0"
+version = "1.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
[[package]]
name = "oorandom"
-version = "11.1.4"
+version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9"
+checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
+
+[[package]]
+name = "opengl-bindings"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "bitflags 2.9.0",
+ "gl_generator",
+ "glutin",
+ "safer-ffi",
+ "thiserror",
+ "toml",
+ "util-macros",
+]
+
+[[package]]
+name = "orbclient"
+version = "0.3.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43"
+dependencies = [
+ "libredox",
+]
[[package]]
name = "overload"
@@ -589,28 +1380,77 @@ 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 0.5.10",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[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"
-version = "0.1.2"
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pin-project"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
+checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
[[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 = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[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",
@@ -620,63 +1460,272 @@ dependencies = [
]
[[package]]
+name = "polling"
+version = "3.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix 1.0.7",
+ "tracing",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86"
+dependencies = [
+ "proc-macro2",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.100",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
+dependencies = [
+ "toml_edit",
+]
+
+[[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 = "quick-xml"
+version = "0.37.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
+dependencies = [
+ "memchr",
+]
+
+[[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 = "r-efi"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.16",
+]
+
+[[package]]
+name = "raw-window-handle"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+
+[[package]]
+name = "redox_syscall"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[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 = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
+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 = "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 = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustc-hash"
-version = "1.1.0"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags 2.9.0",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustix"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+dependencies = [
+ "bitflags 2.9.0",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.9.4",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "ryu"
-version = "1.0.18"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "safer-ffi"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435fdd58b61a6f1d8545274c1dfa458e905ff68c166e65e294a0130ef5e675bd"
+dependencies = [
+ "extern-c",
+ "libc",
+ "macro_rules_attribute",
+ "paste",
+ "safer_ffi-proc_macros",
+ "scopeguard",
+ "stabby",
+ "uninit",
+ "unwind_safe",
+ "with_builtin_macros",
+]
+
+[[package]]
+name = "safer_ffi-proc_macros"
+version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+checksum = "f0f25be5ba5f319542edb31925517e0380245ae37df50a9752cdbc05ef948156"
+dependencies = [
+ "macro_rules_attribute",
+ "prettyplease 0.1.25",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
[[package]]
name = "same-file"
@@ -688,36 +1737,54 @@ dependencies = [
]
[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+
+[[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",
- "syn",
+ "syn 2.0.100",
]
[[package]]
name = "serde_json"
-version = "1.0.133"
+version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
@@ -727,14 +1794,20 @@ dependencies = [
[[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",
]
[[package]]
+name = "sha2-const-stable"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9"
+
+[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -745,9 +1818,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"
@@ -756,16 +1829,105 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
name = "smallvec"
-version = "1.11.1"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
+
+[[package]]
+name = "smithay-client-toolkit"
+version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
+checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
+dependencies = [
+ "bitflags 2.9.0",
+ "calloop",
+ "calloop-wayland-source",
+ "cursor-icon",
+ "libc",
+ "log",
+ "memmap2",
+ "rustix 0.38.44",
+ "thiserror",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-csd-frame",
+ "wayland-cursor",
+ "wayland-protocols",
+ "wayland-protocols-wlr",
+ "wayland-scanner",
+ "xkeysym",
+]
+
+[[package]]
+name = "smol_str"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "stabby"
+version = "36.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89b7e94eaf470c2e76b5f15fb2fb49714471a36cc512df5ee231e62e82ec79f8"
+dependencies = [
+ "rustversion",
+ "stabby-abi",
+]
+
+[[package]]
+name = "stabby-abi"
+version = "36.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dc7a63b8276b54e51bfffe3d85da56e7906b2dcfcb29018a8ab666c06734c1a"
+dependencies = [
+ "rustc_version",
+ "rustversion",
+ "sha2-const-stable",
+ "stabby-macros",
+]
+
+[[package]]
+name = "stabby-macros"
+version = "36.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eecb7ec5611ec93ec79d120fbe55f31bea234dc1bed1001d4a071bb688651615"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "rand",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
[[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",
@@ -774,29 +1936,29 @@ 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",
- "syn",
+ "syn 2.0.100",
]
[[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",
@@ -814,9 +1976,9 @@ dependencies = [
[[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",
@@ -826,18 +1988,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",
@@ -848,9 +2010,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",
@@ -859,55 +2021,67 @@ 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",
- "syn",
+ "syn 2.0.100",
]
[[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 = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "uninit"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e130f2ed46ca5d8ec13c7ff95836827f92f5f5f37fd2b2bf16f33c408d98bb6"
+dependencies = [
+ "extension-traits",
+]
+
+[[package]]
+name = "unwind_safe"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0976c77def3f1f75c4ef892a292c31c0bbe9e3d0702c63044d7c76db298171a3"
[[package]]
name = "util-macros"
@@ -915,14 +2089,24 @@ version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.100",
]
[[package]]
-name = "valuable"
-version = "0.1.0"
+name = "version_check"
+version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+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"
@@ -935,6 +2119,221 @@ dependencies = [
]
[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wayland-backend"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121"
+dependencies = [
+ "cc",
+ "downcast-rs",
+ "rustix 0.38.44",
+ "scoped-tls",
+ "smallvec",
+ "wayland-sys",
+]
+
+[[package]]
+name = "wayland-client"
+version = "0.31.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61"
+dependencies = [
+ "bitflags 2.9.0",
+ "rustix 0.38.44",
+ "wayland-backend",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-csd-frame"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e"
+dependencies = [
+ "bitflags 2.9.0",
+ "cursor-icon",
+ "wayland-backend",
+]
+
+[[package]]
+name = "wayland-cursor"
+version = "0.31.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a65317158dec28d00416cb16705934070aef4f8393353d41126c54264ae0f182"
+dependencies = [
+ "rustix 0.38.44",
+ "wayland-client",
+ "xcursor",
+]
+
+[[package]]
+name = "wayland-protocols"
+version = "0.32.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a"
+dependencies = [
+ "bitflags 2.9.0",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-plasma"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fd38cdad69b56ace413c6bcc1fbf5acc5e2ef4af9d5f8f1f9570c0c83eae175"
+dependencies = [
+ "bitflags 2.9.0",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-wlr"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf"
+dependencies = [
+ "bitflags 2.9.0",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-scanner"
+version = "0.31.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484"
+dependencies = [
+ "proc-macro2",
+ "quick-xml",
+ "quote",
+]
+
+[[package]]
+name = "wayland-sys"
+version = "0.31.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615"
+dependencies = [
+ "dlib",
+ "log",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -967,11 +2366,20 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
]
[[package]]
@@ -980,7 +2388,37 @@ version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
]
[[package]]
@@ -989,30 +2427,66 @@ 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_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[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.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[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.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
@@ -1025,39 +2499,244 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[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.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[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.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[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.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
+name = "winit"
+version = "0.30.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4"
+dependencies = [
+ "ahash",
+ "android-activity",
+ "atomic-waker",
+ "bitflags 2.9.0",
+ "block2",
+ "bytemuck",
+ "calloop",
+ "cfg_aliases",
+ "concurrent-queue",
+ "core-foundation",
+ "core-graphics",
+ "cursor-icon",
+ "dpi",
+ "js-sys",
+ "libc",
+ "memmap2",
+ "ndk",
+ "objc2 0.5.2",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
+ "objc2-ui-kit",
+ "orbclient",
+ "percent-encoding",
+ "pin-project",
+ "raw-window-handle",
+ "redox_syscall 0.4.1",
+ "rustix 0.38.44",
+ "smithay-client-toolkit",
+ "smol_str",
+ "tracing",
+ "unicode-segmentation",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-protocols-plasma",
+ "web-sys",
+ "web-time",
+ "windows-sys 0.52.0",
+ "x11-dl",
+ "x11rb",
+ "xkbcommon-dl",
+]
+
+[[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 = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags 2.9.0",
+]
+
+[[package]]
+name = "with_builtin_macros"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a59d55032495429b87f9d69954c6c8602e4d3f3e0a747a12dea6b0b23de685da"
+dependencies = [
+ "with_builtin_macros-proc_macros",
+]
+
+[[package]]
+name = "with_builtin_macros-proc_macros"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15bd7679c15e22924f53aee34d4e448c45b674feb6129689af88593e129f8f42"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "x11-dl"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+dependencies = [
+ "libc",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "x11rb"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12"
+dependencies = [
+ "as-raw-xcb-connection",
+ "gethostname",
+ "libc",
+ "libloading",
+ "once_cell",
+ "rustix 0.38.44",
+ "x11rb-protocol",
+]
+
+[[package]]
+name = "x11rb-protocol"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d"
+
+[[package]]
+name = "xcursor"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61"
+
+[[package]]
+name = "xkbcommon-dl"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5"
+dependencies = [
+ "bitflags 2.9.0",
+ "dlib",
+ "log",
+ "once_cell",
+ "xkeysym",
+]
+
+[[package]]
+name = "xkeysym"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
+
+[[package]]
name = "xml-rs"
-version = "0.8.19"
+version = "0.8.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4"
+
+[[package]]
+name = "zerocopy"
+version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a"
+checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 18fe135..21eb777 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,9 +4,13 @@ version = "0.1.0"
edition = "2021"
[workspace]
-members = ["glfw", "engine", "ecs", "ecs-macros", "util-macros"]
+members = ["engine", "ecs", "ecs-macros", "util-macros", "opengl-bindings"]
[dependencies]
engine = { path = "./engine" }
-tracing = "0.1.39"
-tracing-subscriber = "0.3.17"
+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"]
diff --git a/TODO.md b/TODO.md
index d0fa923..ecbb8e7 100644
--- a/TODO.md
+++ b/TODO.md
@@ -3,7 +3,7 @@
- [ ] Remove possible edge cases in ECS component storage
- [ ] A Query<()> yields all components. Should this be the behaviour?
- [ ] Improve ECS component storage performance
- - [ ] Give archetypes edges for faster component addition & removal
+ - [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
@@ -12,6 +12,16 @@
- [ ] 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
diff --git a/ecs-macros/src/lib.rs b/ecs-macros/src/lib.rs
index b178022..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,61 +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::event::component::{
- Removed as ComponentRemovedEvent,
- Kind as ComponentEventKind,
- };
- 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
+ *#id_var_ident
}
- fn self_id(&self) -> Uid
+ fn name(&self) -> &'static str
{
- Self::id()
- }
-
- fn get_event_uid(&self, event_kind: ComponentEventKind) -> Uid
- {
- match event_kind {
- ComponentEventKind::Removed => ComponentRemovedEvent::<Self>::id(),
- _ => {
- panic!(
- "Support for event kind {event_kind:?} not implemented!"
- );
- }
- }
- }
-
- fn as_any_mut(&mut self) -> &mut dyn Any
- {
- self
- }
-
- fn as_any(&self) -> &dyn Any
- {
- self
+ std::any::type_name::<Self>()
}
}
@@ -169,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
{
@@ -217,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()
}
@@ -264,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);
@@ -390,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 5e1abac..5ea9fc7 100644
--- a/ecs/Cargo.toml
+++ b/ecs/Cargo.toml
@@ -4,17 +4,18 @@ 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 }
-linkme = "0.3.29"
+tracing = "0.1.39"
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"
diff --git a/ecs/examples/component_changed_event.rs b/ecs/examples/component_changed_event.rs
new file mode 100644
index 0000000..1a53a88
--- /dev/null
+++ b/ecs/examples/component_changed_event.rs
@@ -0,0 +1,78 @@
+use ecs::event::component::Changed;
+use ecs::pair::Pair;
+use ecs::phase::UPDATE as UPDATE_PHASE;
+use ecs::system::observer::Observe;
+use ecs::{Component, Query, World};
+
+#[derive(Component)]
+struct SomeData
+{
+ num: u64,
+}
+
+#[derive(Component)]
+struct Greeting
+{
+ greeting: String,
+}
+
+fn say_hello(query: Query<(&SomeData, &mut Greeting)>)
+{
+ for (data, mut greeting) in &query {
+ println!("{}: {}", greeting.greeting, data.num);
+
+ if greeting.greeting == "Good evening" {
+ greeting.greeting = "Good morning".to_string();
+ greeting.set_changed();
+ }
+ }
+}
+
+fn print_changed_greetings(observe: Observe<'_, Pair<Changed, Greeting>>)
+{
+ println!("\nChanged greetings:");
+
+ for evt_match in &observe {
+ let greeting = evt_match.get_changed_comp();
+
+ println!("A greeting changed to {}", greeting.greeting);
+ }
+
+ println!("");
+}
+
+fn main()
+{
+ let mut world = World::new();
+
+ world.register_system(*UPDATE_PHASE, say_hello);
+
+ world.register_observer(print_changed_greetings);
+
+ world.create_entity((
+ SomeData { num: 987_654 },
+ Greeting {
+ greeting: "Good afternoon".to_string(),
+ },
+ ));
+
+ world.create_entity((
+ SomeData { num: 345 },
+ Greeting { greeting: "Good evening".to_string() },
+ ));
+
+ world.step();
+
+ world.step();
+
+ for (mut greeting,) in &world.query::<(&mut Greeting,), ()>() {
+ if greeting.greeting == "Good afternoon" {
+ greeting.greeting = "Yo yo".to_string();
+ greeting.set_changed();
+ }
+ }
+
+ world.step();
+
+ world.step();
+}
diff --git a/ecs/examples/component_events.rs b/ecs/examples/component_events.rs
new file mode 100644
index 0000000..af09ff9
--- /dev/null
+++ b/ecs/examples/component_events.rs
@@ -0,0 +1,64 @@
+use ecs::actions::Actions;
+use ecs::component::Component;
+use ecs::event::component::{Changed, Removed};
+use ecs::pair::Pair;
+use ecs::phase::UPDATE;
+use ecs::system::observer::Observe;
+use ecs::{Component, Query, World};
+
+#[derive(Debug, Component)]
+struct CheeseCrumbs
+{
+ cnt: usize,
+}
+
+#[derive(Debug, Component)]
+struct Cheese
+{
+ name: &'static str,
+}
+
+fn eat_cheese(query: Query<(&Cheese, &mut CheeseCrumbs)>, mut actions: Actions)
+{
+ for (cheese_ent_id, (_, mut cheese_crumbs)) in query.iter_with_euids() {
+ println!("Eating cheese!");
+
+ cheese_crumbs.cnt += 40;
+ cheese_crumbs.set_changed();
+
+ actions.remove_components(cheese_ent_id, [Cheese::id()]);
+ }
+}
+
+fn on_cheese_removed(observe: Observe<Pair<Removed, Cheese>>)
+{
+ for evt_match in &observe {
+ let cheese = evt_match.get_removed_comp();
+
+ println!("{} cheese was eaten", cheese.name);
+ }
+}
+
+fn on_cheese_crumbs_changed(observe: Observe<Pair<Changed, CheeseCrumbs>>)
+{
+ for evt_match in &observe {
+ let cheese_crumbs = evt_match.get_changed_comp();
+
+ println!("Cheese crumbs count changed to {}", cheese_crumbs.cnt);
+ }
+}
+
+fn main()
+{
+ let mut world = World::new();
+
+ world.register_system(*UPDATE, eat_cheese);
+ world.register_observer(on_cheese_removed);
+ world.register_observer(on_cheese_crumbs_changed);
+
+ world.create_entity((Cheese { name: "Brie" }, CheeseCrumbs { cnt: 0 }));
+ world.create_entity((Cheese { name: "Parmesan" }, CheeseCrumbs { cnt: 0 }));
+ world.create_entity((Cheese { name: "Gouda" }, CheeseCrumbs { cnt: 0 }));
+
+ world.step();
+}
diff --git a/ecs/examples/component_relationship.rs b/ecs/examples/component_relationship.rs
new file mode 100644
index 0000000..e07b214
--- /dev/null
+++ b/ecs/examples/component_relationship.rs
@@ -0,0 +1,65 @@
+use ecs::pair::Pair;
+use ecs::phase::START as START_PHASE;
+use ecs::{Component, Query, World};
+
+#[derive(Component)]
+struct Person
+{
+ name: String,
+}
+
+fn print_dog_likers(query: Query<(&Person, Pair<Likes, &Dogs>)>)
+{
+ for (person, liked_dogs) in &query {
+ println!(
+ "{} likes {} dogs!",
+ person.name,
+ if liked_dogs.large { "large" } else { "small" },
+ );
+ }
+}
+
+#[derive(Component)]
+struct Likes;
+
+#[derive(Component)]
+struct Cats;
+
+#[derive(Component)]
+struct Dogs
+{
+ large: bool,
+}
+
+fn main()
+{
+ let mut world = World::new();
+
+ world.register_system(*START_PHASE, print_dog_likers);
+
+ world.create_entity((
+ Person { name: "Irving".to_string() },
+ Pair::builder()
+ .relation::<Likes>()
+ .target_as_data(Dogs { large: true })
+ .build(),
+ ));
+
+ world.create_entity((
+ Person { name: "Mark".to_string() },
+ Pair::builder()
+ .relation::<Likes>()
+ .target_as_data(Cats)
+ .build(),
+ ));
+
+ world.create_entity((
+ Person { name: "Helena".to_string() },
+ Pair::builder()
+ .relation::<Likes>()
+ .target_as_data(Dogs { large: false })
+ .build(),
+ ));
+
+ world.step();
+}
diff --git a/ecs/examples/component_removed_event.rs b/ecs/examples/component_removed_event.rs
new file mode 100644
index 0000000..776aa48
--- /dev/null
+++ b/ecs/examples/component_removed_event.rs
@@ -0,0 +1,46 @@
+use ecs::actions::Actions;
+use ecs::component::Component;
+use ecs::event::component::Removed;
+use ecs::pair::Pair;
+use ecs::phase::UPDATE;
+use ecs::system::observer::Observe;
+use ecs::{Component, Query, World};
+
+#[derive(Debug, Component)]
+struct Cheese
+{
+ name: &'static str,
+}
+
+fn eat_cheese(query: Query<(&Cheese,)>, mut actions: Actions)
+{
+ for (cheese_ent_id, (_,)) in query.iter_with_euids() {
+ println!("Eating cheese!");
+
+ actions.remove_components(cheese_ent_id, [Cheese::id()]);
+ }
+}
+
+fn on_cheese_removed(observe: Observe<Pair<Removed, Cheese>>)
+{
+ for evt_match in &observe {
+ let cheese = evt_match.get_removed_comp();
+
+ println!("{} cheese was eaten", cheese.name);
+ }
+}
+
+fn main()
+{
+ let mut world = World::new();
+
+ world.register_system(*UPDATE, eat_cheese);
+ world.register_observer(on_cheese_removed);
+
+ world.create_entity((Cheese { name: "Brie" },));
+ world.create_entity((Cheese { name: "Parmesan" },));
+ world.create_entity((Cheese { name: "Gouda" },));
+
+ world.step();
+ world.step();
+}
diff --git a/ecs/examples/event_loop.rs b/ecs/examples/event_loop.rs
index 2365eb0..bec2c00 100644
--- a/ecs/examples/event_loop.rs
+++ b/ecs/examples/event_loop.rs
@@ -1,7 +1,7 @@
use ecs::actions::Actions;
+use ecs::pair::{ChildOf, Pair};
use ecs::phase::{Phase, UPDATE as UPDATE_PHASE};
-use ecs::relationship::{ChildOf, Relationship};
-use ecs::{static_entity, Component, Query, World};
+use ecs::{declare_entity, Component, Query, World};
#[derive(Component)]
struct Wool
@@ -65,25 +65,47 @@ fn age(query: Query<(&mut Health, &Name)>, mut actions: Actions)
}
}
-static_entity!(
+declare_entity!(
SHEER_PHASE,
- (Phase, <Relationship<ChildOf, Phase>>::new(*UPDATE_PHASE))
+ (
+ Phase,
+ Pair::builder()
+ .relation::<ChildOf>()
+ .target_id(*UPDATE_PHASE)
+ .build()
+ )
);
-static_entity!(
+declare_entity!(
FEED_PHASE,
- (Phase, <Relationship<ChildOf, Phase>>::new(*SHEER_PHASE))
+ (
+ Phase,
+ Pair::builder()
+ .relation::<ChildOf>()
+ .target_id(*SHEER_PHASE)
+ .build()
+ )
);
-static_entity!(
+declare_entity!(
AGE_PHASE,
- (Phase, <Relationship<ChildOf, Phase>>::new(*FEED_PHASE))
+ (
+ Phase,
+ Pair::builder()
+ .relation::<ChildOf>()
+ .target_id(*FEED_PHASE)
+ .build()
+ )
);
fn main()
{
let mut world = World::new();
+ world.create_declared_entity(&SHEER_PHASE);
+ world.create_declared_entity(&FEED_PHASE);
+ world.create_declared_entity(&AGE_PHASE);
+
world.register_system(*SHEER_PHASE, sheer);
world.register_system(*FEED_PHASE, feed);
world.register_system(*AGE_PHASE, age);
diff --git a/ecs/examples/optional_component.rs b/ecs/examples/optional_component.rs
index 488dad2..ebc9115 100644
--- a/ecs/examples/optional_component.rs
+++ b/ecs/examples/optional_component.rs
@@ -21,7 +21,7 @@ pub struct CatName
name: String,
}
-fn pet_cats(query: Query<(&CatName, &mut 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 {
diff --git a/ecs/examples/relationship.rs b/ecs/examples/relationship.rs
index 240884a..4e94151 100644
--- a/ecs/examples/relationship.rs
+++ b/ecs/examples/relationship.rs
@@ -1,5 +1,5 @@
+use ecs::pair::{Pair, Wildcard};
use ecs::phase::START as START_PHASE;
-use ecs::relationship::Relationship;
use ecs::{Component, Query, World};
#[derive(Component)]
@@ -17,16 +17,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 {
+ for (_, health, target_sword) in &player_query {
println!("Player health: {}", health.health);
- if let Some(sword) = sword_relationship.get(0) {
+ if let Some(sword_ent) = target_sword.get_target_ent() {
+ let sword = sword_ent
+ .get::<Sword>()
+ .expect("Sword entity is missing sword component");
+
println!("Player sword attack strength: {}", sword.attack_strength);
}
}
@@ -43,7 +46,10 @@ fn main()
world.create_entity((
Player,
Health { health: 180 },
- Relationship::<Holding, Sword>::new(sword_uid),
+ Pair::builder()
+ .relation::<Holding>()
+ .target_id(sword_uid)
+ .build(),
));
world.step();
diff --git a/ecs/examples/with_local.rs b/ecs/examples/with_local.rs
index 4658fc0..7a36d0e 100644
--- a/ecs/examples/with_local.rs
+++ b/ecs/examples/with_local.rs
@@ -1,6 +1,7 @@
use ecs::component::local::Local;
use ecs::phase::UPDATE as UPDATE_PHASE;
-use ecs::system::{Into, System};
+use ecs::system::initializable::Initializable;
+use ecs::system::Into;
use ecs::{Component, Query, World};
#[derive(Component)]
diff --git a/ecs/examples/with_sole.rs b/ecs/examples/with_sole.rs
index 689e562..7e89b0a 100644
--- a/ecs/examples/with_sole.rs
+++ b/ecs/examples/with_sole.rs
@@ -1,7 +1,7 @@
+use ecs::pair::{ChildOf, Pair};
use ecs::phase::{Phase, UPDATE as UPDATE_PHASE};
-use ecs::relationship::{ChildOf, Relationship};
use ecs::sole::Single;
-use ecs::{static_entity, Component, Query, Sole, World};
+use ecs::{declare_entity, Component, Query, Sole, World};
#[derive(Component)]
struct Ammo
@@ -31,15 +31,23 @@ fn print_total_ammo_count(ammo_counter: Single<AmmoCounter>)
assert_eq!(ammo_counter.counter, 19);
}
-static_entity!(
+declare_entity!(
PRINT_AMMO_COUNT_PHASE,
- (Phase, <Relationship<ChildOf, Phase>>::new(*UPDATE_PHASE))
+ (
+ Phase,
+ Pair::builder()
+ .relation::<ChildOf>()
+ .target_id(*UPDATE_PHASE)
+ .build()
+ )
);
fn main()
{
let mut world = World::new();
+ world.create_declared_entity(&PRINT_AMMO_COUNT_PHASE);
+
world.register_system(*UPDATE_PHASE, count_ammo);
world.register_system(*PRINT_AMMO_COUNT_PHASE, print_total_ammo_count);
diff --git a/ecs/src/actions.rs b/ecs/src/actions.rs
index 3988a77..549e341 100644
--- a/ecs/src/actions.rs
+++ b/ecs/src/actions.rs
@@ -1,13 +1,12 @@
+use std::any::type_name;
use std::marker::PhantomData;
-use std::sync::{Arc, Weak};
-
-use crate::component::{
- Component,
- Metadata as ComponentMetadata,
- Sequence as ComponentSequence,
-};
-use crate::system::{Param as SystemParam, System};
-use crate::uid::{Kind as UidKind, Uid};
+use std::rc::{Rc, Weak};
+
+use crate::component::{Parts as ComponentParts, Sequence as ComponentSequence};
+use crate::event::component::Removed;
+use crate::pair::Pair;
+use crate::system::{Metadata as SystemMetadata, Param as SystemParam};
+use crate::uid::{Kind as UidKind, Uid, WithUidTuple};
use crate::{ActionQueue, World};
/// Used to to queue up actions for a [`World`] to perform.
@@ -15,18 +14,23 @@ use crate::{ActionQueue, World};
pub struct Actions<'world>
{
action_queue: &'world ActionQueue,
- action_queue_weak: Weak<ActionQueue>,
+ world: Option<&'world World>,
}
-impl<'world> Actions<'world>
+impl Actions<'_>
{
- /// Queues up a entity to spawn at the end of the current tick.
- pub fn spawn<Comps: ComponentSequence>(&mut self, components: Comps)
+ /// Queues up a entity to spawn at the end of the current tick, returning the [`Uid`]
+ /// that the entity will have.
+ pub fn spawn<Comps: ComponentSequence>(&mut self, components: Comps) -> Uid
{
+ let new_entity_uid = Uid::new_unique(UidKind::Entity);
+
self.action_queue.push(Action::Spawn(
- components.into_vec(),
- EventIds { ids: Comps::added_event_ids() },
+ new_entity_uid,
+ components.into_parts_array().into(),
));
+
+ new_entity_uid
}
/// Queues up despawning a entity at the end of the current tick.
@@ -34,6 +38,31 @@ impl<'world> Actions<'world>
{
debug_assert_eq!(entity_uid.kind(), UidKind::Entity);
+ let Some(world) = self.world else {
+ self.action_queue.push(Action::Despawn(entity_uid));
+ return;
+ };
+
+ let Some(ent) = world.get_entity(entity_uid) else {
+ tracing::warn!("Cannot entity that doesn't exist");
+ return;
+ };
+
+ // TODO: Submit all events with a single function call to reduce overhead
+ for comp_id in ent.component_ids() {
+ if comp_id.kind() == UidKind::Pair {
+ continue;
+ }
+
+ world.event_submitter().submit_event(
+ &Pair::builder()
+ .relation::<Removed>()
+ .target_id(comp_id)
+ .build(),
+ entity_uid,
+ );
+ }
+
self.action_queue.push(Action::Despawn(entity_uid));
}
@@ -50,27 +79,70 @@ impl<'world> Actions<'world>
self.action_queue.push(Action::AddComponents(
entity_uid,
- components.into_vec(),
- EventIds { ids: Comps::added_event_ids() },
+ components.into_parts_array().into(),
));
}
/// Queues up removing component(s) from a entity at the end of the current tick.
- pub fn remove_components<Comps>(&mut self, entity_uid: Uid)
- where
- Comps: ComponentSequence,
+ #[tracing::instrument(skip(self, component_ids))]
+ pub fn remove_components(
+ &mut self,
+ entity_uid: Uid,
+ component_ids: impl IntoIterator<Item = Uid>,
+ )
{
debug_assert_eq!(entity_uid.kind(), UidKind::Entity);
- if Comps::COUNT == 0 {
+ let mut component_ids = component_ids.into_iter().peekable();
+
+ if component_ids.peek().is_none() {
return;
}
- self.action_queue.push(Action::RemoveComponents(
- entity_uid,
- Comps::metadata().into_iter().collect(),
- EventIds { ids: Comps::removed_event_ids() },
- ));
+ let Some(world) = self.world else {
+ self.action_queue.push(Action::RemoveComponents(
+ entity_uid,
+ component_ids.collect(),
+ ));
+ return;
+ };
+
+ let Some(ent) = world.get_entity(entity_uid) else {
+ tracing::warn!("Cannot remove components from entity that doesn't exist");
+ return;
+ };
+
+ let component_ids = component_ids
+ .filter(|comp_id| ent.has_component(*comp_id))
+ .collect::<Vec<_>>();
+
+ if component_ids.is_empty() {
+ return;
+ }
+
+ // TODO: Submit all events with a single function call to reduce overhead
+ for comp_id in &component_ids {
+ if comp_id.kind() == UidKind::Pair {
+ continue;
+ }
+
+ world.event_submitter().submit_event(
+ &Pair::builder()
+ .relation::<Removed>()
+ .target_id(*comp_id)
+ .build(),
+ entity_uid,
+ );
+ }
+
+ self.action_queue
+ .push(Action::RemoveComponents(entity_uid, component_ids));
+ }
+
+ /// Queues up removing component(s) from a entity at the end of the current tick.
+ pub fn remove_comps<Ids: WithUidTuple>(&mut self, entity_uid: Uid)
+ {
+ self.remove_components(entity_uid, Ids::uids());
}
/// Stops the [`World`]. The world will finish the current tick and that tick will be
@@ -83,19 +155,22 @@ impl<'world> Actions<'world>
/// Returns a struct which holds a weak reference to the [`World`] that `Actions`
/// references and that can be used to aquire a new `Actions` instance if the
/// referenced [`World`] is still alive.
+ ///
+ /// # Panics
+ /// This function will panic if `self` was retrieved from a [`WeakRef`].
#[must_use]
pub fn to_weak_ref(&self) -> WeakRef
{
- WeakRef {
- action_queue: self.action_queue_weak.clone(),
- }
- }
+ let world = self.world.unwrap_or_else(|| {
+ panic!(
+ "This function cannot be called if the {} was retrieved from a {}",
+ type_name::<Self>(),
+ type_name::<WeakRef>()
+ )
+ });
- fn new(action_queue: &'world Arc<ActionQueue>) -> Self
- {
- Self {
- action_queue,
- action_queue_weak: Arc::downgrade(action_queue),
+ WeakRef {
+ action_queue: Rc::downgrade(&world.data.action_queue),
}
}
}
@@ -104,19 +179,12 @@ impl<'world> SystemParam<'world> for Actions<'world>
{
type Input = ();
- fn initialize<SystemImpl>(
- _system: &mut impl System<'world, SystemImpl>,
- _input: Self::Input,
- )
- {
- }
-
- fn new<SystemImpl>(
- _system: &'world impl System<'world, SystemImpl>,
- world: &'world World,
- ) -> Self
+ fn new(world: &'world World, _system_metadata: &SystemMetadata) -> Self
{
- Self::new(&world.data.action_queue)
+ Self {
+ action_queue: &world.data.action_queue,
+ world: Some(world),
+ }
}
}
@@ -146,32 +214,29 @@ 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<'_>
{
- Actions::new(&self.action_queue)
+ Actions {
+ action_queue: &self.action_queue,
+ world: None,
+ }
}
}
-#[derive(Debug)]
-pub(crate) struct EventIds
-{
- pub(crate) ids: Vec<Uid>,
-}
-
/// A action for a [`System`] to perform.
#[derive(Debug)]
pub(crate) enum Action
{
- Spawn(Vec<Box<dyn Component>>, EventIds),
+ Spawn(Uid, Vec<ComponentParts>),
Despawn(Uid),
- AddComponents(Uid, Vec<Box<dyn Component>>, EventIds),
- RemoveComponents(Uid, Vec<ComponentMetadata>, EventIds),
+ 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 354d206..0000000
--- a/ecs/src/archetype.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-use std::hash::{DefaultHasher, Hash, Hasher};
-
-use crate::component::{
- IsOptional as ComponentIsOptional,
- Metadata as ComponentMetadata,
-};
-
-/// 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
- {
- if components_metadata.as_ref().len() == 0 {
- return Self { hash: 0 };
- }
-
- 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"
- );
-
- let component_ids =
- components_metadata
- .as_ref()
- .iter()
- .filter_map(|component_metadata| {
- if component_metadata.is_optional == ComponentIsOptional::Yes {
- return None;
- }
-
- Some(component_metadata.id)
- });
-
- let mut hasher = DefaultHasher::new();
-
- for component_id in component_ids {
- component_id.hash(&mut hasher);
- }
-
- let hash = hasher.finish();
-
- assert_ne!(
- hash, 0,
- "Archetype ID 0 is reserved for a archetype with zero components"
- );
-
- Self { hash }
- }
-}
diff --git a/ecs/src/component.rs b/ecs/src/component.rs
index 35e5430..17b279b 100644
--- a/ecs/src/component.rs
+++ b/ecs/src/component.rs
@@ -1,88 +1,54 @@
use std::any::{type_name, Any};
use std::fmt::Debug;
+use std::ops::{Deref, DerefMut};
use seq_macro::seq;
-use crate::event::component::{
- Added as ComponentAddedEvent,
- Kind as ComponentEventKind,
- Removed as ComponentRemovedEvent,
+use crate::event::component::Changed;
+use crate::event::Submitter as EventSubmitter;
+use crate::lock::{
+ Error as LockError,
+ MappedReadGuard,
+ MappedWriteGuard,
+ ReadGuard,
+ WriteGuard,
};
-use crate::lock::{Error as LockError, Lock, ReadGuard, WriteGuard};
-use crate::system::{ComponentRef, ComponentRefMut, Input as SystemInput};
-use crate::type_name::TypeName;
+use crate::pair::Pair;
+use crate::system::Input as SystemInput;
use crate::uid::Uid;
use crate::util::Array;
-use crate::{EntityComponent, World};
+use crate::{EntityComponentRef, World};
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>: FromOptionalMut<'component> + FromLockedOptional<'component>
- where
- Self: Sized;
-
- type Ref<'component>: FromOptional<'component> + FromLockedOptional<'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;
-
- /// Returns the component UID of a component event for this component.
- fn get_event_uid(&self, event_kind: ComponentEventKind) -> 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>()
}
}
@@ -94,275 +60,154 @@ impl Debug for dyn Component
}
}
-impl TypeName for Box<dyn Component>
-{
- fn type_name(&self) -> &'static str
- {
- self.as_ref().type_name()
- }
-}
-
-impl<ComponentT> Component for Option<ComponentT>
-where
- ComponentT: Component,
-{
- type Component = ComponentT;
- type Ref<'component> = Option<ComponentRef<'component, ComponentT>>;
- type RefMut<'component> = Option<ComponentRefMut<'component, ComponentT>>;
-
- fn id() -> Uid
- {
- ComponentT::id()
- }
-
- fn self_id(&self) -> Uid
- {
- Self::id()
- }
-
- fn get_event_uid(&self, event_kind: ComponentEventKind) -> Uid
- {
- match event_kind {
- ComponentEventKind::Removed => ComponentRemovedEvent::<Self>::id(),
- }
- }
-
- fn as_any_mut(&mut self) -> &mut dyn Any
- {
- self
- }
-
- fn as_any(&self) -> &dyn Any
- {
- self
- }
-
- fn self_is_optional(&self) -> IsOptional
- {
- Self::is_optional()
- }
-
- fn is_optional() -> IsOptional
- {
- IsOptional::Yes
- }
-}
-
-impl<ComponentT> TypeName for Option<ComponentT>
-where
- ComponentT: Component,
-{
- fn type_name(&self) -> &'static str
- {
- type_name::<Self>()
- }
-}
-
-impl<ComponentT> SystemInput for Option<ComponentT> where ComponentT: Component {}
-
/// A sequence of components.
pub trait Sequence
{
/// The number of components in this component sequence.
const COUNT: usize;
- fn into_vec(self) -> Vec<Box<dyn Component>>;
-
- fn metadata() -> impl Array<Metadata>;
+ type PartsArray: Array<Parts>;
- fn added_event_ids() -> Vec<Uid>;
-
- fn removed_event_ids() -> Vec<Uid>;
+ fn into_parts_array(self) -> Self::PartsArray;
}
-/// A sequence of references (immutable or mutable) to components.
-pub trait RefSequence
+#[derive(Debug)]
+pub struct Handle<'a, DataT: 'static>
{
- type Handles<'component>;
-
- type Metadata: Array<Metadata>;
-
- fn metadata() -> Self::Metadata;
-
- fn from_components<'component>(
- components: &'component [EntityComponent],
- component_index_lookup: impl Fn(Uid) -> Option<usize>,
- world: &'component World,
- ) -> Self::Handles<'component>;
+ inner: MappedReadGuard<'a, DataT>,
}
-/// A mutable or immutable reference to a component.
-pub trait Ref
+impl<'comp, DataT: 'static> Handle<'comp, DataT>
{
- type Component: Component;
- type Handle<'component>: FromLockedOptional<'component>;
-}
+ /// 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::new(
+ entity_component_ref
+ .component()
+ .read_nonblock()
+ .map_err(AcquireLockError)?,
+ )
+ }
-impl<ComponentT> Ref for &ComponentT
-where
- ComponentT: Component,
-{
- type Component = ComponentT;
- type Handle<'component> = ComponentT::Ref<'component>;
+ fn new(inner: ReadGuard<'comp, Box<dyn Any>>) -> Result<Self, HandleError>
+ {
+ Ok(Self {
+ inner: ReadGuard::try_map(inner, |component| {
+ component.downcast_ref::<DataT>()
+ })
+ .map_err(|_| HandleError::IncorrectType)?,
+ })
+ }
}
-impl<ComponentT> Ref for &mut ComponentT
-where
- ComponentT: Component,
+impl<DataT: 'static> Deref for Handle<'_, DataT>
{
- type Component = ComponentT;
- type Handle<'component> = ComponentT::RefMut<'component>;
+ type Target = DataT;
+
+ fn deref(&self) -> &Self::Target
+ {
+ &self.inner
+ }
}
-/// [`Component`] metadata.
-#[derive(Debug, Clone)]
-#[non_exhaustive]
-pub struct Metadata
+#[derive(Debug)]
+pub struct HandleMut<'a, DataT: 'static>
{
- pub id: Uid,
- pub is_optional: IsOptional,
+ entity_component_ref: EntityComponentRef<'a>,
+ inner: MappedWriteGuard<'a, DataT>,
+ event_submitter: EventSubmitter<'a>,
}
-impl Metadata
+impl<'comp, DataT: 'static> HandleMut<'comp, DataT>
{
- pub fn get<ComponentT: Component + ?Sized>(component: &ComponentT) -> Self
+ /// 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>,
+ world: &'comp World,
+ ) -> Result<Self, HandleError>
{
- Self {
- id: component.self_id(),
- is_optional: component.self_is_optional(),
- }
+ let inner = entity_component_ref
+ .component()
+ .write_nonblock()
+ .map_err(AcquireLockError)?;
+
+ Ok(Self {
+ entity_component_ref: entity_component_ref.clone(),
+ inner: WriteGuard::try_map(inner, |component| {
+ component.downcast_mut::<DataT>()
+ })
+ .map_err(|_| HandleError::IncorrectType)?,
+ event_submitter: world.event_submitter(),
+ })
}
- pub fn of<ComponentT: Component>() -> Self
+ pub fn set_changed(&self)
{
- Self {
- id: ComponentT::id(),
- is_optional: ComponentT::is_optional(),
- }
+ self.event_submitter.submit_event(
+ &Pair::builder()
+ .relation::<Changed>()
+ .target_id(self.entity_component_ref.id())
+ .build(),
+ self.entity_component_ref.entity_id(),
+ );
}
}
-/// Whether or not a `Component` is optional.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum IsOptional
+impl<DataT: 'static> Deref for HandleMut<'_, DataT>
{
- Yes,
- No,
-}
+ type Target = DataT;
-impl From<bool> for IsOptional
-{
- fn from(is_optional: bool) -> Self
+ fn deref(&self) -> &Self::Target
{
- if is_optional {
- return IsOptional::Yes;
- }
-
- IsOptional::No
+ &self.inner
}
}
-pub trait FromOptionalMut<'comp>
+impl<DataT: 'static> DerefMut for HandleMut<'_, DataT>
{
- fn from_optional_mut_component(
- optional_component: Option<WriteGuard<'comp, Box<dyn Component>>>,
- world: &'comp World,
- ) -> Self;
+ fn deref_mut(&mut self) -> &mut Self::Target
+ {
+ &mut self.inner
+ }
}
-pub trait FromOptional<'comp>
+#[derive(Debug, thiserror::Error)]
+pub enum HandleError
{
- fn from_optional_component(
- optional_component: Option<ReadGuard<'comp, Box<dyn Component>>>,
- world: &'comp World,
- ) -> Self;
-}
+ #[error(transparent)]
+ AcquireLockFailed(#[from] AcquireLockError),
-pub trait FromLockedOptional<'comp>: Sized
-{
- fn from_locked_optional_component(
- optional_component: Option<&'comp Lock<Box<dyn Component>>>,
- world: &'comp World,
- ) -> Result<Self, LockError>;
+ #[error("Incorrect component type")]
+ IncorrectType,
}
+#[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<#(Comp~I: Component,)*> Sequence for (#(Comp~I,)*)
- where
- #(for<'comp> Comp~I::RefMut<'comp>: FromOptionalMut<'comp>,)*
- #(for<'comp> Comp~I::Ref<'comp>: FromOptional<'comp>,)*
+ impl<#(IntoCompParts~I: IntoParts,)*> Sequence for (#(IntoCompParts~I,)*)
{
const COUNT: usize = $c + 1;
- fn into_vec(self) -> Vec<Box<dyn Component>>
- {
- Vec::from_iter([#(Box::new(self.I) as Box<dyn Component>,)*])
- }
-
- fn metadata() -> impl Array<Metadata>
- {
- [
- #(
- Metadata {
- id: Comp~I::id(),
- is_optional: Comp~I::is_optional(),
- },
- )*
- ]
- }
-
- fn added_event_ids() -> Vec<Uid>
- {
- vec![
- #(ComponentAddedEvent::<Comp~I>::id(),)*
- ]
- }
+ type PartsArray = [Parts; $c + 1];
- fn removed_event_ids() -> Vec<Uid>
+ fn into_parts_array(self) -> Self::PartsArray
{
- vec![
- #(ComponentRemovedEvent::<Comp~I>::id(),)*
- ]
- }
- }
-
- impl<#(CompRef~I: Ref,)*> RefSequence for (#(CompRef~I,)*)
- {
- type Handles<'component> = (#(CompRef~I::Handle<'component>,)*);
-
- type Metadata = [Metadata; $c + 1];
-
- fn metadata() -> Self::Metadata
- {
- [#(
- Metadata {
- id: CompRef~I::Component::id(),
- is_optional: CompRef~I::Component::is_optional(),
- },
- )*]
- }
-
- fn from_components<'component>(
- components: &'component [EntityComponent],
- component_index_lookup: impl Fn(Uid) -> Option<usize>,
- world: &'component World,
- ) -> Self::Handles<'component>
- {
- (#(
- CompRef~I::Handle::from_locked_optional_component(
- component_index_lookup(CompRef~I::Component::id())
- .and_then(|component_index| components.get(component_index))
- .map(|component| &component.component),
- world,
- ).unwrap_or_else(|err| {
- panic!(
- "Taking component {} lock failed: {err}",
- type_name::<CompRef~I::Component>()
- );
- }),
- )*)
+ [#({
+ self.I.into_parts()
+ },)*]
}
}
});
@@ -375,45 +220,105 @@ seq!(C in 0..=16 {
impl Sequence for ()
{
+ type PartsArray = [Parts; 0];
+
const COUNT: usize = 0;
- fn into_vec(self) -> Vec<Box<dyn Component>>
+ 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
{
- Vec::new()
+ Parts::builder()
+ .name(type_name::<Self>())
+ .build(Self::id(), self)
}
+}
+
+/// The parts of a component.
+#[derive(Debug)]
+#[non_exhaustive]
+pub struct Parts
+{
+ id: Uid,
+ name: &'static str,
+ data: Box<dyn Any>,
+}
- fn metadata() -> impl Array<Metadata>
+impl Parts
+{
+ #[must_use]
+ pub fn id(&self) -> Uid
{
- []
+ self.id
}
- fn added_event_ids() -> Vec<Uid>
+ #[must_use]
+ pub fn name(&self) -> &'static str
{
- Vec::new()
+ self.name
}
- fn removed_event_ids() -> Vec<Uid>
+ #[must_use]
+ pub fn builder() -> PartsBuilder
{
- Vec::new()
+ PartsBuilder::default()
+ }
+
+ pub(crate) fn into_data(self) -> Box<dyn Any>
+ {
+ self.data
}
}
-impl RefSequence for ()
+#[derive(Debug)]
+pub struct PartsBuilder
{
- type Handles<'component> = ();
- type Metadata = [Metadata; 0];
+ name: &'static str,
+}
- fn metadata() -> Self::Metadata
+impl PartsBuilder
+{
+ #[must_use]
+ pub fn name(mut self, name: &'static str) -> Self
{
- []
+ self.name = name;
+ self
}
- fn from_components<'component>(
- _components: &'component [EntityComponent],
- _component_index_lookup: impl Fn(Uid) -> Option<usize>,
- _world: &'component World,
- ) -> Self::Handles<'component>
+ #[must_use]
+ pub fn build<Data: 'static>(self, id: Uid, data: Data) -> Parts
+ {
+ Parts {
+ id,
+ name: self.name,
+ data: Box::new(data),
+ }
+ }
+
+ #[must_use]
+ pub fn build_with_any_data(self, id: Uid, data: Box<dyn Any>) -> Parts
+ {
+ Parts { id, name: self.name, data }
+ }
+}
+
+impl Default for PartsBuilder
+{
+ fn default() -> Self
{
- ()
+ Self { name: "(unspecified)" }
}
}
diff --git a/ecs/src/component/local.rs b/ecs/src/component/local.rs
index ad79f6f..b19a30b 100644
--- a/ecs/src/component/local.rs
+++ b/ecs/src/component/local.rs
@@ -1,14 +1,24 @@
+use std::any::type_name;
use std::ops::{Deref, DerefMut};
-use crate::component::Component;
-use crate::system::{ComponentRefMut, Param as SystemParam, System};
+use ecs_macros::Component;
+
+use crate::component::{
+ Component,
+ HandleMut as ComponentHandleMut,
+ IntoParts as _,
+ Parts as ComponentParts,
+};
+use crate::pair::Pair;
+use crate::system::initializable::Param as InitializableParam;
+use crate::system::{Metadata as SystemMetadata, Param as SystemParam};
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>,
}
impl<'world, LocalComponent> SystemParam<'world> for Local<'world, LocalComponent>
@@ -17,28 +27,53 @@ where
{
type Input = LocalComponent;
- fn initialize<SystemImpl>(
- system: &mut impl System<'world, SystemImpl>,
- input: Self::Input,
- )
+ fn new(world: &'world World, system_metadata: &SystemMetadata) -> Self
{
- system.set_local_component(input);
- }
+ let Some(system_ent) = world.get_entity(system_metadata.ent_id) else {
+ panic!(
+ "System entity with ID {} does not exist",
+ system_metadata.ent_id
+ );
+ };
- fn new<SystemImpl>(
- system: &'world impl System<'world, SystemImpl>,
- _world: &'world World,
- ) -> Self
- {
- let local_component = system
- .get_local_component_mut::<LocalComponent>()
- .expect("Local component is uninitialized");
+ let Some(local_component) = system_ent.get_with_id_mut::<LocalComponent>(
+ Pair::builder()
+ .relation::<IsLocalComponent>()
+ .target::<LocalComponent>()
+ .build()
+ .id(),
+ ) else {
+ panic!(
+ "Local component {} of system with ID {} is uninitialized",
+ type_name::<LocalComponent>(),
+ system_metadata.ent_id
+ );
+ };
Self { local_component }
}
}
-impl<'world, LocalComponent> Deref for Local<'world, LocalComponent>
+impl<'world, LocalComponent, SystemT> InitializableParam<'world, SystemT>
+ for Local<'world, LocalComponent>
+where
+ LocalComponent: Component,
+ SystemT: SystemWithLocalComponents,
+ Self: SystemParam<'world, Input = LocalComponent>,
+{
+ fn initialize(system: &mut SystemT, input: Self::Input)
+ {
+ system.add_local_component(
+ Pair::builder()
+ .relation::<IsLocalComponent>()
+ .target_as_data(input)
+ .build()
+ .into_parts(),
+ );
+ }
+}
+
+impl<LocalComponent> Deref for Local<'_, LocalComponent>
where
LocalComponent: Component,
{
@@ -50,7 +85,7 @@ where
}
}
-impl<'world, LocalComponent> DerefMut for Local<'world, LocalComponent>
+impl<LocalComponent> DerefMut for Local<'_, LocalComponent>
where
LocalComponent: Component,
{
@@ -59,3 +94,11 @@ where
&mut self.local_component
}
}
+
+pub trait SystemWithLocalComponents
+{
+ fn add_local_component(&mut self, component_parts: ComponentParts);
+}
+
+#[derive(Component)]
+struct IsLocalComponent;
diff --git a/ecs/src/component/storage.rs b/ecs/src/component/storage.rs
index 5ce587f..a8711c5 100644
--- a/ecs/src/component/storage.rs
+++ b/ecs/src/component/storage.rs
@@ -1,666 +1,786 @@
-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::slice::Iter as SliceIter;
-use std::vec::IntoIter as OwnedVecIter;
+use std::vec::IntoIter as VecIntoIter;
-use hashbrown::{HashMap, HashSet};
+use hashbrown::HashMap;
-use crate::archetype::Id as ArchetypeId;
-use crate::component::{
- Component,
- IsOptional as ComponentIsOptional,
- Metadata as ComponentMetadata,
+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::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 iter_archetypes_with_comps(
- &self,
- comp_metadata: impl AsRef<[ComponentMetadata]>,
- ) -> ArchetypeRefIter<'_>
+ fn excluded_contains(&self, comp_id: Uid) -> bool
{
- debug_assert!(comp_metadata
- .as_ref()
- .is_sorted_by_key(|metadata| metadata.id));
+ let comp_id_kind = comp_id.kind();
- let archetype_id = ArchetypeId::from_components_metadata(&comp_metadata);
+ debug_assert!(
+ comp_id_kind == UidKind::Component
+ || (comp_id_kind == UidKind::Pair
+ && comp_id.target_component() != Uid::wildcard())
+ );
- if !self.archetype_lookup.borrow().contains_key(&archetype_id) {
- self.archetype_lookup.borrow_mut().insert(
- archetype_id,
- self.create_populated_archetype_lookup_entry(comp_metadata.as_ref()),
- );
+ let is_found = self.excluded_components.binary_search(&comp_id).is_ok();
+
+ 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()
+ });
}
- self.iter_archetypes_by_lookup(archetype_id)
+ is_found
}
- pub fn get_entity_archetype(&self, entity_uid: Uid) -> Option<&Archetype>
+ fn contains_conflicting(&self) -> bool
{
- let archetype_id = self.entity_archetype_lookup.get(&entity_uid)?;
-
- let archetype_index = self.get_archetype_index_by_id(*archetype_id)?;
-
- self.archetypes.get(archetype_index)
+ self.excluded_components.iter().any(|excluded_comp_id| {
+ self.required_components
+ .binary_search(excluded_comp_id)
+ .is_ok()
+ })
}
- pub fn remove_entity(&mut self, entity_uid: Uid)
+ fn archetype_contains_all_required(&self, archetype: &Archetype) -> bool
{
- let Some(archetype_id) = self.entity_archetype_lookup.get(&entity_uid) else {
- return;
- };
+ self.required_components
+ .iter()
+ .all(|comp_id| archetype.contains_matching_component(*comp_id))
+ }
+}
- let Some(archetype_index) = self.get_archetype_index_by_id(*archetype_id) else {
- return;
- };
+#[derive(Debug, Default)]
+pub struct Storage
+{
+ graph: Graph,
+ entity_archetype_lookup: HashMap<Uid, ArchetypeId>,
+ imaginary_archetypes: RefCell<Vec<ImaginaryArchetype>>,
+}
- let Some(archetype) = self.archetypes.get_mut(archetype_index) else {
- return;
+impl Storage
+{
+ pub fn search_archetypes<'search_terms>(
+ &self,
+ search_terms: ArchetypeSearchTerms<'search_terms>,
+ ) -> ArchetypeRefIter<'_, 'search_terms>
+ {
+ 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 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,
+ };
};
- archetype.take_entity(entity_uid);
+ ArchetypeRefIter {
+ storage: self,
+ pre_iter: Either::A([archetype_id].into_iter()),
+ dfs_iter: add_edge_recursive_iter,
+ search_terms,
+ }
+ }
- self.entity_archetype_lookup.remove(&entity_uid);
+ pub fn get_archetype_by_id(&self, id: ArchetypeId) -> Option<&Archetype>
+ {
+ Some(self.graph.get_node_by_id(id)?.archetype())
}
- #[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 create_entity(&mut self, uid: Uid) -> Result<(), Error>
{
- if self.entity_archetype_lookup.contains_key(&entity_uid) {
- return Err(Error::EntityAlreadyExists(entity_uid));
+ 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().fold(
- String::with_capacity(components.len() * 25),
- |mut acc, component| {
- acc.extend([", ", component.type_name()]);
- acc
- }
- )[2..]
- );
+ 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, &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");
- self.entity_archetype_lookup
- .insert(entity_uid, archetype_id);
+ self.entity_archetype_lookup.remove(&entity_uid);
- Ok((archetype_id, entity_uid))
+ Ok(entity)
}
- pub fn add_components_to_entity(
- &mut self,
- entity_uid: Uid,
- components: Vec<Box<dyn Component>>,
- ) -> Option<()>
+ pub fn get_entity_archetype(&self, entity_uid: Uid) -> Option<&Archetype>
{
let archetype_id = self.entity_archetype_lookup.get(&entity_uid)?;
- let archetype_index = self.get_archetype_index_by_id(*archetype_id)?;
-
- let archetype = self.archetypes.get_mut(archetype_index)?;
-
- 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 entity = archetype.take_entity(entity_uid)?;
-
- self.entity_archetype_lookup.remove(&entity_uid);
-
- 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");
-
- Some(())
+ self.get_archetype_by_id(*archetype_id)
}
- pub fn remove_components_from_entity(
+ pub fn add_entity_component(
&mut self,
entity_uid: Uid,
- component_ids: impl IntoIterator<Item = Uid>,
- ) -> 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 archetype_index = self.get_archetype_index_by_id(*archetype_id)?;
+ 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)?;
+ 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 entity = archetype.take_entity(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 component_ids_set = component_ids.into_iter().collect::<HashSet<_>>();
+ add_edge_id
+ } else {
+ let archetype_node = self
+ .graph
+ .get_node_by_id(archetype_id)
+ .expect("Archetype should exist");
- self.entity_archetype_lookup.remove(&entity_uid);
+ let (add_edge_id, add_edge_comp_ids) =
+ archetype_node.make_add_edge(component_id);
- 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");
+ if !self.graph.contains_archetype(add_edge_id) {
+ self.graph.create_node(add_edge_id, &add_edge_comp_ids);
+ }
- Some(())
- }
+ add_edge_id
+ };
- fn populate_matching_archetype_lookup_entries(
- &mut self,
- comp_ids_set: &HashSet<Uid>,
- archetype_index: usize,
- )
- {
- let mut archetype_lookup = self.archetype_lookup.borrow_mut();
+ 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_name),
+ add_edge_archetype,
+ );
- for (_, lookup_entry) in archetype_lookup.iter_mut() {
- if &lookup_entry.component_ids == comp_ids_set {
- continue;
- }
+ add_edge_archetype.push_entity(entity);
- // There shouldn't be duplicate archetype indices in the lookup entry
- if lookup_entry.archetype_indices.contains(&archetype_index) {
- continue;
- }
+ self.entity_archetype_lookup
+ .insert(entity_uid, add_edge_archetype_id);
- if lookup_entry.component_ids.is_subset(comp_ids_set) {
- lookup_entry.archetype_indices.push(archetype_index);
- }
- }
+ Ok(())
}
- fn get_or_create_archetype(
+ pub fn remove_entity_component(
&mut self,
- archetype_id: ArchetypeId,
- components: &[Box<dyn Component>],
- ) -> usize
+ entity_uid: Uid,
+ component_id: Uid,
+ ) -> Result<(), Error>
{
- let mut archetype_lookup = self.archetype_lookup.borrow_mut();
+ let Some(archetype_id) = self.entity_archetype_lookup.get(&entity_uid) else {
+ return Err(Error::EntityDoesNotExist(entity_uid));
+ };
- if !archetype_lookup.contains_key(&archetype_id) {
- self.archetypes.push(Archetype::new(
- components.iter().map(|component| component.self_id()),
- ));
+ 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 lookup_entry = archetype_lookup.entry(archetype_id).or_insert_with(|| {
- self.create_populated_archetype_lookup_entry(
- components
- .iter()
- .map(|component| ComponentMetadata::get(&**component)),
- )
- });
+ 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);
+ }
- // SAFETY: Above, we push a archetype index if archetype_indices is empty so this
- // cannot fail
- unsafe { *lookup_entry.archetype_indices.first().unwrap_unchecked() }
- }
+ remove_edge_id
+ });
- fn get_archetype_index_by_id(&self, archetype_id: ArchetypeId) -> Option<usize>
- {
- let archetype_lookup = self.archetype_lookup.borrow();
+ let archetype_node = self
+ .graph
+ .get_node_by_id_mut(archetype_id)
+ .expect("Archetype should exist");
- let archetype_lookup_entry = archetype_lookup.get(&archetype_id)?;
+ let mut entity = archetype_node
+ .archetype_mut()
+ .remove_entity(entity_uid)
+ .expect("Entity should exist in archetype");
- let index = *archetype_lookup_entry
- .archetype_indices
- .first()
- .expect("No archetype indices in archetype lookup entry");
+ entity.remove_component(component_id, archetype_node.archetype());
- debug_assert!(
- self.archetypes.get(index).is_some_and(|archetype| archetype
- .component_ids_is(&archetype_lookup_entry.component_ids)),
- "Archetype components is not exact match"
- );
+ self.graph
+ .get_node_by_id_mut(remove_edge_id)
+ .expect("Remove edge archetype should exist")
+ .archetype_mut()
+ .push_entity(entity);
+
+ self.entity_archetype_lookup
+ .insert(entity_uid, remove_edge_id);
- Some(index)
+ Ok(())
}
- fn iter_archetypes_by_lookup(&self, archetype_id: ArchetypeId)
- -> ArchetypeRefIter<'_>
+ pub fn create_imaginary_archetypes(&mut self)
{
- 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();
+ for imaginary_archetype in self.imaginary_archetypes.get_mut().drain(..) {
+ if self.graph.contains_archetype(imaginary_archetype.id) {
+ continue;
+ }
- ArchetypeRefIter {
- indices: archetype_indices.into_iter(),
- archetypes: &self.archetypes,
+ self.graph
+ .create_node(imaginary_archetype.id, &imaginary_archetype.component_ids);
}
}
- fn create_populated_archetype_lookup_entry<CompMetadataIter>(
+ fn find_all_archetype_with_comps(
&self,
- comp_metadata_iter: CompMetadataIter,
- ) -> ArchetypeLookupEntry
- where
- CompMetadataIter: IntoIterator<Item: Borrow<ComponentMetadata>>,
+ search_terms: &ArchetypeSearchTerms<'_>,
+ ) -> Vec<ArchetypeId>
{
- let comp_ids_set = create_non_opt_component_id_set(comp_metadata_iter);
+ 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 exact_matching_archetype_index = None;
+ let mut found = Vec::<ArchetypeId>::new();
- let matching_archetype_indices = self
- .archetypes
- .iter()
- .enumerate()
- .filter_map(|(index, archetype)| {
- if archetype.component_ids_is(&comp_ids_set) {
- exact_matching_archetype_index = Some(index);
+ 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;
+ };
- return None;
- }
+ if search_terms.excluded_contains(add_edge_component_id) {
+ search_iter.pop();
+ continue;
+ }
- if archetype.component_ids_is_superset(&comp_ids_set) {
- return Some(index);
- }
+ let node = self
+ .graph
+ .get_node_by_id(node_id)
+ .expect("Graph node found through DFS doesn't exist");
- None
- })
- .collect::<Vec<_>>();
-
- ArchetypeLookupEntry {
- component_ids: comp_ids_set,
- archetype_indices: exact_matching_archetype_index
- .into_iter()
- .chain(matching_archetype_indices)
- .collect(),
- }
- }
-}
+ if node.archetype().component_cnt() < search_terms.required_components.len() {
+ continue;
+ }
-/// Component storage error
-#[derive(Debug, Clone, thiserror::Error)]
-pub enum Error
-{
- #[error("Entity already exists")]
- EntityAlreadyExists(Uid),
-}
+ if !search_terms.archetype_contains_all_required(node.archetype()) {
+ continue;
+ }
-impl TypeName for Storage
-{
- fn type_name(&self) -> &'static str
- {
- type_name::<Self>()
- }
-}
+ found.push(node.archetype().id());
-#[derive(Debug)]
-struct ArchetypeLookupEntry
-{
- component_ids: HashSet<Uid>,
- archetype_indices: Vec<usize>,
-}
+ search_iter.pop();
+ }
-#[derive(Debug)]
-pub struct Archetype
-{
- component_ids: HashMap<Uid, usize>,
- entity_lookup: HashMap<Uid, usize>,
- entity_uid_lookup: Vec<Uid>,
- entities: Vec<ArchetypeEntity>,
+ found
+ }
}
-impl Archetype
+#[cfg(feature = "vizoxide")]
+impl Storage
{
- fn new(component_ids: impl IntoIterator<Item = Uid>) -> Self
+ pub fn create_vizoxide_archetype_graph(
+ &self,
+ graph_name: impl AsRef<str>,
+ params: VizoxideArchetypeGraphParams,
+ ) -> Result<vizoxide::Graph, vizoxide::GraphvizError>
{
- Self {
- component_ids: component_ids
- .into_iter()
- .enumerate()
- .map(|(index, component_id)| (component_id, index))
- .collect(),
- entity_lookup: HashMap::new(),
- entity_uid_lookup: Vec::new(),
- entities: Vec::new(),
- }
- }
+ 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);
+ }
- pub fn component_ids_is_superset(&self, other_component_ids: &HashSet<Uid>) -> bool
- {
- if other_component_ids.len() <= self.component_ids.len() {
- other_component_ids
- .iter()
- .all(|v| self.component_ids.contains_key(v))
- } else {
- false
- }
- }
+ 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,
+ &params,
+ )?;
+
+ 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()?;
+ }
- pub fn component_ids_is(&self, other_component_ids: &HashSet<Uid>) -> bool
- {
- 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(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,
+ &params,
+ )?;
+
+ 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()?;
+ }
+ }
}
- }
- pub fn get_entity(&self, entity_uid: Uid) -> Option<&ArchetypeEntity>
- {
- let entity_index = *self.entity_lookup.get(&entity_uid)?;
+ drop(viz_node_lookup);
- self.entities.get(entity_index)
+ Ok(viz_graph)
}
- pub fn entities(&self) -> EntityIter<'_>
- {
- EntityIter { iter: self.entities.iter() }
- }
-
- pub fn entity_cnt(&self) -> usize
- {
- self.entities.len()
- }
-
- 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(),
- });
-
- let index = self.entities.len() - 1;
-
- self.entity_lookup.insert(entity_uid, index);
-
- self.entity_uid_lookup.push(entity_uid);
- }
-
- pub fn take_entity(&mut self, entity_uid: Uid) -> Option<ArchetypeEntity>
+ 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 entity_index = self.entity_lookup.remove(&entity_uid)?;
-
- let last_entity_uid = *self
- .entity_uid_lookup
- .get(self.entities.len() - 1)
- .expect("Entity UID lookup contains too few entity UIDS");
-
- // 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_lookup.insert(last_entity_uid, entity_index);
-
- self.entity_uid_lookup.swap_remove(entity_index);
-
- Some(removed_entity)
+ 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()
+ }
+ }
}
}
-#[derive(Debug)]
-pub struct ArchetypeEntity
+#[cfg(feature = "vizoxide")]
+pub struct VizoxideArchetypeGraphParams
{
- uid: Uid,
- components: Vec<EntityComponent>,
+ 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 ArchetypeEntity
+#[cfg(feature = "vizoxide")]
+#[derive(Debug, Clone)]
+pub struct ArchetypeMetadata
{
- pub fn uid(&self) -> Uid
- {
- self.uid
- }
-
- pub fn components(&self) -> &[EntityComponent]
- {
- &self.components
- }
+ pub is_imaginary: bool,
+}
- pub fn get_component(&self, index: usize) -> Option<&EntityComponent>
- {
- self.components.get(index)
- }
+#[cfg(feature = "vizoxide")]
+#[derive(Debug, Clone, Copy)]
+pub enum VizoxideArchetypeGraphEdgeKind
+{
+ Add,
+ Remove,
}
#[derive(Debug)]
-pub struct ArchetypeRefIter<'component_storage>
+pub struct ArchetypeRefIter<'storage, 'search_terms>
{
- indices: OwnedVecIter<usize>,
- archetypes: &'component_storage [Archetype],
+ storage: &'storage Storage,
+ pre_iter: Either<ArrayIter<ArchetypeId, 1>, VecIntoIter<ArchetypeId>>,
+ dfs_iter: ArchetypeAddEdgeDfsIter<'storage>,
+ search_terms: ArchetypeSearchTerms<'search_terms>,
}
-impl<'component_storage> Iterator for ArchetypeRefIter<'component_storage>
+impl<'component_storage> Iterator for ArchetypeRefIter<'component_storage, '_>
{
type Item = &'component_storage Archetype;
fn next(&mut self) -> Option<Self::Item>
{
- let archetype_index = self.indices.next()?;
+ 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"),
+ );
+ }
+
+ 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_part_pt_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(
- self.archetypes
- .get(archetype_index)
- .expect("Archetype index in archetype lookup entry was not found"),
+ self.storage
+ .get_archetype_by_id(archetype_id)
+ .expect("Archetype should exist"),
)
}
}
-#[derive(Debug)]
-pub struct EntityIter<'archetype>
+impl ArchetypeRefIter<'_, '_>
{
- iter: SliceIter<'archetype, ArchetypeEntity>,
-}
+ fn find_edges_of_imaginary_archetype(
+ &self,
+ imaginary_archetype_comps: &[Uid],
+ ) -> Vec<(Uid, ArchetypeEdges)>
+ {
+ 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();
-impl<'archetype> Iterator for EntityIter<'archetype>
-{
- type Item = &'archetype ArchetypeEntity;
+ if found_archetype.component_cnt() < imaginary_archetype_comps.len() + 1 {
+ return None;
+ }
- fn next(&mut self) -> Option<Self::Item>
- {
- self.iter.next()
- }
-}
+ 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");
-fn create_non_opt_component_id_set<Item>(
- component_metadata_iter: impl IntoIterator<Item = Item>,
-) -> HashSet<Uid>
-where
- Item: Borrow<ComponentMetadata>,
-{
- component_metadata_iter
- .into_iter()
- .filter_map(|item| {
- let component_metadata = item.borrow();
+ let mut add_edge_comp_ids = imaginary_archetype_comps.to_vec();
- if component_metadata.is_optional == ComponentIsOptional::Yes {
- return None;
- }
+ add_edge_comp_ids.insert_at_part_pt_by_key(unique_comp_id, |id| id);
- Some(component_metadata.id)
- })
- .collect::<HashSet<_>>()
+ let add_edge = ArchetypeId::new(&add_edge_comp_ids);
+
+ Some((
+ unique_comp_id,
+ ArchetypeEdges { add: Some(add_edge), remove: None },
+ ))
+ })
+ .collect::<Vec<_>>()
+ }
}
-#[cfg(test)]
-mod tests
+#[derive(Debug, thiserror::Error)]
+pub enum Error
{
+ #[error("Entity with ID {0:?} already exists")]
+ EntityAlreadyExists(Uid),
- 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::uid::{Kind as UidKind, Uid};
+ #[error("Entity with ID {0:?} does not exist")]
+ EntityDoesNotExist(Uid),
- #[derive(Debug, Component)]
- struct HealthPotion
+ #[error("Entity with ID {entity:?} already has component with ID {component:?}")]
+ ComponentAlreadyInEntity
{
- _hp_restoration: u32,
- }
+ entity: Uid, component: Uid
+ },
- #[derive(Debug, Component)]
- struct Hookshot
+ #[error("Entity with ID {entity:?} does not have component with ID {component:?}")]
+ ComponentNotFoundInEntity
{
- _range: u32,
- }
-
- #[derive(Debug, Component)]
- struct DekuNut
- {
- _throwing_damage: u32,
- }
+ entity: Uid, component: Uid
+ },
+}
- #[derive(Debug, Component)]
- struct Bow
- {
- _damage: u32,
- }
+#[derive(Debug)]
+struct ImaginaryArchetype
+{
+ id: ArchetypeId,
+ component_ids: Vec<Uid>,
+}
- #[derive(Debug, Component)]
- struct IronBoots;
+#[cfg(test)]
+mod tests
+{
+ use crate::component::storage::archetype::Id as ArchetypeId;
+ use crate::component::storage::Storage;
+ use crate::uid::{Kind as UidKind, Uid};
#[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");
+ let mut new_storage = Storage::default();
- assert_eq!(archetype.component_ids.len(), 2);
+ let uid = Uid::new_unique(UidKind::Entity);
- // One entity
- assert_eq!(archetype.entities.len(), 1);
+ new_storage.create_entity(uid).expect("Expected Ok");
- let entity_components = archetype
- .entities
- .first()
- .expect("Expected a entity in archetype");
+ 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!(entity_components.components.len(), 2);
+ assert_eq!(archetype_node.archetype().component_cnt(), 0);
+ assert_eq!(archetype_node.archetype().entity_cnt(), 1);
- assert_eq!(component_storage.archetype_lookup.borrow().len(), 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..d96632e
--- /dev/null
+++ b/ecs/src/component/storage/archetype.rs
@@ -0,0 +1,374 @@
+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
+{
+ component: Lock<Box<dyn Any>>,
+}
+
+impl EntityComponent
+{
+ pub fn new(component: Box<dyn Any>, component_name: &'static str) -> Self
+ {
+ Self {
+ component: Lock::new(component, component_name),
+ }
+ }
+
+ 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 {
+ assert!(
+ prev_component_id.is_none_or(|prev_comp_id| *comp_id >= prev_comp_id),
+ "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..76200f9
--- /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..ad9f179 100644
--- a/ecs/src/entity.rs
+++ b/ecs/src/entity.rs
@@ -1,32 +1,295 @@
-use linkme::distributed_slice;
+use std::any::type_name;
+use std::ops::Deref;
+use std::sync::LazyLock;
-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::pair::{
+ ComponentOrWildcard,
+ MultipleWithWildcard as PairMultipleWithWildcard,
+ Pair,
+ WithWildcard as PairWithWildcard,
+};
+use crate::uid::{Kind as UidKind, Uid};
+use crate::{EntityComponentRef, World};
+
+pub mod obtainer;
+
+/// A handle to a entity.
+#[derive(Debug, Clone)]
+pub struct Handle<'a>
+{
+ archetype: &'a Archetype,
+ entity: &'a ArchetypeEntity,
+ world: &'a World,
+}
+
+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<'a, 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!(
+ "Creating handle to component {} 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<'a, ComponentT>>
+ {
+ assert_eq!(ComponentT::id().kind(), UidKind::Component);
+
+ let component = self.get_matching_components(ComponentT::id()).next()?;
+
+ Some(
+ ComponentHandleMut::from_entity_component_ref(&component, self.world)
+ .unwrap_or_else(|err| {
+ panic!(
+ "Creating handle to component {} failed: {err}",
+ type_name::<ComponentT>()
+ );
+ }),
+ )
+ }
+
+ /// Returns a reference to the component with the ID `id` in this entity.
+ /// `None` is returned if the component isn't found.
+ ///
+ /// # Panics
+ /// Will panic if:
+ /// - The ID is not a component/pair ID
+ /// - The component is borrowed mutably elsewhere
+ /// - The component type is incorrect
+ #[must_use]
+ pub fn get_with_id<ComponentDataT: 'static>(
+ &self,
+ id: Uid,
+ ) -> Option<ComponentHandle<'a, ComponentDataT>>
+ {
+ assert!(
+ matches!(id.kind(), UidKind::Component | UidKind::Pair),
+ "ID {id:?} is not a component/pair ID"
+ );
+
+ let component = self.get_matching_components(id).next()?;
+
+ Some(
+ ComponentHandle::from_entity_component_ref(&component).unwrap_or_else(
+ |err| {
+ panic!(
+ "Creating handle to component {} failed: {err}",
+ type_name::<ComponentDataT>()
+ );
+ },
+ ),
+ )
+ }
+
+ /// Returns a mutable reference to the component with the ID `id` in this entity.
+ /// `None` is returned if the component isn't found.
+ ///
+ /// # Panics
+ /// Will panic if:
+ /// - The ID is not a component/pair ID
+ /// - The component is borrowed elsewhere
+ /// - The component type is incorrect
+ #[must_use]
+ pub fn get_with_id_mut<ComponentDataT: 'static>(
+ &self,
+ id: Uid,
+ ) -> Option<ComponentHandleMut<'a, ComponentDataT>>
+ {
+ assert!(
+ matches!(id.kind(), UidKind::Component | UidKind::Pair),
+ "ID {id:?} is not a component/pair ID"
+ );
+
+ let component = self.get_matching_components(id).next()?;
+
+ Some(
+ ComponentHandleMut::from_entity_component_ref(&component, self.world)
+ .unwrap_or_else(|err| {
+ panic!(
+ "Creating handle to component {} failed: {err}",
+ type_name::<ComponentDataT>()
+ );
+ }),
+ )
+ }
+
+ #[must_use]
+ pub fn get_first_wildcard_pair_match<Relation, Target>(
+ &self,
+ ) -> Option<PairWithWildcard<'a, Relation, Target>>
+ where
+ Relation: ComponentOrWildcard,
+ Target: ComponentOrWildcard,
+ {
+ let mut matching_comps = self.get_matching_components(
+ Pair::builder()
+ .relation_id(Relation::uid())
+ .target_id(Target::uid())
+ .build()
+ .id(),
+ );
+
+ Some(PairWithWildcard::new(self.world, matching_comps.next()?))
+ }
+
+ #[must_use]
+ pub fn get_wildcard_pair_matches<Relation, Target>(
+ &self,
+ ) -> PairMultipleWithWildcard<'a, Relation, Target>
+ where
+ Relation: ComponentOrWildcard,
+ Target: ComponentOrWildcard,
+ {
+ PairMultipleWithWildcard::new(self.world, self.clone())
+ }
+
+ #[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,
+ }
+ }
+
+ /// Returns whether or not this entity contains a component with the specified `Uid`.
+ #[must_use]
+ pub fn has_component(&self, component_uid: Uid) -> bool
+ {
+ self.archetype
+ .contains_component_with_exact_id(component_uid)
+ }
+
+ /// Returns the `Uids`s of the components this entity has.
+ pub fn component_ids(&self) -> impl Iterator<Item = Uid> + '_
+ {
+ self.archetype.component_ids_sorted()
+ }
+
+ pub(crate) fn new(
+ archetype: &'a Archetype,
+ entity: &'a ArchetypeEntity,
+ world: &'a World,
+ ) -> Self
+ {
+ Self { archetype, entity, world }
+ }
+}
+
+#[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(),
+ self.entity.uid(),
+ ))
+ }
+}
+
+/// The data type of a declaration of a entity.
+#[derive(Debug)]
+pub struct Declaration
+{
+ uid: LazyLock<Uid>,
+ create_func: fn(&mut World),
+}
+
+impl Declaration
+{
+ pub(crate) fn create(&self, world: &mut World)
+ {
+ (self.create_func)(world);
+ }
+
+ #[doc(hidden)]
+ pub const fn new(create_func: fn(&mut World)) -> Self
+ {
+ Self {
+ uid: LazyLock::new(|| Uid::new_unique(UidKind::Entity)),
+ create_func,
+ }
+ }
+}
+
+impl Deref for Declaration
+{
+ type Target = Uid;
+
+ fn deref(&self) -> &Self::Target
+ {
+ &self.uid
+ }
+}
#[allow(clippy::module_name_repetitions)]
#[macro_export]
-macro_rules! static_entity {
+macro_rules! declare_entity {
($visibility: vis $ident: ident, $components: expr) => {
- $visibility static $ident: ::std::sync::LazyLock<$crate::uid::Uid> =
- ::std::sync::LazyLock::new(|| {
- $crate::uid::Uid::new_unique($crate::uid::Kind::Entity)
+ $visibility static $ident: $crate::entity::Declaration =
+ $crate::entity::Declaration::new(|world| {
+ world.create_entity_with_uid(*$ident, $components);
});
-
- $crate::private::paste::paste! {
- mod [<__ecs_ $ident:lower _static_entity_priv>] {
- use super::*;
-
- #[$crate::private::linkme::distributed_slice(
- $crate::entity::CREATE_STATIC_ENTITIES
- )]
- #[linkme(crate=$crate::private::linkme)]
- static CREATE_STATIC_ENTITY: fn(&$crate::World) = |world| {
- world.create_entity_with_uid($components, *$ident);
- };
- }
- }
}
}
-
-#[distributed_slice]
-#[doc(hidden)]
-pub static CREATE_STATIC_ENTITIES: [fn(&World)];
diff --git a/ecs/src/entity/obtainer.rs b/ecs/src/entity/obtainer.rs
new file mode 100644
index 0000000..6c2ea96
--- /dev/null
+++ b/ecs/src/entity/obtainer.rs
@@ -0,0 +1,29 @@
+use crate::entity::Handle as EntityHandle;
+use crate::system::{Metadata as SystemMetadata, Param as SystemParam};
+use crate::uid::Uid;
+use crate::World;
+
+#[derive(Debug)]
+pub struct Obtainer<'world>
+{
+ world: &'world World,
+}
+
+impl<'world> SystemParam<'world> for Obtainer<'world>
+{
+ type Input = ();
+
+ fn new(world: &'world World, _system_metadata: &SystemMetadata) -> Self
+ {
+ Self { world }
+ }
+}
+
+impl Obtainer<'_>
+{
+ #[must_use]
+ pub fn get_entity(&self, entity_id: Uid) -> Option<EntityHandle<'_>>
+ {
+ self.world.get_entity(entity_id)
+ }
+}
diff --git a/ecs/src/event.rs b/ecs/src/event.rs
index 9cea807..15455b6 100644
--- a/ecs/src/event.rs
+++ b/ecs/src/event.rs
@@ -1 +1,105 @@
+use crate::lock::Lock;
+use crate::pair::Pair;
+use crate::uid::{Kind as UidKind, Uid};
+use crate::util::VecExt;
+
pub mod component;
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct Emitted<'a>
+{
+ pub event: Uid,
+ pub match_ids: &'a [Uid],
+}
+
+#[derive(Debug)]
+pub struct Submitter<'world>
+{
+ new_events: &'world Lock<NewEvents>,
+}
+
+impl<'world> Submitter<'world>
+{
+ /// Submits a event to be handled later.
+ ///
+ /// # Panics
+ /// Will panic if unable to acquire a read-write lock to the event store.
+ pub fn submit_event(&self, event: &Pair<Uid, Uid>, match_id: Uid)
+ {
+ let mut new_events_lock = self
+ .new_events
+ .write_nonblock()
+ .expect("Failed to acquire read-write lock to new events");
+
+ new_events_lock.push_event_match(event, match_id);
+ }
+
+ pub(crate) fn new(new_events: &'world Lock<NewEvents>) -> Self
+ {
+ Self { new_events }
+ }
+}
+
+#[derive(Debug, Default)]
+pub(crate) struct NewEvents
+{
+ events: Vec<(Uid, Matches)>,
+}
+
+impl NewEvents
+{
+ pub fn push_event_match(&mut self, event: &Pair<Uid, Uid>, match_id: Uid)
+ {
+ let event_id = event.id();
+
+ assert_eq!(event_id.kind(), UidKind::Pair);
+
+ if let Ok(event_index) = self
+ .events
+ .binary_search_by_key(&event_id, |(other_event_id, _)| *other_event_id)
+ {
+ let Some((_, matches)) = self.events.get_mut(event_index) else {
+ unreachable!();
+ };
+
+ matches.sorted_push(match_id);
+
+ return;
+ }
+
+ self.events.insert_at_part_pt_by_key(
+ (event_id, Matches { match_ids: Vec::from([match_id]) }),
+ |(other_event_id, _)| other_event_id,
+ );
+ }
+
+ pub fn take(&mut self) -> Vec<(Uid, Matches)>
+ {
+ std::mem::take(&mut self.events)
+ }
+
+ pub fn is_empty(&self) -> bool
+ {
+ self.events.is_empty()
+ }
+}
+
+#[derive(Debug)]
+pub(crate) struct Matches
+{
+ pub match_ids: Vec<Uid>,
+}
+
+impl Matches
+{
+ fn sorted_push(&mut self, match_id: Uid)
+ {
+ if self.match_ids.binary_search(&match_id).is_ok() {
+ return;
+ }
+
+ self.match_ids
+ .insert_at_part_pt_by_key(match_id, |other_match_id| other_match_id);
+ }
+}
diff --git a/ecs/src/event/component.rs b/ecs/src/event/component.rs
index b4edffc..ed6b7cf 100644
--- a/ecs/src/event/component.rs
+++ b/ecs/src/event/component.rs
@@ -1,84 +1,71 @@
//! Component events.
-use std::fmt::{Debug, Formatter};
-use std::marker::PhantomData;
+use std::convert::Infallible;
-use ecs_macros::Component;
+use crate::component::{Handle as ComponentHandle, HandleMut as ComponentHandleMut};
+use crate::entity::Handle as EntityHandle;
+use crate::pair::Pair;
+use crate::system::observer::EventMatch;
+use crate::util::impl_multiple;
+use crate::Component;
-use crate::component::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)]
+pub struct Added(Infallible);
-/// Event emitted when:
-/// a) A entity with component `ComponentT` is spawned.
-/// b) A component `ComponentT` is added to a entity.
-#[derive(Clone, Component)]
-pub struct Added<ComponentT>
-where
- ComponentT: Component,
-{
- _pd: PhantomData<ComponentT>,
-}
+/// Pair relation for events emitted **before**:
+/// a) The target component is removed from a entity.
+/// b) A entity with the target component is despawned.
+#[derive(Debug, Component)]
+pub struct Removed(Infallible);
-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()
- }
-}
+#[derive(Debug, Component)]
+pub struct Changed(Infallible);
-impl<ComponentT> Default for Added<ComponentT>
-where
- ComponentT: Component,
-{
- fn default() -> Self
- {
- Self { _pd: PhantomData }
- }
-}
+impl_multiple!(
+ EventMatch,
+ (
+ impl<Target: Component> _<'_><Pair<Removed, Target>> (removed),
+ impl<Target: Component> _<'_><Pair<Added, Target>> (added),
+ impl<Target: Component> _<'_><Pair<Changed, Target>> (changed)
+ )
+ cb=(type_params=(observable_type), event_name) => {
+ paste::paste! {
+ #[must_use]
+ pub fn [<get_ $event_name _comp>](&self) -> ComponentHandle<'_, Target>
+ {
+ let ent = self.get_ent_infallible();
-/// Event emitted when:
-/// a) A `ComponentT` component is removed from a entity.
-/// b) A entity with component `ComponentT` is despawned.
-#[derive(Clone, Component)]
-pub struct Removed<ComponentT>
-where
- ComponentT: Component,
-{
- _pd: PhantomData<ComponentT>,
-}
+ let Some(comp) = ent.get::<Target>() else {
+ unreachable!();
+ };
-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()
- }
-}
+ comp
+ }
-impl<ComponentT> Default for Removed<ComponentT>
-where
- ComponentT: Component,
-{
- fn default() -> Self
- {
- Self { _pd: PhantomData }
- }
-}
+ #[must_use]
+ pub fn [<get_ $event_name _comp_mut>](&self) -> ComponentHandleMut<'_, Target>
+ {
+ let ent = self.get_ent_infallible();
-/// Specifies a kind of component event UID.
-#[derive(Debug, Clone, Copy)]
-#[non_exhaustive]
-pub enum Kind
-{
- Removed,
-}
+ let Some(comp) = ent.get_mut::<Target>() else {
+ unreachable!();
+ };
+
+ comp
+ }
+ }
+
+ #[must_use]
+ pub fn get_ent_infallible(&self) -> EntityHandle<'_>
+ {
+ let Some(ent) = self.get_entity() else {
+ unreachable!();
+ };
+
+ ent
+ }
+ }
+);
diff --git a/ecs/src/extension.rs b/ecs/src/extension.rs
index 42ebef9..9c6614b 100644
--- a/ecs/src/extension.rs
+++ b/ecs/src/extension.rs
@@ -1,5 +1,7 @@
use crate::component::Sequence as ComponentSequence;
+use crate::entity::Declaration as EntityDeclaration;
use crate::sole::Sole;
+use crate::system::observer::Observer;
use crate::system::System;
use crate::uid::Uid;
use crate::{SoleAlreadyExistsError, World};
@@ -34,6 +36,15 @@ impl<'world> Collector<'world>
self.world.register_system(phase_euid, system);
}
+ /// Adds a observer system to the [`World`].
+ pub fn add_observer<'this, SystemImpl>(
+ &'this mut self,
+ observer: impl Observer<'this, SystemImpl>,
+ )
+ {
+ self.world.register_observer(observer);
+ }
+
/// Adds a entity to the [`World`].
pub fn add_entity<Comps>(&mut self, components: Comps)
where
@@ -42,6 +53,12 @@ impl<'world> Collector<'world>
self.world.create_entity(components);
}
+ /// Adds a declared entity to the [`World`].
+ pub fn add_declared_entity(&mut self, entity_decl: &EntityDeclaration)
+ {
+ self.world.create_declared_entity(entity_decl);
+ }
+
/// Adds a globally shared singleton value to the [`World`].
///
/// # Errors
diff --git a/ecs/src/lib.rs b/ecs/src/lib.rs
index 1b9a31b..f6fba64 100644
--- a/ecs/src/lib.rs
+++ b/ecs/src/lib.rs
@@ -1,59 +1,66 @@
#![deny(clippy::all, clippy::pedantic)]
-use std::any::{type_name, TypeId};
-use std::cell::RefCell;
+use std::any::{type_name, Any, TypeId};
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,
- IsOptional as ComponentIsOptional,
- Metadata as ComponentMetadata,
- RefSequence as ComponentRefSequence,
+ IntoParts as IntoComponentParts,
+ Parts as ComponentParts,
Sequence as ComponentSequence,
};
-use crate::entity::CREATE_STATIC_ENTITIES;
-use crate::event::component::Kind as ComponentEventKind;
+use crate::entity::{Declaration as EntityDeclaration, Handle as EntityHandle};
+use crate::event::component::Added;
+use crate::event::{Emitted as EmittedEvent, NewEvents, Submitter as EventSubmitter};
use crate::extension::{Collector as ExtensionCollector, Extension};
-use crate::lock::{Lock, WriteGuard};
-use crate::phase::{Phase, START as START_PHASE};
+use crate::lock::Lock;
+use crate::pair::{ChildOf, DependsOn, Pair};
+use crate::phase::{
+ Phase,
+ POST_UPDATE as POST_UPDATE_PHASE,
+ PRE_UPDATE as PRE_UPDATE_PHASE,
+ START as START_PHASE,
+ UPDATE as UPDATE_PHASE,
+};
use crate::query::flexible::Query as FlexibleQuery;
-use crate::query::options::{Not, Options as QueryOptions, With};
-use crate::relationship::{ChildOf, DependsOn, Relationship};
-use crate::sole::Sole;
+use crate::query::{
+ TermWithFieldTuple as QueryTermWithFieldTuple,
+ TermWithoutFieldTuple as QueryTermWithoutFieldTuple,
+ Terms as QueryTerms,
+ TermsBuilderInterface,
+ MAX_TERM_CNT as QUERY_MAX_TERM_CNT,
+};
+use crate::sole::{Single, Sole};
use crate::stats::Stats;
-use crate::system::{System, SystemComponent};
-use crate::type_name::TypeName;
+use crate::system::observer::{Observer, WrapperComponent as ObserverWrapperComponent};
+use crate::system::{Callbacks, Metadata as SystemMetadata, System, SystemComponent};
use crate::uid::{Kind as UidKind, Uid};
-use crate::util::Sortable;
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 lock;
pub use ecs_macros::{Component, Sole};
@@ -78,55 +85,49 @@ impl World
is_first_tick: AtomicBool::new(false),
};
- world.add_sole(Stats::default()).ok();
+ crate::phase::spawn_entities(&mut world);
- for create_static_entity in CREATE_STATIC_ENTITIES {
- create_static_entity(&world);
- }
+ world.add_sole(Stats::default()).ok();
world
}
- /// Creates a new entity with the given components.
- ///
- /// # Panics
- /// Will panic if mutable internal lock cannot be acquired.
+ /// Creates a entity with the given components. A new unique [`Uid`] will be generated
+ /// for this entity.
pub fn create_entity<Comps>(&mut self, components: Comps) -> Uid
where
Comps: ComponentSequence,
{
let entity_uid = Uid::new_unique(UidKind::Entity);
- self.create_entity_with_uid(components, entity_uid);
+ self.create_entity_with_uid(entity_uid, components);
entity_uid
}
- #[cfg_attr(feature = "debug", tracing::instrument(skip_all))]
- #[doc(hidden)]
- pub fn create_entity_with_uid<Comps>(&self, components: Comps, entity_uid: Uid)
+ /// Creates a entity with the given components. The entity will have the specified
+ /// [`Uid`].
+ #[tracing::instrument(skip_all)]
+ pub fn create_entity_with_uid<Comps>(&mut self, entity_uid: Uid, components: Comps)
where
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}");
+ self.create_ent(entity_uid, components.into_parts_array());
+ }
- return;
- };
+ pub fn add_component(&mut self, entity_id: Uid, component_parts: ComponentParts)
+ {
+ Self::add_entity_components(
+ entity_id,
+ [component_parts],
+ &mut self.data.component_storage,
+ &EventSubmitter::new(&self.data.new_events),
+ );
+ }
- for added_event_id in Comps::added_event_ids() {
- self.emit_event_by_id(added_event_id);
- }
+ pub fn create_declared_entity(&mut self, entity_decl: &EntityDeclaration)
+ {
+ entity_decl.create(self);
}
/// Adds a globally shared singleton value.
@@ -140,35 +141,48 @@ impl World
self.data.sole_storage.insert(sole)
}
- pub fn register_system<'this, SystemImpl>(
+ pub fn register_observer<'this, SystemImpl, ObserverT>(
&'this mut self,
- phase_euid: Uid,
- system: impl System<'this, SystemImpl>,
- )
+ observer: ObserverT,
+ ) where
+ ObserverT: Observer<'this, SystemImpl>,
{
- self.create_entity((
- SystemComponent { system: system.into_type_erased() },
- Relationship::<DependsOn, Phase>::new(phase_euid),
- ));
+ let (wrapper_comp, mut system_callbacks) = observer.finish_observer();
+
+ let ent_id = Uid::new_unique(UidKind::Entity);
+
+ self.create_ent(
+ ent_id,
+ [wrapper_comp.into_parts()].into_iter().chain(
+ ObserverT::observed_events()
+ .into_iter()
+ .map(IntoComponentParts::into_parts),
+ ),
+ );
+
+ system_callbacks.on_created(self, SystemMetadata { ent_id });
}
- pub fn register_observer_system<'this, SystemImpl, Event>(
+ pub fn register_system<'this, SystemImpl>(
&'this mut self,
+ phase_euid: Uid,
system: impl System<'this, SystemImpl>,
- event: Event,
- ) where
- Event: Component,
+ )
{
- self.create_entity::<(SystemComponent, Event)>((
- SystemComponent { system: system.into_type_erased() },
- event,
+ let (type_erased_system, mut system_callbacks) = system.finish();
+
+ let system_ent_id = self.create_entity((
+ SystemComponent { system: type_erased_system },
+ Pair::builder()
+ .relation::<DependsOn>()
+ .target_id(phase_euid)
+ .build(),
));
+
+ system_callbacks.on_created(self, SystemMetadata { ent_id: system_ent_id });
}
/// 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);
@@ -176,29 +190,52 @@ impl World
extension.collect(extension_collector);
}
- pub fn query<Comps, OptionsT>(&self) -> Query<Comps, OptionsT>
+ pub fn query<FieldTerms, FieldlessTerms>(
+ &self,
+ ) -> Query<'_, FieldTerms, FieldlessTerms>
where
- Comps: ComponentRefSequence,
- OptionsT: QueryOptions,
+ FieldTerms: QueryTermWithFieldTuple,
+ FieldlessTerms: QueryTermWithoutFieldTuple,
{
Query::new(self)
}
- pub fn flexible_query<CompMetadata>(
+ pub fn flexible_query<const MAX_TERM_CNT: usize>(
&self,
- comp_metadata: CompMetadata,
- ) -> FlexibleQuery<CompMetadata>
- where
- CompMetadata: Sortable<Item = ComponentMetadata> + AsRef<[ComponentMetadata]>,
+ terms: QueryTerms<MAX_TERM_CNT>,
+ ) -> FlexibleQuery<'_, MAX_TERM_CNT>
{
- FlexibleQuery::new(self, comp_metadata)
+ FlexibleQuery::new(self, terms)
+ }
+
+ pub fn get_entity(&self, entity_id: Uid) -> Option<EntityHandle<'_>>
+ {
+ let archetype = self
+ .data
+ .component_storage
+ .get_entity_archetype(entity_id)?;
+
+ let Some(entity) = archetype.get_entity_by_id(entity_id) else {
+ unreachable!("Should exist since archetype was found by entity id");
+ };
+
+ Some(EntityHandle::new(archetype, entity, self))
+ }
+
+ pub fn get_sole<SoleT: Sole>(&self) -> Option<Single<'_, SoleT>>
+ {
+ Some(Single::new(self.data.sole_storage.get::<SoleT>()?))
+ }
+
+ pub fn event_submitter(&self) -> EventSubmitter<'_>
+ {
+ EventSubmitter::new(&self.data.new_events)
}
/// Performs a single tick.
- ///
/// # Panics
- /// Will panic if a internal lock cannot be acquired.
- pub fn step(&self) -> StepResult
+ /// Will panic if mutable internal lock cannot be acquired.
+ pub fn step(&mut self) -> StepResult
{
if self.stop.load(Ordering::Relaxed) {
return StepResult::Stop;
@@ -214,23 +251,19 @@ impl World
self.perform_phases();
+ self.emit_new_events();
+
+ self.data.component_storage.create_imaginary_archetypes();
+
self.perform_queued_actions();
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");
+ let Some(mut stats) = self.get_sole::<Stats>() else {
+ unreachable!(); // Reason: is added in World::new
+ };
stats.current_tick += 1;
@@ -238,164 +271,220 @@ impl World
}
/// Starts a loop which calls [`Self::step`] until the world is stopped.
- ///
- /// # Panics
- /// Will panic if a internal lock cannot be acquired.
- pub fn start_loop(&self)
+ pub fn start_loop(&mut self)
{
while let StepResult::Continue = self.step() {}
}
- fn query_and_run_systems(&self, phase_euid: Uid)
+ #[cfg(feature = "vizoxide")]
+ pub fn create_vizoxide_archetype_graph(
+ &self,
+ name: impl AsRef<str>,
+ ) -> Result<vizoxide::Graph, vizoxide::GraphvizError>
{
- let system_comps_query =
- self.query::<(&SystemComponent, &Relationship<DependsOn, Phase>), ()>();
+ use std::borrow::Cow;
- let system_iter = system_comps_query.iter().filter(|(_, phase_rel)| {
- phase_rel
- .target_uids()
- .any(|target_uid| target_uid == phase_euid)
- });
+ 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");
+ }
- for (system_component, _) in system_iter {
+ 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",
+ },
+ )
+ },
+ },
+ )
+ }
+
+ #[tracing::instrument(skip_all)]
+ fn create_ent(
+ &mut self,
+ entity_uid: Uid,
+ components: impl IntoIterator<Item = ComponentParts>,
+ )
+ {
+ debug_assert_eq!(entity_uid.kind(), UidKind::Entity);
+
+ if let Err(err) = self.data.component_storage.create_entity(entity_uid) {
+ tracing::warn!("Failed to create entity: {err}");
+ return;
+ }
+
+ Self::add_entity_components(
+ entity_uid,
+ components,
+ &mut self.data.component_storage,
+ &EventSubmitter::new(&self.data.new_events),
+ );
+ }
+
+ fn query_and_run_systems(&self, phase_euid: Uid)
+ {
+ let system_query = Query::<(&SystemComponent,)>::from_flexible_query(
+ self.flexible_query(
+ QueryTerms::<QUERY_MAX_TERM_CNT>::builder()
+ .with_required([
+ SystemComponent::id(),
+ Pair::builder()
+ .relation::<DependsOn>()
+ .target_id(phase_euid)
+ .build()
+ .id(),
+ ])
+ .build(),
+ ),
+ );
+
+ for (system_ent_id, (system_component,)) in system_query.iter_with_euids() {
// SAFETY: The world lives long enough
unsafe {
- system_component.system.run(self);
+ system_component
+ .system
+ .run(self, SystemMetadata { ent_id: system_ent_id });
}
}
}
fn perform_child_phases(&self, parent_phase_euid: Uid)
{
- let phase_query = self.query::<(&Phase, &Relationship<ChildOf, Phase>), ()>();
-
- for (index, (_, phase_rel)) in phase_query.iter().enumerate() {
- if !phase_rel
- .target_uids()
- .any(|phase_euid| phase_euid == parent_phase_euid)
- {
- continue;
- }
-
- let phase_euid = phase_query
- .get_entity_uid(index)
- .expect("Cannot get current query iteration entity UID");
-
- self.query_and_run_systems(phase_euid);
+ let phase_query = self.flexible_query(
+ QueryTerms::<2>::builder()
+ .with_required([
+ Phase::id(),
+ Pair::builder()
+ .relation::<ChildOf>()
+ .target_id(parent_phase_euid)
+ .build()
+ .id(),
+ ])
+ .build(),
+ );
- self.perform_child_phases(phase_euid);
+ 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)
+ fn perform_single_phase(&self, phase_entity_id: Uid)
{
- let phase_query =
- self.query::<(&Phase,), Not<With<Relationship<ChildOf, Phase>>>>();
+ self.query_and_run_systems(phase_entity_id);
+ self.perform_child_phases(phase_entity_id);
+ }
- for (index, (_,)) in phase_query.iter().enumerate() {
- let child_phase_euid = phase_query
- .get_entity_uid(index)
- .expect("Cannot get current query iteration entity UID");
+ fn perform_phases(&self)
+ {
+ self.perform_single_phase(*PRE_UPDATE_PHASE);
+ self.perform_single_phase(*UPDATE_PHASE);
+ self.perform_single_phase(*POST_UPDATE_PHASE);
+ }
- if child_phase_euid == *START_PHASE {
- continue;
- }
+ fn emit_new_events(&self)
+ {
+ loop {
+ let new_events = {
+ let mut new_events_lock = self
+ .data
+ .new_events
+ .write_nonblock()
+ .expect("Failed to acquire read-write lock to new events");
+
+ if new_events_lock.is_empty() {
+ break;
+ }
- self.query_and_run_systems(child_phase_euid);
+ new_events_lock.take()
+ };
- self.perform_child_phases(child_phase_euid);
+ for (event_id, event_matches) in new_events {
+ self.emit_event_observers(
+ event_id,
+ &EmittedEvent {
+ event: event_id,
+ match_ids: &event_matches.match_ids,
+ },
+ );
+ }
}
}
- #[cfg_attr(feature = "debug", tracing::instrument(skip_all))]
- fn perform_queued_actions(&self)
+ #[tracing::instrument(skip_all)]
+ fn perform_queued_actions(&mut self)
{
- let mut active_action_queue = match *self.data.action_queue.active_queue.borrow()
- {
- ActiveActionQueue::A => &self.data.action_queue.queue_a,
- ActiveActionQueue::B => &self.data.action_queue.queue_b,
- }
- .write_nonblock()
- .unwrap_or_else(|err| {
- panic!(
- "Failed to take read-write action queue lock {:?}: {err}",
- self.data.action_queue.active_queue
- );
- });
-
- let mut has_swapped_active_queue = false;
+ let mut action_queue_lock = self
+ .data
+ .action_queue
+ .queue
+ .write_nonblock()
+ .unwrap_or_else(|err| {
+ panic!("Failed to take read-write action queue lock: {err}",);
+ });
- for action in active_action_queue.drain(..) {
+ for action in action_queue_lock.drain(..) {
match action {
- Action::Spawn(components, component_added_event_ids) => {
- let mut component_storage_lock = self.lock_component_storage_rw();
-
- #[allow(unused_variables)]
- if let Err(err) = component_storage_lock
- .push_entity(Uid::new_unique(UidKind::Entity), components)
+ Action::Spawn(new_entity_uid, 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);
-
- if !has_swapped_active_queue {
- self.swap_event_queue(&mut has_swapped_active_queue);
- }
-
- for comp_added_event_id in component_added_event_ids.ids {
- self.emit_event_by_id(comp_added_event_id);
- }
+ Self::add_entity_components(
+ new_entity_uid,
+ components,
+ &mut self.data.component_storage,
+ &EventSubmitter::new(&self.data.new_events),
+ );
}
Action::Despawn(entity_uid) => {
- self.despawn_entity(entity_uid, &mut has_swapped_active_queue);
- }
- Action::AddComponents(
- entity_uid,
- components,
- component_added_event_ids,
- ) => {
- let mut component_storage_lock = self.lock_component_storage_rw();
-
- component_storage_lock
- .add_components_to_entity(entity_uid, components);
-
- drop(component_storage_lock);
-
- if !has_swapped_active_queue {
- self.swap_event_queue(&mut has_swapped_active_queue);
- }
-
- for comp_added_event_id in component_added_event_ids.ids {
- self.emit_event_by_id(comp_added_event_id);
+ if let Err(err) =
+ self.data.component_storage.remove_entity(entity_uid)
+ {
+ tracing::error!("Failed to despawn entity: {err}");
}
}
- Action::RemoveComponents(
- entity_uid,
- components_metadata,
- component_removed_event_ids,
- ) => {
- let mut component_storage_lock = self.lock_component_storage_rw();
-
- component_storage_lock.remove_components_from_entity(
+ Action::AddComponents(entity_uid, components) => {
+ Self::add_entity_components(
entity_uid,
- components_metadata
- .iter()
- .map(|component_metadata| component_metadata.id),
+ components,
+ &mut self.data.component_storage,
+ &EventSubmitter::new(&self.data.new_events),
+ );
+ }
+ Action::RemoveComponents(entity_uid, component_ids) => {
+ Self::remove_entity_components(
+ entity_uid,
+ component_ids,
+ &mut self.data.component_storage,
);
-
- drop(component_storage_lock);
-
- if !has_swapped_active_queue {
- self.swap_event_queue(&mut has_swapped_active_queue);
- }
-
- for comp_removed_event_id in component_removed_event_ids.ids {
- self.emit_event_by_id(comp_removed_event_id);
- }
}
Action::Stop => {
self.stop.store(true, Ordering::Relaxed);
@@ -404,90 +493,80 @@ impl World
}
}
- fn despawn_entity(&self, entity_uid: Uid, has_swapped_active_queue: &mut bool)
+ fn add_entity_components(
+ entity_uid: Uid,
+ components: impl IntoIterator<Item = ComponentParts>,
+ component_storage: &mut ComponentStorage,
+ event_submitter: &EventSubmitter<'_>,
+ )
{
- let mut component_storage_lock = self.lock_component_storage_rw();
+ let component_iter = components.into_iter();
- let Some(archetype) = component_storage_lock.get_entity_archetype(entity_uid)
- else {
- #[cfg(feature = "debug")]
- tracing::error!("No archetype for entity {entity_uid:?} was found");
+ for component_parts in component_iter {
+ let comp_id = component_parts.id();
- return;
- };
+ let comp_name = component_parts.name();
- let entity = archetype
- .get_entity(entity_uid)
- .expect("Entity archetype was found but the entity is not in the archetype");
-
- let component_removed_event_uids = entity
- .components()
- .iter()
- .map(|component| {
- component
- .component
- .read_nonblock()
- .unwrap_or_else(|_| {
- panic!(
- "Failed to acquire read-only {} component lock",
- component.name
- )
- })
- .get_event_uid(ComponentEventKind::Removed)
- })
- .collect::<Vec<_>>();
-
- component_storage_lock.remove_entity(entity_uid);
-
- drop(component_storage_lock);
-
- if !*has_swapped_active_queue {
- self.swap_event_queue(has_swapped_active_queue);
- }
+ if let Err(err) = component_storage.add_entity_component(
+ entity_uid,
+ (comp_id, comp_name, component_parts.into_data()),
+ ) {
+ tracing::error!("Failed to add component {comp_name} to entity: {err}");
+ continue;
+ }
- for comp_removed_event_id in component_removed_event_uids {
- self.emit_event_by_id(comp_removed_event_id);
+ if comp_id.kind() == UidKind::Pair {
+ continue;
+ }
+
+ event_submitter.submit_event(
+ &Pair::builder()
+ .relation::<Added>()
+ .target_id(comp_id)
+ .build(),
+ entity_uid,
+ );
}
}
- fn emit_event_by_id(&self, event_id: Uid)
+ fn remove_entity_components(
+ entity_uid: Uid,
+ component_ids: impl IntoIterator<Item = Uid>,
+ component_storage: &mut ComponentStorage,
+ )
{
- let query = self.flexible_query([
- ComponentMetadata::of::<SystemComponent>(),
- ComponentMetadata {
- id: event_id,
- is_optional: ComponentIsOptional::No,
- },
- ]);
+ let component_id_iter = component_ids.into_iter();
- for (system,) in query
- .iter::<()>()
- .into_component_iter::<(&SystemComponent,)>(self)
- {
- unsafe {
- system.system.run(self);
+ 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}");
}
}
}
- fn swap_event_queue(&self, has_swapped_active_queue: &mut bool)
+ fn emit_event_observers(&self, event_id: Uid, emitted_event: &EmittedEvent<'_>)
{
- let mut active_queue = self.data.action_queue.active_queue.borrow_mut();
-
- *active_queue = match *active_queue {
- ActiveActionQueue::A => ActiveActionQueue::B,
- ActiveActionQueue::B => ActiveActionQueue::A,
- };
-
- *has_swapped_active_queue = true;
- }
+ assert_eq!(event_id.kind(), UidKind::Pair);
+
+ let query = Query::<(&ObserverWrapperComponent,)>::from_flexible_query(
+ self.flexible_query(
+ QueryTerms::<QUERY_MAX_TERM_CNT>::builder()
+ .with_required([ObserverWrapperComponent::id(), event_id])
+ .build(),
+ ),
+ );
- fn lock_component_storage_rw(&self) -> WriteGuard<'_, ComponentStorage>
- {
- self.data
- .component_storage
- .write_nonblock()
- .expect("Failed to acquire read-write component storage lock")
+ for (observer_ent_id, (observer,)) in query.iter_with_euids() {
+ unsafe {
+ observer.run(
+ self,
+ SystemMetadata { ent_id: observer_ent_id },
+ emitted_event.clone(),
+ );
+ }
+ }
}
}
@@ -510,74 +589,66 @@ pub enum StepResult
}
#[derive(Debug, Default)]
-pub struct WorldData
+struct WorldData
{
- component_storage: Arc<Lock<ComponentStorage>>,
+ component_storage: ComponentStorage,
sole_storage: SoleStorage,
- action_queue: Arc<ActionQueue>,
+ action_queue: Rc<ActionQueue>,
+ new_events: Lock<NewEvents>,
}
-#[derive(Debug)]
-#[non_exhaustive]
-pub struct EntityComponent
+#[derive(Debug, Clone)]
+pub struct EntityComponentRef<'a>
{
- pub id: Uid,
- pub name: &'static str,
- pub component: Lock<Box<dyn Component>>,
+ component_id: Uid,
+ component: &'a ArchetypeEntityComponent,
+ entity_id: Uid,
}
-impl From<Box<dyn Component>> for EntityComponent
+impl<'a> EntityComponentRef<'a>
{
- fn from(component: Box<dyn Component>) -> Self
+ fn component(&self) -> &'a Lock<Box<dyn Any>>
+ {
+ self.component.component()
+ }
+
+ #[must_use]
+ pub fn id(&self) -> Uid
+ {
+ self.component_id
+ }
+
+ #[must_use]
+ pub fn entity_id(&self) -> Uid
+ {
+ self.entity_id
+ }
+
+ fn new(component_id: Uid, comp: &'a ArchetypeEntityComponent, entity_id: Uid)
+ -> Self
{
Self {
- id: component.self_id(),
- name: component.type_name(),
- component: Lock::new(component),
+ component_id,
+ component: comp,
+ entity_id,
}
}
}
-#[derive(Debug, Default, Clone, Copy)]
-enum ActiveActionQueue
-{
- #[default]
- A,
- B,
-}
-
#[derive(Debug, Default)]
struct ActionQueue
{
- queue_a: Lock<Vec<Action>>,
- queue_b: Lock<Vec<Action>>,
- active_queue: RefCell<ActiveActionQueue>,
+ queue: Lock<Vec<Action>>,
}
impl ActionQueue
{
fn push(&self, action: Action)
{
- match *self.active_queue.borrow() {
- ActiveActionQueue::A => self
- .queue_a
- .write_nonblock()
- .expect("Failed to aquire read-write action queue A lock")
- .push(action),
- ActiveActionQueue::B => self
- .queue_b
- .write_nonblock()
- .expect("Failed to aquire read-write action queue A lock")
- .push(action),
- }
- }
-}
-
-impl TypeName for ActionQueue
-{
- fn type_name(&self) -> &'static str
- {
- type_name::<Self>()
+ self.queue
+ .write_nonblock()
+ .expect("Failed to aquire read-write lock to action queue")
+ .push(action);
}
}
@@ -622,7 +693,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,
}),
);
@@ -639,34 +710,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 135f654..fe4e08b 100644
--- a/ecs/src/lock.rs
+++ b/ecs/src/lock.rs
@@ -1,68 +1,73 @@
-use std::mem::transmute;
+use std::any::type_name;
+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.
///
/// # Errors
/// Returns `Err` if unavailable (A mutable handle is hold).
- pub fn read_nonblock(&self) -> Result<ReadGuard<Value>, Error>
+ pub fn read_nonblock(&self) -> Result<ReadGuard<'_, Value>, Error>
{
- let guard = self.inner.try_read().or_else(|err| match err {
- TryLockError::WouldBlock => Err(Error::ReadUnavailable),
- 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.
///
/// # Errors
/// Returns `Err` if unavailable (A mutable or immutable handle is hold).
- pub fn write_nonblock(&self) -> Result<WriteGuard<Value>, Error>
+ pub fn write_nonblock(&self) -> Result<WriteGuard<'_, Value>, Error>
{
- let guard = self.inner.try_write().or_else(|err| match err {
- TryLockError::WouldBlock => Err(Error::WriteUnavailable),
- 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 })
+ Ok(WriteGuard {
+ inner: guard,
+ value_type_name: self.value_type_name,
+ })
}
+}
- pub fn into_inner(self) -> Value
+impl<Value: Default + 'static> Default for Lock<Value>
+{
+ fn default() -> Self
{
- self.inner
- .into_inner()
- .unwrap_or_else(PoisonError::into_inner)
+ Self::new(Value::default(), type_name::<Value>())
}
}
@@ -78,31 +83,63 @@ pub enum Error
#[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 try_map<NewValue>(
+ this: Self,
+ func: impl FnOnce(&Value) -> Option<&NewValue>,
+ ) -> Result<MappedReadGuard<'guard, NewValue>, Self>
+ {
+ let value_type_name = this.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(&raw const this.inner) };
+ forget(this);
+
+ match RwLockReadGuard::try_map(inner, func) {
+ Ok(mapped_guard) => {
+ Ok(MappedReadGuard { inner: mapped_guard, value_type_name })
+ }
+ Err(unmapped_guard) => Err(Self {
+ inner: unmapped_guard,
+ value_type_name,
+ }),
+ }
+ }
+}
+
+impl<Value> Deref for ReadGuard<'_, Value>
+{
+ type Target = Value;
+
+ fn deref(&self) -> &Self::Target
+ {
+ &self.inner
+ }
+}
+
+impl<Value> Drop for ReadGuard<'_, Value>
+{
+ fn drop(&mut self)
{
- unsafe { transmute(self) }
+ tracing::trace!("Dropped lock to value of type {}", self.value_type_name);
}
}
-impl<'guard, Value> Deref for ReadGuard<'guard, Value>
-where
- Value: TypeName,
+#[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;
@@ -112,28 +149,87 @@ where
}
}
-impl<'guard, Value> Drop for ReadGuard<'guard, Value>
-where
- Value: TypeName,
+impl<Value> Drop for MappedReadGuard<'_, Value>
{
fn drop(&mut self)
{
- #[cfg(feature = "debug")]
- tracing::trace!("Dropped lock to value of type {}", self.type_name());
+ 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 try_map<NewValue>(
+ this: Self,
+ func: impl FnOnce(&mut Value) -> Option<&mut NewValue>,
+ ) -> Result<MappedWriteGuard<'guard, NewValue>, Self>
+ {
+ let value_type_name = this.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(&raw const this.inner) };
+ forget(this);
+
+ match RwLockWriteGuard::try_map(inner, func) {
+ Ok(mapped_guard) => {
+ Ok(MappedWriteGuard { inner: mapped_guard, value_type_name })
+ }
+ Err(unmapped_guard) => Err(Self {
+ inner: unmapped_guard,
+ value_type_name,
+ }),
+ }
+ }
+}
+
+impl<Value> Deref for WriteGuard<'_, Value>
+{
+ type Target = Value;
+
+ fn deref(&self) -> &Self::Target
+ {
+ &self.inner
+ }
+}
+
+impl<Value> DerefMut for WriteGuard<'_, Value>
+{
+ fn deref_mut(&mut self) -> &mut Self::Target
+ {
+ &mut self.inner
+ }
+}
+
+impl<Value> Drop for WriteGuard<'_, Value>
+{
+ fn drop(&mut self)
+ {
+ 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<'guard, Value> Deref for WriteGuard<'guard, Value>
-where
- Value: TypeName,
+impl<Value> Deref for MappedWriteGuard<'_, Value>
{
type Target = Value;
@@ -143,9 +239,7 @@ where
}
}
-impl<'guard, Value> DerefMut for WriteGuard<'guard, Value>
-where
- Value: TypeName,
+impl<Value> DerefMut for MappedWriteGuard<'_, Value>
{
fn deref_mut(&mut self) -> &mut Self::Target
{
@@ -153,13 +247,13 @@ where
}
}
-impl<'guard, Value> Drop for WriteGuard<'guard, Value>
-where
- Value: TypeName,
+impl<Value> Drop for MappedWriteGuard<'_, Value>
{
fn drop(&mut self)
{
- #[cfg(feature = "debug")]
- tracing::trace!("Dropped mutable lock to value of type {}", self.type_name());
+ 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..b4bfa57
--- /dev/null
+++ b/ecs/src/pair.rs
@@ -0,0 +1,687 @@
+use std::any::type_name;
+use std::convert::Infallible;
+use std::marker::PhantomData;
+
+use crate::component::{
+ Handle as ComponentHandle,
+ HandleError as ComponentHandleError,
+ HandleMut as ComponentHandleMut,
+ 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::{Kind as UidKind, PairParams as UidPairParams, Uid, With as WithUid};
+use crate::util::impl_multiple;
+use crate::{Component, EntityComponentRef, World};
+
+/// Pair builder.
+#[derive(Debug)]
+pub struct Builder<Relation, Target>
+{
+ relation: Relation,
+ target: Target,
+}
+
+impl<Relation, Target> Builder<Relation, Target>
+{
+ pub fn relation<NewRelation: Component>(self) -> Builder<Uid, Target>
+ {
+ Builder {
+ relation: NewRelation::id(),
+ target: self.target,
+ }
+ }
+
+ pub fn relation_id(self, id: Uid) -> Builder<Uid, Target>
+ {
+ Builder { relation: id, target: self.target }
+ }
+
+ pub fn target<NewTarget: Component>(self) -> Builder<Relation, Uid>
+ {
+ Builder {
+ relation: self.relation,
+ target: NewTarget::id(),
+ }
+ }
+
+ pub fn target_id(self, id: Uid) -> Builder<Relation, Uid>
+ {
+ Builder { relation: self.relation, target: id }
+ }
+}
+
+impl_multiple!(
+ Builder,
+ (impl<Target> _<><Uid, Target>, impl<Target> _<><(), Target>)
+ cb=(type_params=(ty_param_1, ty_param_2)) => {
+ pub fn target_as_data<NewTarget: Component>(
+ self,
+ data: NewTarget,
+ ) -> Builder<$ty_param_1, NewTarget>
+ {
+ Builder {
+ relation: self.relation,
+ target: data,
+ }
+ }
+ }
+);
+
+impl_multiple!(
+ Builder,
+ (impl<Relation> _<><Relation, Uid>, impl<Relation> _<><Relation, ()>)
+ cb=(type_params=(ty_param_1, ty_param_2)) => {
+ pub fn relation_as_data<NewRelation: Component>(
+ self,
+ data: NewRelation,
+ ) -> Builder<NewRelation, $ty_param_2>
+ {
+ Builder {
+ relation: data,
+ target: self.target,
+ }
+ }
+ }
+);
+
+impl_multiple!(
+ Builder,
+ (
+ impl _<><Uid, Uid>,
+ impl<Relation: Component> _<><Relation, Uid>,
+ impl<Target: Component> _<><Uid, Target>,
+ impl<Relation: Component, Target: Component> _<><Relation, Target>
+ )
+ cb=(type_params=(ty_param_1, ty_param_2)) => {
+ #[must_use]
+ pub fn build(self) -> Pair<$ty_param_1, $ty_param_2>
+ {
+ Pair {
+ relation: self.relation,
+ target: self.target
+ }
+ }
+ }
+);
+
+impl Default for Builder<(), ()>
+{
+ fn default() -> Self
+ {
+ Self { relation: (), target: () }
+ }
+}
+
+#[derive(Debug)]
+pub struct Pair<Relation, Target>
+{
+ relation: Relation,
+ target: Target,
+}
+
+impl Pair<(), ()>
+{
+ #[must_use]
+ pub fn builder() -> Builder<(), ()>
+ {
+ Builder { relation: (), target: () }
+ }
+}
+
+impl Pair<Uid, Uid>
+{
+ #[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<Target> IntoComponentParts for Pair<Uid, Target>
+where
+ Target: Component,
+{
+ fn into_parts(self) -> ComponentParts
+ {
+ let id = Uid::new_pair(&UidPairParams {
+ relation: self.relation,
+ target: Target::id(),
+ });
+
+ ComponentParts::builder()
+ .name("Pair")
+ .build(id, self.target)
+ }
+}
+
+impl<Relation> IntoComponentParts for Pair<Relation, Uid>
+where
+ Relation: Component,
+{
+ fn into_parts(self) -> ComponentParts
+ {
+ let id = Uid::new_pair(&UidPairParams {
+ relation: Relation::id(),
+ target: self.target,
+ });
+
+ ComponentParts::builder()
+ .name("Pair")
+ .build(id, self.relation)
+ }
+}
+
+impl<Relation, Target> QueryTermWithField for Pair<Relation, &Target>
+where
+ Relation: Component,
+ Target: Component,
+{
+ type Field<'a> = ComponentHandle<'a, Target>;
+
+ fn apply_to_terms_builder<const MAX_TERM_CNT: usize>(
+ terms_builder: &mut QueryTermsBuilder<MAX_TERM_CNT>,
+ )
+ {
+ terms_builder.with_required([Pair::<Relation, Target>::uid()]);
+ }
+
+ fn get_field<'world>(
+ entity_handle: &EntityHandle<'world>,
+ _world: &'world World,
+ ) -> Self::Field<'world>
+ {
+ let target_component = entity_handle
+ .get_matching_components(Pair::<Relation, Target>::uid())
+ .next()
+ .expect("Not possible");
+
+ Self::Field::from_entity_component_ref(&target_component).unwrap_or_else(|err| {
+ panic!(
+ "Creating handle to target component {} failed: {err}",
+ type_name::<Target>()
+ );
+ })
+ }
+}
+
+impl<Relation, Target> QueryTermWithField for Pair<Relation, &mut Target>
+where
+ Relation: Component,
+ Target: Component,
+{
+ type Field<'a> = ComponentHandleMut<'a, Target>;
+
+ fn apply_to_terms_builder<const MAX_TERM_CNT: usize>(
+ terms_builder: &mut QueryTermsBuilder<MAX_TERM_CNT>,
+ )
+ {
+ terms_builder.with_required([Pair::<Relation, Target>::uid()]);
+ }
+
+ fn get_field<'world>(
+ entity_handle: &EntityHandle<'world>,
+ world: &'world World,
+ ) -> Self::Field<'world>
+ {
+ let target_component = entity_handle
+ .get_matching_components(Pair::<Relation, Target>::uid())
+ .next()
+ .expect("Not possible");
+
+ Self::Field::from_entity_component_ref(&target_component, world).unwrap_or_else(
+ |err| {
+ panic!(
+ "Creating handle to target component {} failed: {err}",
+ type_name::<Target>()
+ );
+ },
+ )
+ }
+}
+
+// TODO: implement QueryTermWithField for Pair<&Relation, Target> (or equivalent)
+// TODO: implement QueryTermWithField for Pair<&mut Relation, Target> (or equivalent)
+
+impl<Relation> QueryTermWithField for Pair<Relation, Wildcard>
+where
+ Relation: Component,
+{
+ type Field<'a> = WithWildcard<'a, Relation, Wildcard>;
+
+ 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");
+
+ WithWildcard {
+ world,
+ component_ref: first_matching_comp,
+ _pd: PhantomData,
+ }
+ }
+}
+
+impl<Relation, Target> WithUid for Pair<Relation, Target>
+where
+ Relation: Component,
+ Target: Component,
+{
+ fn uid() -> Uid
+ {
+ Uid::new_pair(&UidPairParams {
+ relation: Relation::id(),
+ target: Target::id(),
+ })
+ }
+}
+
+impl<Relation> WithUid for Pair<Relation, Wildcard>
+where
+ Relation: Component,
+{
+ fn uid() -> Uid
+ {
+ Uid::new_pair(&UidPairParams {
+ relation: Relation::id(),
+ target: Wildcard::uid(),
+ })
+ }
+}
+
+impl<Relation> QueryTermWithField for &'_ [Pair<Relation, Wildcard>]
+where
+ Relation: Component,
+{
+ type Field<'a> = MultipleWithWildcard<'a, Relation, Wildcard>;
+
+ fn apply_to_terms_builder<const MAX_TERM_CNT: usize>(
+ _terms_builder: &mut QueryTermsBuilder<MAX_TERM_CNT>,
+ )
+ {
+ }
+
+ fn get_field<'world>(
+ entity_handle: &EntityHandle<'world>,
+ world: &'world World,
+ ) -> Self::Field<'world>
+ {
+ MultipleWithWildcard {
+ entity_handle: entity_handle.clone(),
+ world,
+ _pd: PhantomData,
+ }
+ }
+}
+
+/// Reference to a pair with a wildcard relation/target.
+#[derive(Debug)]
+pub struct WithWildcard<'world, Relation, Target>
+{
+ world: &'world World,
+ component_ref: EntityComponentRef<'world>,
+ _pd: PhantomData<(Relation, Target)>,
+}
+
+impl<'world, Relation, Target> WithWildcard<'world, Relation, Target>
+{
+ /// Returns a new `WithWildcard`.
+ ///
+ /// # Panics
+ /// This function will panic if:
+ /// - The given component's ID is not a pair ID.
+ /// - `Relation::uid()` is not wildcard and does not equal to the relation of the
+ /// given component's ID
+ /// - `Target::uid()` is not wildcard and does not equal to the target of the given
+ /// component's ID
+ /// - Both `Relation::uid()` and `Target::uid()` are wildcards
+ /// - Neither `Relation::uid()` or `Target::uid()` are wildcards
+ pub fn new(world: &'world World, component_ref: EntityComponentRef<'world>) -> Self
+ where
+ Relation: ComponentOrWildcard,
+ Target: ComponentOrWildcard,
+ {
+ let component_id = component_ref.id();
+
+ assert!(component_id.kind() == UidKind::Pair);
+
+ assert!(
+ Relation::uid() == Wildcard::uid()
+ || component_id.relation_component() == Relation::uid()
+ );
+
+ assert!(
+ Target::uid() == Wildcard::uid()
+ || component_id.target_component() == Target::uid()
+ );
+
+ assert!(Relation::uid() == Wildcard::uid() || Target::uid() == Wildcard::uid());
+
+ assert!(
+ !(Relation::uid() == Wildcard::uid() && Target::uid() == Wildcard::uid())
+ );
+
+ WithWildcard {
+ world,
+ component_ref,
+ _pd: PhantomData,
+ }
+ }
+
+ /// Returns the [`Uid`] of the pair.
+ #[must_use]
+ pub fn id(&self) -> Uid
+ {
+ self.component_ref.id()
+ }
+
+ /// Attempts to get the component data of this pair, returning `None` if the `Data`
+ /// type is incorrect.
+ ///
+ /// # Panics
+ /// Will panic if the component data is mutably borrowed elsewhere.
+ #[must_use]
+ pub fn get_data<Data>(&self) -> Option<ComponentHandle<'_, Data>>
+ where
+ Data: 'static,
+ {
+ ComponentHandle::<Data>::from_entity_component_ref(&self.component_ref)
+ .map_or_else(
+ |err| match err {
+ ComponentHandleError::IncorrectType => None,
+ err @ ComponentHandleError::AcquireLockFailed(_) => {
+ panic!(
+ "Creating handle to pair data as component {} failed: {err}",
+ type_name::<Data>()
+ );
+ }
+ },
+ Some,
+ )
+ }
+
+ /// Attempts to get the component data of this pair, returning `None` if the `Data`
+ /// type is incorrect.
+ ///
+ /// # Panics
+ /// Will panic if the component data is borrowed elsewhere.
+ #[must_use]
+ pub fn get_data_mut<Data>(&self) -> Option<ComponentHandleMut<'_, Data>>
+ where
+ Data: 'static,
+ {
+ ComponentHandleMut::<Data>::from_entity_component_ref(
+ &self.component_ref,
+ self.world,
+ )
+ .map_or_else(
+ |err| match err {
+ ComponentHandleError::IncorrectType => None,
+ err @ ComponentHandleError::AcquireLockFailed(_) => {
+ panic!(
+ "Creating handle to pair data as component {} failed: {err}",
+ type_name::<Data>()
+ );
+ }
+ },
+ Some,
+ )
+ }
+}
+
+impl<Relation> WithWildcard<'_, Relation, Wildcard>
+where
+ Relation: Component,
+{
+ /// Attempts to retrieve the target as a entity, returning `None` if not found.
+ #[must_use]
+ pub fn get_target_ent(&self) -> Option<EntityHandle<'_>>
+ {
+ let archetype = self
+ .world
+ .data
+ .component_storage
+ .get_entity_archetype(self.component_ref.id().target_entity())?;
+
+ let Some(archetype_entity) =
+ archetype.get_entity_by_id(self.component_ref.id().target_entity())
+ else {
+ unreachable!();
+ };
+
+ Some(EntityHandle::new(archetype, archetype_entity, self.world))
+ }
+
+ /// Attempts to get the component data of this pair, returning `None` if the
+ /// `Relation` type is incorrect.
+ ///
+ /// # Panics
+ /// Will panic if the component data is mutably borrowed elsewhere.
+ #[must_use]
+ pub fn get_data_as_relation(&self) -> Option<ComponentHandle<'_, Relation>>
+ {
+ ComponentHandle::<Relation>::from_entity_component_ref(&self.component_ref)
+ .map_or_else(
+ |err| match err {
+ ComponentHandleError::IncorrectType => None,
+ err @ ComponentHandleError::AcquireLockFailed(_) => {
+ panic!(
+ "Creating handle to pair data as component {} failed: {err}",
+ type_name::<Relation>()
+ );
+ }
+ },
+ Some,
+ )
+ }
+
+ /// Attempts to get the component data of this pair, returning `None` if the
+ /// `Relation` type is incorrect.
+ ///
+ /// # Panics
+ /// Will panic if the component data is borrowed elsewhere.
+ #[must_use]
+ pub fn get_data_as_relation_mut(&self) -> Option<ComponentHandleMut<'_, Relation>>
+ {
+ ComponentHandleMut::<Relation>::from_entity_component_ref(
+ &self.component_ref,
+ self.world,
+ )
+ .map_or_else(
+ |err| match err {
+ ComponentHandleError::IncorrectType => None,
+ err @ ComponentHandleError::AcquireLockFailed(_) => {
+ panic!(
+ "Creating handle to pair data as component {} failed: {err}",
+ type_name::<Relation>()
+ );
+ }
+ },
+ Some,
+ )
+ }
+}
+
+/// Used to access matching pairs in a entity containing zero or more matching pairs.
+#[derive(Debug)]
+pub struct MultipleWithWildcard<'a, Relation, Target>
+{
+ entity_handle: EntityHandle<'a>,
+ world: &'a World,
+ _pd: PhantomData<(Relation, Target)>,
+}
+
+impl<'world, Relation, Target> MultipleWithWildcard<'world, Relation, Target>
+{
+ /// Returns a new `MultipleWithWildcard`.
+ ///
+ /// # Panics
+ /// This function will panic if:
+ /// - Both `Relation::uid()` and `Target::uid()` are wildcards
+ /// - Neither `Relation::uid()` or `Target::uid()` are wildcards
+ pub fn new(world: &'world World, entity_handle: EntityHandle<'world>) -> Self
+ where
+ Relation: ComponentOrWildcard,
+ Target: ComponentOrWildcard,
+ {
+ assert!(Relation::uid() == Wildcard::uid() || Target::uid() == Wildcard::uid());
+
+ assert!(
+ !(Relation::uid() == Wildcard::uid() && Target::uid() == Wildcard::uid())
+ );
+
+ MultipleWithWildcard {
+ entity_handle,
+ world,
+ _pd: PhantomData,
+ }
+ }
+}
+
+impl<'a, Relation: Component> MultipleWithWildcard<'a, Relation, Wildcard>
+{
+ #[must_use]
+ pub fn get_with_target_id(
+ &self,
+ target_id: Uid,
+ ) -> Option<WithWildcard<'a, Relation, Wildcard>>
+ {
+ Some(WithWildcard {
+ world: self.world,
+ component_ref: self
+ .entity_handle
+ .get_matching_components(
+ Pair::builder()
+ .relation::<Relation>()
+ .target_id(target_id)
+ .build()
+ .id(),
+ )
+ .next()?,
+ _pd: PhantomData,
+ })
+ }
+}
+
+impl<'a, Relation: Component> IntoIterator
+ for MultipleWithWildcard<'a, Relation, Wildcard>
+{
+ type IntoIter = WithWildcardIter<'a, Relation, Wildcard>;
+ type Item = <Self::IntoIter as Iterator>::Item;
+
+ fn into_iter(self) -> Self::IntoIter
+ {
+ WithWildcardIter {
+ inner: self
+ .entity_handle
+ .get_matching_components(Pair::<Relation, Wildcard>::uid()),
+ world: self.world,
+ _pd: PhantomData,
+ }
+ }
+}
+
+/// Iterator of matching pairs in a entity.
+pub struct WithWildcardIter<'a, Relation, Target>
+{
+ inner: EntityMatchingComponentIter<'a>,
+ world: &'a World,
+ _pd: PhantomData<(Relation, Target)>,
+}
+
+impl<'a, Relation, Target> Iterator for WithWildcardIter<'a, Relation, Target>
+{
+ type Item = WithWildcard<'a, Relation, Target>;
+
+ fn next(&mut self) -> Option<Self::Item>
+ {
+ let matching_comp = self.inner.next()?;
+
+ Some(WithWildcard {
+ world: self.world,
+ component_ref: matching_comp,
+ _pd: PhantomData,
+ })
+ }
+}
+
+/// 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;
+
+#[derive(Debug)]
+pub struct Wildcard(Infallible);
+
+impl Wildcard
+{
+ #[must_use]
+ pub fn uid() -> Uid
+ {
+ Uid::wildcard()
+ }
+}
+
+pub trait ComponentOrWildcard: sealed::Sealed
+{
+ fn uid() -> Uid;
+}
+
+impl<ComponentT: Component> ComponentOrWildcard for ComponentT
+{
+ fn uid() -> Uid
+ {
+ ComponentT::id()
+ }
+}
+
+impl<ComponentT: Component> sealed::Sealed for ComponentT {}
+
+impl ComponentOrWildcard for Wildcard
+{
+ fn uid() -> Uid
+ {
+ Wildcard::uid()
+ }
+}
+
+impl sealed::Sealed for Wildcard {}
+
+mod sealed
+{
+ pub trait Sealed {}
+}
diff --git a/ecs/src/phase.rs b/ecs/src/phase.rs
index b8660f2..39f2a08 100644
--- a/ecs/src/phase.rs
+++ b/ecs/src/phase.rs
@@ -1,15 +1,19 @@
use ecs_macros::Component;
-use crate::relationship::{ChildOf, Relationship};
-use crate::static_entity;
+use crate::{declare_entity, World};
#[derive(Debug, Default, Clone, Copy, Component)]
pub struct Phase;
-static_entity!(pub START, (Phase,));
+declare_entity!(pub START, (Phase,));
+declare_entity!(pub PRE_UPDATE, (Phase,));
+declare_entity!(pub UPDATE, (Phase,));
+declare_entity!(pub POST_UPDATE, (Phase,));
-static_entity!(pub PRE_UPDATE, (Phase,));
-
-static_entity!(pub UPDATE, (Phase, <Relationship<ChildOf, Phase>>::new(*PRE_UPDATE)));
-
-static_entity!(pub PRESENT, (Phase, <Relationship<ChildOf, Phase>>::new(*UPDATE)));
+pub(crate) fn spawn_entities(world: &mut World)
+{
+ world.create_declared_entity(&START);
+ world.create_declared_entity(&PRE_UPDATE);
+ world.create_declared_entity(&UPDATE);
+ world.create_declared_entity(&POST_UPDATE);
+}
diff --git a/ecs/src/private.rs b/ecs/src/private.rs
deleted file mode 100644
index 56a6552..0000000
--- a/ecs/src/private.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-#[doc(hidden)]
-pub use {linkme, paste};
diff --git a/ecs/src/query.rs b/ecs/src/query.rs
index 8c1ede5..5f13579 100644
--- a/ecs/src/query.rs
+++ b/ecs/src/query.rs
@@ -1,46 +1,70 @@
+use std::any::type_name;
use std::marker::PhantomData;
-use crate::component::RefSequence as ComponentRefSequence;
-use crate::query::flexible::{
- EntityHandle,
- Iter as FlexibleQueryIter,
- Query as FlexibleQuery,
+use seq_macro::seq;
+
+use crate::component::{
+ Component,
+ Handle as ComponentHandle,
+ HandleMut as ComponentHandleMut,
};
-use crate::query::options::Options;
-use crate::system::{Param as SystemParam, System};
-use crate::uid::Uid;
+use crate::entity::Handle as EntityHandle;
+use crate::query::flexible::{Iter as FlexibleQueryIter, Query as FlexibleQuery};
+use crate::system::{Metadata as SystemMetadata, Param as SystemParam};
+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 flexible;
-pub mod options;
+pub mod term;
+
+// A term tuple type can have a maximum of 17 elements
+pub const MAX_TERM_CNT: usize = 17;
#[derive(Debug)]
-pub struct Query<'world, Comps, OptionsT = ()>
+pub struct Query<'world, FieldTerms, FieldlessTerms = ()>
where
- Comps: ComponentRefSequence,
+ FieldTerms: TermWithFieldTuple,
+ FieldlessTerms: TermWithoutFieldTuple,
{
- world: &'world World,
- inner: FlexibleQuery<'world, Comps::Metadata>,
- _pd: PhantomData<(Comps, OptionsT)>,
+ inner: FlexibleQuery<'world, MAX_TERM_CNT>,
+ _pd: PhantomData<(FieldTerms, FieldlessTerms)>,
}
-impl<'world, Comps, OptionsT> Query<'world, Comps, OptionsT>
+impl<'world, FieldTerms, FieldlessTerms> Query<'world, FieldTerms, FieldlessTerms>
where
- Comps: ComponentRefSequence,
- 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<'query>(
&'query self,
- ) -> ComponentIter<'query, 'world, Comps, FlexibleQueryIter<'query>>
+ ) -> Iter<'query, 'world, FieldTerms, FlexibleQueryIter<'query>>
+ {
+ tracing::trace!("Searching for {}", std::any::type_name::<FieldTerms>());
+
+ Iter {
+ world: self.inner.world(),
+ inner: self.inner.iter(),
+ comps_pd: PhantomData,
+ }
+ }
+
+ /// Iterates over the entities matching this query, the iterator item being the entity
+ /// [`Uid`] and the matching entity components.
+ #[must_use]
+ 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>());
- ComponentIter {
- world: self.world,
- iter: self.inner.iter::<OptionsT>(),
+ ComponentAndEuidIter {
+ world: self.inner.world(),
+ iter: self.inner.iter(),
comps_pd: PhantomData,
}
}
@@ -49,21 +73,20 @@ where
/// `func`.
///
/// This function exists so that a custom [`EntityHandle`] iterator can be given to
- /// [`ComponentIter`] without giving the user access to a reference to the [`World`].
+ /// [`Iter`] without giving the user access to a reference to the [`World`].
#[must_use]
pub fn iter_with<'query, OutIter>(
&'query self,
func: impl FnOnce(FlexibleQueryIter<'query>) -> OutIter,
- ) -> ComponentIter<'query, 'world, Comps, OutIter>
+ ) -> Iter<'query, 'world, FieldTerms, OutIter>
where
OutIter: Iterator<Item = EntityHandle<'query>>,
{
- #[cfg(feature = "debug")]
- tracing::debug!("Searching for {}", std::any::type_name::<Comps>());
+ tracing::trace!("Searching for {}", std::any::type_name::<FieldTerms>());
- ComponentIter {
- world: self.world,
- iter: func(self.inner.iter::<OptionsT>()),
+ Iter {
+ world: self.inner.world(),
+ inner: func(self.inner.iter()),
comps_pd: PhantomData,
}
}
@@ -72,27 +95,50 @@ where
#[must_use]
pub fn get_entity_uid(&self, entity_index: usize) -> Option<Uid>
{
- Some(self.inner.iter::<OptionsT>().nth(entity_index)?.uid())
+ Some(self.inner.iter().nth(entity_index)?.uid())
+ }
+
+ /// Returns a new `Query` created from a [`FlexibleQuery`].
+ ///
+ /// # Important notes
+ /// The terms in `FieldTerms` and `FieldlessTerms` must be compatible with the terms
+ /// in the given [`FlexibleQuery`], otherwise any method call or iterating might
+ /// panic.
+ #[must_use]
+ pub fn from_flexible_query(
+ flexible_query: FlexibleQuery<'world, MAX_TERM_CNT>,
+ ) -> Self
+ {
+ // TODO: Check compatability of terms
+
+ Self {
+ inner: flexible_query,
+ _pd: PhantomData,
+ }
}
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,
- inner: world.flexible_query(Comps::metadata()),
+ inner: world.flexible_query(terms_builder.build()),
_pd: PhantomData,
}
}
}
-impl<'query, 'world, Comps, OptionsT> IntoIterator
- for &'query Query<'world, Comps, OptionsT>
+impl<'query, 'world, FieldTerms, FieldlessTerms> IntoIterator
+ for &'query Query<'world, FieldTerms, FieldlessTerms>
where
- Comps: ComponentRefSequence + 'world,
- OptionsT: Options,
+ FieldTerms: TermWithFieldTuple,
+ FieldlessTerms: TermWithoutFieldTuple,
{
- type IntoIter = ComponentIter<'query, 'world, Comps, FlexibleQueryIter<'query>>;
- type Item = Comps::Handles<'query>;
+ type IntoIter = Iter<'query, 'world, FieldTerms, FlexibleQueryIter<'query>>;
+ type Item = FieldTerms::Fields<'query>;
fn into_iter(self) -> Self::IntoIter
{
@@ -100,68 +146,424 @@ where
}
}
-impl<'world, Comps, OptionsT> SystemParam<'world> for Query<'world, Comps, OptionsT>
+impl<'world, FieldTerms, FieldlessTerms> SystemParam<'world>
+ for Query<'world, FieldTerms, FieldlessTerms>
where
- Comps: ComponentRefSequence,
- OptionsT: Options,
+ FieldTerms: TermWithFieldTuple,
+ FieldlessTerms: TermWithoutFieldTuple,
{
type Input = ();
- fn initialize<SystemImpl>(
- _system: &mut impl System<'world, SystemImpl>,
- _input: Self::Input,
+ fn new(world: &'world World, _system_metadata: &SystemMetadata) -> Self
+ {
+ Self::new(world)
+ }
+}
+
+#[derive(Debug)]
+pub struct Terms<const MAX_TERM_CNT: usize>
+{
+ required_components: ArrayVec<Uid, MAX_TERM_CNT>,
+ excluded_components: ArrayVec<Uid, MAX_TERM_CNT>,
+}
+
+impl<const MAX_TERM_CNT: usize> Terms<MAX_TERM_CNT>
+{
+ pub fn builder() -> TermsBuilder<MAX_TERM_CNT>
+ {
+ TermsBuilder::default()
+ }
+}
+
+#[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>,
+}
+
+#[allow(clippy::return_self_not_must_use)]
+pub trait TermsBuilderInterface
+{
+ 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;
+}
+
+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>
+{
+ #[must_use]
+ pub fn build(self) -> Terms<MAX_TERM_CNT>
+ {
+ debug_assert!(self.required_components.is_sorted());
+ debug_assert!(self.excluded_components.is_sorted());
+
+ 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>,
)
{
+ terms_builder.with::<ComponentT>();
}
- fn new<SystemImpl>(
- _system: &'world impl System<'world, SystemImpl>,
+ 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>()
+ );
+ })
+ }
+}
+
+impl<ComponentT: Component> TermWithField for &mut ComponentT
+{
+ 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
+ ) -> Self::Field<'world>
{
- Self::new(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, world).unwrap_or_else(|err| {
+ panic!(
+ "Creating handle to component {} failed: {err}",
+ type_name::<ComponentT>()
+ );
+ })
}
}
-pub struct ComponentIter<'query, 'world, Comps, EntityHandleIter>
+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<Comps>,
+ inner: EntityHandleIter,
+ comps_pd: PhantomData<FieldTerms>,
}
-impl<'query, 'world, Comps, EntityHandleIter>
- ComponentIter<'query, 'world, Comps, EntityHandleIter>
+impl<'query, 'world, FieldTerms, EntityHandleIter> Iterator
+ for Iter<'query, 'world, FieldTerms, EntityHandleIter>
where
- Comps: ComponentRefSequence + 'world,
+ FieldTerms: TermWithFieldTuple,
EntityHandleIter: Iterator<Item = EntityHandle<'query>>,
'world: 'query,
{
- pub(crate) fn new(world: &'world World, iter: EntityHandleIter) -> Self
+ type Item = FieldTerms::Fields<'query>;
+
+ fn next(&mut self) -> Option<Self::Item>
{
- Self { world, iter, comps_pd: PhantomData }
+ let entity_handle = self.inner.next()?;
+
+ Some(FieldTerms::get_fields(&entity_handle, self.world))
}
}
-impl<'query, 'world, Comps, EntityHandleIter> Iterator
- for ComponentIter<'query, 'world, Comps, EntityHandleIter>
+pub struct ComponentAndEuidIter<'query, 'world, FieldTerms, EntityHandleIter>
+where
+ FieldTerms: TermWithFieldTuple,
+ EntityHandleIter: Iterator<Item = EntityHandle<'query>>,
+{
+ world: &'world World,
+ iter: EntityHandleIter,
+ comps_pd: PhantomData<FieldTerms>,
+}
+
+impl<'query, 'world, FieldTerms, EntityHandleIter> Iterator
+ for ComponentAndEuidIter<'query, 'world, FieldTerms, EntityHandleIter>
where
- Comps: ComponentRefSequence + 'world,
+ FieldTerms: TermWithFieldTuple,
EntityHandleIter: Iterator<Item = EntityHandle<'query>>,
'world: 'query,
{
- type Item = Comps::Handles<'query>;
+ type Item = (Uid, FieldTerms::Fields<'query>);
fn next(&mut self) -> Option<Self::Item>
{
let entity_handle = self.iter.next()?;
- Some(Comps::from_components(
- entity_handle.components(),
- |component_uid| entity_handle.get_component_index(component_uid),
- self.world,
+ Some((
+ entity_handle.uid(),
+ FieldTerms::get_fields(&entity_handle, self.world),
))
}
}
+
+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 ()
+{
+ 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
index 5c23e68..936ab82 100644
--- a/ecs/src/query/flexible.rs
+++ b/ecs/src/query/flexible.rs
@@ -1,145 +1,92 @@
//! Low-level querying.
-use std::iter::{repeat_n, Filter, Flatten, Map, RepeatN, Zip};
+use std::iter::{repeat_n, FlatMap, RepeatN, Zip};
-use crate::component::storage::{
- Archetype,
- ArchetypeEntity,
- ArchetypeRefIter,
- EntityIter,
- Storage as ComponentStorage,
-};
-use crate::component::{
- Metadata as ComponentMetadata,
- RefSequence as ComponentRefSequence,
-};
-use crate::lock::ReadGuard;
-use crate::query::options::Options;
-use crate::query::ComponentIter;
-use crate::uid::Uid;
-use crate::util::Sortable;
-use crate::{EntityComponent, World};
+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, CompMetadata>
-where
- CompMetadata: Sortable<Item = ComponentMetadata> + AsRef<[ComponentMetadata]>,
+pub struct Query<'world, const MAX_TERM_CNT: usize>
{
- component_storage: ReadGuard<'world, ComponentStorage>,
- comp_metadata: CompMetadata,
+ world: &'world World,
+ terms: Terms<MAX_TERM_CNT>,
}
-impl<'world, CompMetadata> Query<'world, CompMetadata>
-where
- CompMetadata: Sortable<Item = ComponentMetadata> + AsRef<[ComponentMetadata]>,
+impl<'world, const MAX_TERM_CNT: usize> Query<'world, MAX_TERM_CNT>
{
/// Iterates over the entities matching this query.
#[must_use]
- pub fn iter<'query, OptionsT: Options>(&'query self) -> Iter<'query>
+ pub fn iter(&self) -> Iter<'_>
{
Iter {
iter: self
+ .world
+ .data
.component_storage
- .iter_archetypes_with_comps(&self.comp_metadata)
- .map(
+ .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,
- )
- .flatten()
- .filter(|(_, entity)| OptionsT::entity_filter(entity.components())),
+ ),
+ world: self.world,
}
}
- pub(crate) fn new(world: &'world World, mut comp_metadata: CompMetadata) -> Self
+ #[must_use]
+ pub fn world(&self) -> &'world World
{
- comp_metadata.sort_by_key_b(|metadata| metadata.id);
-
- Self {
- component_storage: world
- .data
- .component_storage
- .read_nonblock()
- .expect("Failed to acquire read-only component storage lock"),
- comp_metadata,
- }
+ self.world
}
-}
-pub struct Iter<'query>
-{
- iter: QueryEntityIter<'query>,
-}
-
-impl<'query> Iter<'query>
-{
- /// Converts this iterator into a [`ComponentIter`].
- ///
- /// Note: The matching entities of this iterator should have all of the non-[`Option`]
- /// components in `Comps`, otherwise iterating the [`ComponentIter`] will cause a
- /// panic.
- #[must_use]
- #[inline]
- pub fn into_component_iter<'world, Comps>(
- self,
- world: &'world World,
- ) -> ComponentIter<'query, 'world, Comps, Self>
- where
- Comps: ComponentRefSequence + 'world,
- 'world: 'query,
+ pub(crate) fn new(world: &'world World, terms: Terms<MAX_TERM_CNT>) -> Self
{
- ComponentIter::new(world, self)
+ Self { world, terms }
}
}
-impl<'query> Iterator for Iter<'query>
+impl<'query, const MAX_TERM_CNT: usize> IntoIterator for &'query Query<'_, MAX_TERM_CNT>
{
+ type IntoIter = Iter<'query>;
type Item = EntityHandle<'query>;
- fn next(&mut self) -> Option<Self::Item>
+ fn into_iter(self) -> Self::IntoIter
{
- let (archetype, entity) = self.iter.next()?;
-
- Some(EntityHandle { archetype, entity })
+ self.iter()
}
}
-pub struct EntityHandle<'query>
+pub struct Iter<'query>
{
- archetype: &'query Archetype,
- entity: &'query ArchetypeEntity,
+ iter: QueryEntityIter<'query>,
+ world: &'query World,
}
-impl<'query> EntityHandle<'query>
+impl<'query> Iterator for Iter<'query>
{
- /// Returns the [`Uid`] of this entity.
- #[inline]
- pub fn uid(&self) -> Uid
- {
- self.entity.uid()
- }
+ type Item = EntityHandle<'query>;
- #[inline]
- pub fn components(&self) -> &'query [EntityComponent]
+ fn next(&mut self) -> Option<Self::Item>
{
- self.entity.components()
- }
+ let (archetype, entity) = self.iter.next()?;
- #[inline]
- pub fn get_component_index(&self, component_uid: Uid) -> Option<usize>
- {
- self.archetype.get_index_for_component(component_uid)
+ Some(EntityHandle::new(archetype, entity, self.world))
}
}
-type ComponentIterMapFn =
- for<'a> fn(&'a Archetype) -> Zip<RepeatN<&'a Archetype>, EntityIter<'a>>;
+type ComponentIterMapFnOutput<'a> = Zip<RepeatN<&'a Archetype>, EntityIter<'a>>;
-type ComponentIterFilterFn =
- for<'a, 'b> fn(&'a (&'b Archetype, &'b ArchetypeEntity)) -> bool;
+type ComponentIterMapFn = for<'a> fn(&'a Archetype) -> ComponentIterMapFnOutput<'a>;
-type QueryEntityIter<'query> = Filter<
- Flatten<Map<ArchetypeRefIter<'query>, ComponentIterMapFn>>,
- ComponentIterFilterFn,
+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 772d091..0000000
--- a/ecs/src/query/options.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-use std::marker::PhantomData;
-
-use hashbrown::HashSet;
-
-use crate::component::Component;
-use crate::EntityComponent;
-
-/// Query options.
-pub trait Options
-{
- fn entity_filter<'component>(components: &'component [EntityComponent]) -> bool;
-}
-
-impl Options for ()
-{
- fn entity_filter<'component>(_components: &'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: &'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: &'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..0683918
--- /dev/null
+++ b/ecs/src/query/term.rs
@@ -0,0 +1,116 @@
+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()?,
+ world,
+ )
+ .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 d136db4..0000000
--- a/ecs/src/relationship.rs
+++ /dev/null
@@ -1,471 +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,
- FromLockedOptional as FromLockedOptionalComponent,
- FromOptional as FromOptionalComponent,
- FromOptionalMut as FromOptionalMutComponent,
-};
-use crate::lock::{Error as LockError, 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> FromLockedOptionalComponent<'rel_comp>
- for RelationMut<'rel_comp, Kind, ComponentT>
-where
- ComponentT: Component,
-{
- fn from_locked_optional_component(
- optional_component: Option<&'rel_comp crate::lock::Lock<Box<dyn Component>>>,
- world: &'rel_comp World,
- ) -> Result<Self, LockError>
- {
- Ok(Self::from_optional_mut_component(
- optional_component
- .map(|lock| lock.write_nonblock())
- .transpose()?,
- world,
- ))
- }
-}
-
-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> FromLockedOptionalComponent<'rel_comp>
- for Relation<'rel_comp, Kind, ComponentT>
-where
- ComponentT: Component,
-{
- fn from_locked_optional_component(
- optional_component: Option<&'rel_comp Lock<Box<dyn Component>>>,
- world: &'rel_comp World,
- ) -> Result<Self, LockError>
- {
- Ok(Self::from_optional_component(
- optional_component
- .map(|lock| lock.read_nonblock())
- .transpose()?,
- world,
- ))
- }
-}
-
-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(),
- }
- }
-
- pub fn target_uids(&self) -> impl Iterator<Item = Uid> + '_
- {
- (0..self.target_count())
- .map_while(|target_index| self.get_target(target_index).copied())
- }
-
- /// 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)
- }
-}
-
-/// Relationship kind denoting a dependency to another entity
-#[derive(Debug, Default, Clone, Copy)]
-pub struct DependsOn;
-
-/// Relationship kind denoting being the child of another entity.
-#[derive(Debug, Default, Clone, Copy)]
-pub struct ChildOf;
diff --git a/ecs/src/sole.rs b/ecs/src/sole.rs
index a35b520..7cfcc24 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::{Param as SystemParam, System};
-use crate::type_name::TypeName;
+use crate::system::{Metadata as SystemMetadata, Param as SystemParam};
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>
@@ -73,7 +64,7 @@ where
}
}
- fn new(sole: &'world Arc<Lock<Box<dyn Sole>>>) -> Self
+ pub(crate) fn new(sole: &'world Arc<Lock<Box<dyn Sole>>>) -> Self
{
Self {
sole: sole.write_nonblock().unwrap_or_else(|_| {
@@ -94,17 +85,7 @@ where
{
type Input = ();
- fn initialize<SystemImpl>(
- _system: &mut impl System<'world, SystemImpl>,
- _input: Self::Input,
- )
- {
- }
-
- fn new<SystemImpl>(
- _system: &'world impl System<'world, SystemImpl>,
- world: &'world World,
- ) -> Self
+ fn new(world: &'world World, _system_metadata: &SystemMetadata) -> Self
{
let sole = world.data.sole_storage.get::<SoleT>().unwrap_or_else(|| {
panic!("Sole {} was not found in world", type_name::<SoleT>())
@@ -114,7 +95,7 @@ where
}
}
-impl<'world, SoleT> Deref for Single<'world, SoleT>
+impl<SoleT> Deref for Single<'_, SoleT>
where
SoleT: Sole,
{
@@ -126,7 +107,7 @@ where
}
}
-impl<'world, SoleT> DerefMut for Single<'world, SoleT>
+impl<SoleT> DerefMut for Single<'_, SoleT>
where
SoleT: Sole,
{
@@ -173,7 +154,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 a810741..95ab7a8 100644
--- a/ecs/src/system.rs
+++ b/ecs/src/system.rs
@@ -1,46 +1,28 @@
-use std::any::{type_name, 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,
- FromLockedOptional as FromLockedOptionalComponent,
- FromOptional as FromOptionalComponent,
- FromOptionalMut as FromOptionalMutComponent,
-};
-use crate::lock::{Error as LockError, Lock, ReadGuard, WriteGuard};
-use crate::tuple::{ReduceElement as TupleReduceElement, Tuple};
+use crate::uid::Uid;
use crate::World;
+pub mod initializable;
+pub mod observer;
pub mod stateful;
-pub trait System<'world, Impl>: 'static
+/// Metadata of a system.
+#[derive(Debug)]
+#[non_exhaustive]
+pub struct Metadata
{
- type Input;
-
- #[must_use]
- fn initialize(self, input: Self::Input) -> Self;
-
- fn run<'this>(&'this self, world: &'world World)
- where
- 'this: 'world;
-
- fn into_type_erased(self) -> TypeErased;
+ pub ent_id: Uid,
+}
- fn get_local_component_mut<LocalComponent: Component>(
- &self,
- ) -> Option<ComponentRefMut<LocalComponent>>;
+pub trait System<'world, Impl>: 'static
+{
+ type Callbacks: Callbacks;
- fn set_local_component<LocalComponent: Component>(
- &mut self,
- local_component: LocalComponent,
- );
+ fn finish(self) -> (TypeErased, Self::Callbacks);
}
macro_rules! impl_system {
@@ -49,61 +31,26 @@ 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,
+ Func: Fn(#(TParam~I,)*) + Copy + 'static,
#(TParam~I: Param<'world, Input = ()>,)*
{
- type Input = Infallible;
-
- fn initialize(self, _input: Self::Input) -> Self
- {
- self
- }
-
- fn run<'this>(&'this self, world: &'world World)
- where
- 'this: 'world
- {
- let func = *self;
-
- func(#({
- TParam~I::new(self, world)
- },)*);
- }
+ type Callbacks = NoCallbacks;
- fn into_type_erased(self) -> TypeErased
+ fn finish(self) -> (TypeErased, Self::Callbacks)
{
- TypeErased {
- data: Box::new(self),
- run: Box::new(|data, world| {
- // SAFETY: The caller of TypeErased::run ensures the lifetime
- // is correct
- let data = unsafe { &*std::ptr::from_ref(data) };
-
- let me = data
- .downcast_ref::<Func>()
- .expect("Function downcast failed");
-
+ let type_erased = TypeErased {
+ run: Box::new(move |world, metadata| {
// SAFETY: The caller of TypeErased::run ensures the lifetime
// is correct
let world = unsafe { &*std::ptr::from_ref(world) };
- me.run(world);
+ self(#({
+ TParam~I::new(world, &metadata)
+ },)*);
}),
- }
- }
-
- fn get_local_component_mut<LocalComponent: Component>(
- &self,
- ) -> Option<ComponentRefMut<LocalComponent>>
- {
- panic!("System does not have any local components");
- }
+ };
- fn set_local_component<LocalComponent: Component>(
- &mut self,
- _local_component: LocalComponent,
- ) {
- panic!("System does not have any local components");
+ (type_erased, NoCallbacks)
}
}
});
@@ -114,7 +61,7 @@ seq!(C in 1..16 {
impl_system!(C);
});
-pub trait Into<Impl>
+pub trait Into<'world, Impl>
{
type System;
@@ -123,7 +70,6 @@ pub trait Into<Impl>
pub struct TypeErased
{
- data: Box<dyn Any + RefUnwindSafe + UnwindSafe>,
run: Box<TypeErasedRunFn>,
}
@@ -133,12 +79,9 @@ impl TypeErased
///
/// # Safety
/// `world_data` must live at least as long as the [`World`] the system belongs to.
- pub unsafe fn run(&self, world: &World)
+ pub unsafe fn run(&self, world: &World, metadata: Metadata)
{
- // You have to dereference for downcasting to work for some reason
- let data = &*self.data;
-
- (self.run)(data, world);
+ (self.run)(world, metadata);
}
}
@@ -151,233 +94,29 @@ 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(&World, Metadata);
/// A parameter to a [`System`].
pub trait Param<'world>
{
type Input;
- fn initialize<SystemImpl>(
- system: &mut impl System<'world, SystemImpl>,
- input: Self::Input,
- );
-
- fn new<SystemImpl>(
- system: &'world impl System<'world, SystemImpl>,
- world: &'world World,
- ) -> Self;
+ fn new(world: &'world World, system_metadata: &Metadata) -> Self;
}
/// A type which can be used as input to a [`System`].
pub trait Input: 'static {}
-/// Component tuple reducing operation to get the parameters that takes input.
-pub struct ParamWithInputFilter;
-
-impl<InputT: Input, Accumulator> TupleReduceElement<Accumulator, ParamWithInputFilter>
- for InputT
-where
- Accumulator: Tuple,
-{
- type Return = Accumulator::WithElementAtEnd<Self>;
-}
-
-impl<Accumulator> TupleReduceElement<Accumulator, ParamWithInputFilter> for ()
+pub trait Callbacks
{
- type Return = Accumulator;
+ fn on_created(&mut self, world: &mut World, metadata: Metadata);
}
-#[derive(Debug)]
-pub struct ComponentRefMut<'a, ComponentT: Component>
-{
- inner: WriteGuard<'a, Box<dyn Component>>,
- _ph: PhantomData<ComponentT>,
-}
+pub struct NoCallbacks;
-impl<'a, ComponentT: Component> ComponentRefMut<'a, ComponentT>
+impl Callbacks for NoCallbacks
{
- 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>
-{
- 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<'component, ComponentT: Component> FromLockedOptionalComponent<'component>
- for ComponentRefMut<'component, ComponentT>
-{
- fn from_locked_optional_component(
- optional_component: Option<&'component crate::lock::Lock<Box<dyn Component>>>,
- world: &'component World,
- ) -> Result<Self, LockError>
- {
- Ok(Self::from_optional_mut_component(
- optional_component
- .map(|lock| lock.write_nonblock())
- .transpose()?,
- world,
- ))
- }
-}
-
-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<'comp, ComponentT> FromLockedOptionalComponent<'comp>
- for Option<ComponentRefMut<'comp, ComponentT>>
-where
- ComponentT: Component,
-{
- fn from_locked_optional_component(
- optional_component: Option<&'comp Lock<Box<dyn Component>>>,
- _world: &'comp World,
- ) -> Result<Self, LockError>
- {
- optional_component
- .map(|lock| Ok(ComponentRefMut::new(lock.write_nonblock()?)))
- .transpose()
- }
-}
-
-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<'component, ComponentT: Component> FromLockedOptionalComponent<'component>
- for ComponentRef<'component, ComponentT>
-{
- fn from_locked_optional_component(
- optional_component: Option<&'component crate::lock::Lock<Box<dyn Component>>>,
- world: &'component World,
- ) -> Result<Self, LockError>
- {
- Ok(Self::from_optional_component(
- optional_component
- .map(|lock| lock.read_nonblock())
- .transpose()?,
- world,
- ))
- }
-}
-
-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<'comp, ComponentT> FromLockedOptionalComponent<'comp>
- for Option<ComponentRef<'comp, ComponentT>>
-where
- ComponentT: Component,
-{
- fn from_locked_optional_component(
- optional_component: Option<&'comp Lock<Box<dyn Component>>>,
- _world: &'comp World,
- ) -> Result<Self, LockError>
- {
- optional_component
- .map(|lock| Ok(ComponentRef::new(lock.read_nonblock()?)))
- .transpose()
- }
-}
-
-impl<'a, ComponentT: Component> Deref for ComponentRef<'a, ComponentT>
-{
- type Target = ComponentT;
-
- fn deref(&self) -> &Self::Target
- {
- self.inner.downcast_ref().unwrap()
- }
+ fn on_created(&mut self, _world: &mut World, _metadata: Metadata) {}
}
#[derive(Debug, Component)]
diff --git a/ecs/src/system/initializable.rs b/ecs/src/system/initializable.rs
new file mode 100644
index 0000000..b6ec8e8
--- /dev/null
+++ b/ecs/src/system/initializable.rs
@@ -0,0 +1,131 @@
+use std::marker::PhantomData;
+
+use seq_macro::seq;
+
+use crate::system::{Input, Param as SystemParam, System};
+use crate::tuple::{Reduce as TupleReduce, ReduceElement as TupleReduceElement, Tuple};
+
+/// A initializable system.
+pub trait Initializable<'world, Impl>: System<'world, Impl>
+{
+ type Inputs;
+
+ #[must_use]
+ fn initialize(self, inputs: Self::Inputs) -> Self;
+}
+
+pub trait Param<'world, SystemT>: SystemParam<'world>
+{
+ fn initialize(system: &mut SystemT, input: Self::Input);
+}
+
+pub struct ParamTupleFilter<'world, SystemT>
+{
+ _pd: PhantomData<(&'world (), SystemT)>,
+}
+
+impl<'world, SystemT, ParamT, Accumulator>
+ TupleReduceElement<Accumulator, ParamTupleFilter<'world, SystemT>> for ParamT
+where
+ ParamT: SystemParam<
+ 'world,
+ Input: AppendInitializableParam<'world, Accumulator, ParamT, SystemT>,
+ >,
+ Accumulator: Tuple,
+{
+ type Return = <ParamT::Input as AppendInitializableParam<
+ 'world,
+ Accumulator,
+ ParamT,
+ SystemT,
+ >>::Return;
+}
+
+pub trait AppendInitializableParam<'world, Accumulator, ParamT, SystemT>
+{
+ type Return;
+}
+
+impl<'world, InputT, ParamT, Accumulator, SystemT>
+ AppendInitializableParam<'world, Accumulator, ParamT, SystemT> for InputT
+where
+ InputT: Input,
+ Accumulator: Tuple,
+ ParamT: Param<'world, SystemT>,
+{
+ type Return = Accumulator::WithElementAtEnd<ParamT>;
+}
+
+impl<ParamT, Accumulator, SystemT>
+ AppendInitializableParam<'_, Accumulator, ParamT, SystemT> for ()
+where
+ Accumulator: Tuple,
+{
+ type Return = Accumulator;
+}
+
+pub trait ParamTuple<'world, SystemT>
+{
+ type Inputs;
+
+ fn initialize_all(system: &mut SystemT, inputs: Self::Inputs);
+}
+
+macro_rules! impl_initializable_param_tuple {
+ ($c: tt) => {
+ seq!(I in 0..$c {
+ impl<'world, SystemT, #(Param~I,)*> ParamTuple<'world, SystemT>
+ for (#(Param~I,)*)
+ where
+ #(Param~I: Param<'world, SystemT>,)*
+ {
+ type Inputs = (#(Param~I::Input,)*);
+
+ fn initialize_all(
+ system: &mut SystemT,
+ inputs: Self::Inputs,
+ ) {
+ #(
+ <Param~I as Param<'world, SystemT>>::initialize(
+ system,
+ inputs.I
+ );
+ )*
+ }
+ }
+ });
+ };
+}
+
+seq!(C in 1..16 {
+ impl_initializable_param_tuple!(C);
+});
+
+impl<SystemT> ParamTuple<'_, SystemT> for ()
+{
+ type Inputs = ();
+
+ fn initialize_all(_system: &mut SystemT, _inputs: Self::Inputs) {}
+}
+
+/// A tuple of system parameters that may or may not be initializable.
+pub trait MaybeInitializableParamTuple<'world, SystemT>
+{
+ /// A tuple of the inputs of the initializable system parameters in this tuple.
+ type Inputs;
+
+ fn init_initializable(system: &mut SystemT, inputs: Self::Inputs);
+}
+
+impl<'world, SystemT, Params> MaybeInitializableParamTuple<'world, SystemT> for Params
+where
+ Params:
+ TupleReduce<ParamTupleFilter<'world, SystemT>, Out: ParamTuple<'world, SystemT>>,
+{
+ type Inputs = <Params::Out as ParamTuple<'world, SystemT>>::Inputs;
+
+ fn init_initializable(system: &mut SystemT, inputs: Self::Inputs)
+ {
+ Params::Out::initialize_all(system, inputs);
+ }
+}
diff --git a/ecs/src/system/observer.rs b/ecs/src/system/observer.rs
new file mode 100644
index 0000000..236420c
--- /dev/null
+++ b/ecs/src/system/observer.rs
@@ -0,0 +1,310 @@
+use std::fmt::Debug;
+use std::marker::PhantomData;
+use std::mem::transmute;
+use std::slice::Iter as SliceIter;
+
+use ecs_macros::Component;
+use seq_macro::seq;
+
+use crate::component::Component;
+use crate::entity::Handle as EntityHandle;
+use crate::event::Emitted as EmittedEvent;
+use crate::pair::Pair;
+use crate::system::{
+ Metadata,
+ NoCallbacks,
+ Param,
+ System,
+ TypeErased as TypeErasedSystem,
+};
+use crate::uid::Uid;
+use crate::util::Array;
+use crate::World;
+
+pub trait Observed
+{
+ type Events: Array<Pair<Uid, Uid>>;
+
+ fn events() -> Self::Events;
+}
+
+impl<Relation, Target> Observed for Pair<Relation, Target>
+where
+ Relation: Component,
+ Target: Component,
+{
+ type Events = [Pair<Uid, Uid>; 1];
+
+ fn events() -> Self::Events
+ {
+ [Pair::builder()
+ .relation::<Relation>()
+ .target::<Target>()
+ .build()]
+ }
+}
+
+/// Observer system.
+pub trait Observer<'world, Impl>: System<'world, Impl>
+{
+ type ObservedEvents: Array<Pair<Uid, Uid>>;
+
+ fn observed_events() -> Self::ObservedEvents;
+
+ fn finish_observer(self) -> (WrapperComponent, Self::Callbacks);
+}
+
+pub struct Observe<'world, ObservedT: Observed>
+{
+ _pd: PhantomData<ObservedT>,
+ world: &'world World,
+ emitted_event: EmittedEvent<'world>,
+}
+
+impl<'world, ObservedT: Observed> Observe<'world, ObservedT>
+{
+ pub fn new(world: &'world World, emitted_event: EmittedEvent<'world>) -> Self
+ {
+ Self {
+ _pd: PhantomData,
+ world,
+ emitted_event,
+ }
+ }
+
+ #[must_use]
+ pub fn event(&self) -> Uid
+ {
+ self.emitted_event.event
+ }
+}
+
+impl<ObservedT: Observed> Observe<'_, ObservedT>
+{
+ #[must_use]
+ pub fn iter(&self) -> ObserveIter<'_, ObservedT>
+ {
+ ObserveIter {
+ world: self.world,
+ inner: self.emitted_event.match_ids.iter(),
+ _pd: PhantomData,
+ }
+ }
+}
+
+impl<'a, ObservedT: Observed> IntoIterator for &'a Observe<'_, ObservedT>
+{
+ type IntoIter = ObserveIter<'a, ObservedT>;
+ type Item = <Self::IntoIter as Iterator>::Item;
+
+ fn into_iter(self) -> Self::IntoIter
+ {
+ self.iter()
+ }
+}
+
+pub struct ObserveIter<'observe, ObservedT: Observed>
+{
+ world: &'observe World,
+ inner: SliceIter<'observe, Uid>,
+ _pd: PhantomData<ObservedT>,
+}
+
+impl<'observe, ObservedT: Observed> Iterator for ObserveIter<'observe, ObservedT>
+{
+ type Item = EventMatch<'observe, ObservedT>;
+
+ fn next(&mut self) -> Option<Self::Item>
+ {
+ let match_id = *self.inner.next()?;
+
+ Some(EventMatch {
+ world: self.world,
+ id: match_id,
+ _pd: PhantomData,
+ })
+ }
+}
+
+/// A event match.
+#[derive(Debug)]
+pub struct EventMatch<'world, ObservedT: Observed>
+{
+ world: &'world World,
+ id: Uid,
+ _pd: PhantomData<ObservedT>,
+}
+
+impl<'world, ObservedT: Observed> EventMatch<'world, ObservedT>
+{
+ #[must_use]
+ pub fn id(&self) -> Uid
+ {
+ self.id
+ }
+
+ /// Attempts to get the entity with the id of this match.
+ #[must_use]
+ pub fn get_entity(&self) -> Option<EntityHandle<'world>>
+ {
+ self.world.get_entity(self.id)
+ }
+}
+
+macro_rules! impl_observer {
+ ($c: tt) => {
+ seq!(I in 0..$c {
+ impl<'world, ObservedT, Func, #(TParam~I,)*> System<
+ 'world,
+ fn(Observe<'world, ObservedT>, #(TParam~I,)*)
+ > for Func
+ where
+ ObservedT: Observed,
+ Func: Fn(Observe<'world, ObservedT>, #(TParam~I,)*) + Copy + 'static,
+ #(TParam~I: Param<'world, Input = ()>,)*
+ {
+ type Callbacks = NoCallbacks;
+
+ fn finish(self) -> (TypeErasedSystem, NoCallbacks)
+ {
+ unimplemented!();
+ }
+ }
+
+ impl<'world, ObservedT, Func, #(TParam~I,)*> Observer<
+ 'world,
+ fn(Observe<'world, ObservedT>, #(TParam~I,)*)
+ > for Func
+ where
+ ObservedT: Observed,
+ Func: Fn(Observe<'world, ObservedT>, #(TParam~I,)*) + Copy + 'static,
+ #(TParam~I: Param<'world, Input = ()>,)*
+ {
+ type ObservedEvents = ObservedT::Events;
+
+ fn observed_events() -> Self::ObservedEvents
+ {
+ ObservedT::events()
+ }
+
+ fn finish_observer(self) -> (WrapperComponent, NoCallbacks)
+ {
+ let wrapper_comp = WrapperComponent {
+ run: Box::new(move |world, metadata, emitted_event| {
+ // SAFETY: The caller of TypeErased::run ensures the lifetime
+ // is correct
+ let world = unsafe { &*std::ptr::from_ref(world) };
+
+ // SAFETY: The caller of TypeErased::run ensures the lifetime
+ // is correct
+ let emitted_event = unsafe {
+ transmute::<EmittedEvent<'_>, EmittedEvent<'_>>(
+ emitted_event
+ )
+ };
+
+ self(Observe::new(world, emitted_event), #({
+ TParam~I::new(world, &metadata)
+ },)*);
+ }),
+ };
+
+ (wrapper_comp, NoCallbacks)
+ }
+ }
+ });
+ };
+}
+
+seq!(C in 1..16 {
+ impl_observer!(C);
+});
+
+impl<'world, ObservedT, Func> System<'world, fn(Observe<'world, ObservedT>)> for Func
+where
+ ObservedT: Observed,
+ Func: Fn(Observe<'world, ObservedT>) + Copy + 'static,
+{
+ type Callbacks = NoCallbacks;
+
+ fn finish(self) -> (TypeErasedSystem, NoCallbacks)
+ {
+ const {
+ panic!("Observers cannot be used as regular systems");
+ }
+ }
+}
+
+impl<'world, ObservedT, Func> Observer<'world, fn(Observe<'world, ObservedT>)> for Func
+where
+ ObservedT: Observed,
+ Func: Fn(Observe<'world, ObservedT>) + Copy + 'static,
+{
+ type ObservedEvents = ObservedT::Events;
+
+ fn observed_events() -> Self::ObservedEvents
+ {
+ ObservedT::events()
+ }
+
+ fn finish_observer(self) -> (WrapperComponent, NoCallbacks)
+ {
+ let wrapper_comp = WrapperComponent {
+ run: Box::new(move |world, _metadata, emitted_event| {
+ // SAFETY: The caller of TypeErased::run ensures the lifetime
+ // is correct
+ let world = unsafe { &*std::ptr::from_ref(world) };
+
+ // SAFETY: The caller of TypeErased::run ensures the lifetime
+ // is correct
+ let emitted_event = unsafe {
+ transmute::<EmittedEvent<'_>, EmittedEvent<'_>>(emitted_event)
+ };
+
+ self(Observe::new(world, emitted_event));
+ }),
+ };
+
+ (wrapper_comp, NoCallbacks)
+ }
+}
+
+#[derive(Component)]
+pub struct WrapperComponent
+{
+ run: Box<RunFn>,
+}
+
+impl WrapperComponent
+{
+ pub fn new(run: impl Fn(&World, Metadata, EmittedEvent<'_>) + 'static) -> Self
+ {
+ Self { run: Box::new(run) }
+ }
+
+ /// Runs the observer system.
+ ///
+ /// # Safety
+ /// `world` must live at least as long as the [`World`] the system belongs to.
+ pub unsafe fn run(
+ &self,
+ world: &World,
+ metadata: Metadata,
+ emitted_event: EmittedEvent<'_>,
+ )
+ {
+ (self.run)(world, metadata, emitted_event);
+ }
+}
+
+impl Debug for WrapperComponent
+{
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
+ {
+ formatter
+ .debug_struct("WrapperComponent")
+ .finish_non_exhaustive()
+ }
+}
+
+type RunFn = dyn Fn(&World, Metadata, EmittedEvent<'_>);
diff --git a/ecs/src/system/stateful.rs b/ecs/src/system/stateful.rs
index 80ac346..e74ef31 100644
--- a/ecs/src/system/stateful.rs
+++ b/ecs/src/system/stateful.rs
@@ -1,150 +1,243 @@
-use std::any::{Any, TypeId};
+use std::mem::transmute;
use std::panic::{RefUnwindSafe, UnwindSafe};
-use hashbrown::HashMap;
use seq_macro::seq;
-use crate::component::Component;
-use crate::lock::Lock;
-use crate::system::{
- ComponentRefMut,
- Into as IntoSystem,
- Param,
- ParamWithInputFilter,
- System,
- TypeErased,
+use crate::component::local::SystemWithLocalComponents;
+use crate::component::Parts as ComponentParts;
+use crate::event::Emitted as EmittedEvent;
+use crate::system::initializable::{Initializable, MaybeInitializableParamTuple};
+use crate::system::observer::{
+ Observe,
+ Observed,
+ Observer,
+ WrapperComponent as ObserverWrapperComponent,
};
-use crate::tuple::{
- Reduce as TupleReduce,
- Tuple,
- WithAllElemLtStatic as TupleWithAllElemLtStatic,
-};
-use crate::uid::Uid;
+use crate::system::{Into as IntoSystem, Metadata, Param, System, TypeErased};
use crate::World;
/// A stateful system.
pub struct Stateful<Func>
{
func: Func,
- local_components: HashMap<Uid, Lock<Box<dyn Component>>>,
+ local_components: Vec<ComponentParts>,
}
macro_rules! impl_system {
($c: tt) => {
seq!(I in 0..$c {
- impl<'world, Func, #(TParam~I,)*> System<'world, fn(&'world (), #(TParam~I,)*)>
- for Stateful<Func>
+ impl<'world, Func, #(TParam~I,)*>
+ System<'world, fn(&'world (), #(TParam~I,)*)> for Stateful<Func>
where
Func: Fn(#(TParam~I,)*) + Copy + RefUnwindSafe + UnwindSafe + 'static,
- #(TParam~I: Param<'world>,)*
- #(TParam~I::Input: 'static,)*
- (#(TParam~I::Input,)*): TupleReduce<
- ParamWithInputFilter,
- Out: Tuple<InOptions: TupleWithAllElemLtStatic>
- >,
+ #(TParam~I: Param<'world, Input: 'static>,)*
{
- type Input =
- <(#(TParam~I::Input,)*) as TupleReduce<ParamWithInputFilter>>::Out;
+ type Callbacks = Callbacks;
- fn initialize(mut self, input: Self::Input) -> Self
+ fn finish(self) -> (TypeErased, Self::Callbacks)
{
- let mut option_input = input.into_in_options();
-
- let mut index = 0;
-
- #(
- if TypeId::of::<TParam~I::Input>() !=
- TypeId::of::<()>()
- {
- 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;
- }
- }
- )*
+ let Self { func, local_components } = self;
- self
+ let callbacks = Callbacks { local_components };
+
+ let type_erased = TypeErased {
+ run: Box::new(move |world, metadata| {
+ // SAFETY: The caller of TypeErased::run ensures the lifetime
+ // is correct
+ let world = unsafe { &*std::ptr::from_ref(world) };
+
+ func(#({
+ TParam~I::new(&world, &metadata)
+ },)*);
+ }),
+ };
+
+
+ (type_erased, callbacks)
}
+ }
- fn run<'this>(&'this self, world: &'world World)
- where
- 'this: 'world
+ impl<'world, Func, #(TParam~I,)*>
+ Initializable<'world, fn(&'world (), #(TParam~I,)*)> for Stateful<Func>
+ where
+ Func: Fn(#(TParam~I,)*) + Copy + RefUnwindSafe + UnwindSafe + 'static,
+ #(TParam~I: Param<'world, Input: 'static>,)*
+ (#(TParam~I,)*): MaybeInitializableParamTuple<'world, Self>
+ {
+ type Inputs = <
+ (#(TParam~I,)*) as MaybeInitializableParamTuple<'world, Self>
+ >::Inputs;
+
+ fn initialize(mut self, inputs: Self::Inputs) -> Self
{
- let func = self.func;
+ init_initializable_params::<_, (#(TParam~I,)*)>(&mut self, inputs);
- func(#({
- TParam~I::new(self, &world)
- },)*);
+ self
}
+ }
- fn into_type_erased(self) -> TypeErased
+ impl<'world, Func, #(TParam~I,)*> IntoSystem<'world, fn(#(TParam~I,)*)>
+ for Func
+ where
+ #(TParam~I: Param<'world>,)*
+ Func: Fn(#(TParam~I,)*) + Copy + 'static,
+ {
+ type System = Stateful<Func>;
+
+ fn into_system(self) -> Self::System
{
- TypeErased {
- data: Box::new(self),
- run: Box::new(|data, world| {
- // SAFETY: The caller of TypeErased::run ensures the lifetime
- // is correct
- let data = unsafe { &*std::ptr::from_ref::<dyn Any>(data) };
+ Self::System {
+ func: self,
+ local_components: Vec::new(), // TODO: Use Vec::with_capacity
+ }
+ }
+ }
+ });
+ };
+}
- let me = data.downcast_ref::<Self>().unwrap();
+seq!(C in 1..16 {
+ impl_system!(C);
+});
- // SAFETY: The caller of TypeErased::run ensures the lifetime
- // is correct
- let world = unsafe { &*std::ptr::from_ref(world) };
+impl<Func> SystemWithLocalComponents for Stateful<Func>
+{
+ fn add_local_component(&mut self, component_parts: ComponentParts)
+ {
+ self.local_components.push(component_parts);
+ }
+}
- me.run(world);
- }),
- }
+#[derive(Debug)]
+pub struct Callbacks
+{
+ local_components: Vec<ComponentParts>,
+}
+
+impl crate::system::Callbacks for Callbacks
+{
+ fn on_created(&mut self, world: &mut World, metadata: Metadata)
+ {
+ for local_comp_parts in self.local_components.drain(..) {
+ world.add_component(metadata.ent_id, local_comp_parts);
+ }
+ }
+}
+
+fn init_initializable_params<'world, SystemT, Params>(
+ system: &mut SystemT,
+ inputs: Params::Inputs,
+) where
+ Params: MaybeInitializableParamTuple<'world, SystemT>,
+{
+ Params::init_initializable(system, inputs);
+}
+
+macro_rules! impl_observer {
+ ($c: tt) => {
+ seq!(I in 0..$c {
+ impl<'world, ObservedT, Func, #(TParam~I,)*> System<
+ 'world,
+ fn(Observe<'world, ObservedT>, #(TParam~I,)*)
+ > for Stateful<Func>
+ where
+ ObservedT: Observed,
+ Func: Fn(Observe<'world, ObservedT>, #(TParam~I,)*) + Copy + 'static,
+ #(TParam~I: Param<'world>,)*
+ {
+ type Callbacks = Callbacks;
+
+ fn finish(self) -> (TypeErased, Callbacks)
+ {
+ unimplemented!();
}
+ }
- fn get_local_component_mut<LocalComponent: Component>(
- &self,
- ) -> Option<ComponentRefMut<LocalComponent>>
+ impl<'world, ObservedT, Func, #(TParam~I,)*> Initializable<
+ 'world,
+ fn(Observe<'world, ObservedT>, #(TParam~I,)*)
+ > for Stateful<Func>
+ where
+ ObservedT: Observed,
+ Func: Fn(Observe<'world, ObservedT>, #(TParam~I,)*) + Copy + 'static,
+ #(TParam~I: Param<'world>,)*
+ (#(TParam~I,)*): MaybeInitializableParamTuple<'world, Self>
+ {
+ type Inputs = <
+ (#(TParam~I,)*) as MaybeInitializableParamTuple<'world, Self>
+ >::Inputs;
+
+ fn initialize(mut self, inputs: Self::Inputs) -> Self
{
- let local_component = self.local_components
- .get(&LocalComponent::id())?
- .write_nonblock()
- .expect("Failed to aquire read-write local component lock");
+ init_initializable_params::<_, (#(TParam~I,)*)>(&mut self, inputs);
- Some(ComponentRefMut::new(local_component))
+ self
}
+ }
+
+ impl<'world, ObservedT, Func, #(TParam~I,)*> Observer<
+ 'world,
+ fn(Observe<'world, ObservedT>, #(TParam~I,)*)
+ > for Stateful<Func>
+ where
+ ObservedT: Observed,
+ Func: Fn(Observe<'world, ObservedT>, #(TParam~I,)*) + Copy + 'static,
+ #(TParam~I: Param<'world>,)*
+ {
+ type ObservedEvents = ObservedT::Events;
- fn set_local_component<LocalComponent: Component>(
- &mut self,
- local_component: LocalComponent,
- )
+ fn observed_events() -> Self::ObservedEvents
{
- self.local_components
- .insert(
- LocalComponent::id(),
- Lock::new(Box::new(local_component))
- );
+ ObservedT::events()
+ }
+
+ fn finish_observer(self) -> (ObserverWrapperComponent, Callbacks)
+ {
+ let Self { func, local_components } = self;
+
+ let callbacks = Callbacks { local_components };
+
+ let wrapper_comp = ObserverWrapperComponent::new(
+ move |world, metadata, emitted_event| {
+ // SAFETY: The caller of TypeErased::run ensures the lifetime
+ // is correct
+ let world = unsafe { &*std::ptr::from_ref(world) };
+
+ // SAFETY: The caller of TypeErased::run ensures the lifetime
+ // is correct
+ let emitted_event = unsafe {
+ transmute::<EmittedEvent<'_>, EmittedEvent<'_>>(
+ emitted_event
+ )
+ };
+
+ func(Observe::new(world, emitted_event), #({
+ TParam~I::new(world, &metadata)
+ },)*);
+ },
+ );
+
+ (wrapper_comp, callbacks)
}
}
- impl<Func, #(TParam~I,)*> IntoSystem<fn(#(TParam~I,)*)>
- for Func
+ impl<'world, Func, ObservedT, #(TParam~I,)*> IntoSystem<
+ 'world,
+ fn(Observe<'world, ObservedT>,
+ #(TParam~I,)*)
+ > for Func
where
- Func: Fn(#(TParam~I,)*) + Copy + 'static,
+ ObservedT: Observed,
+ #(TParam~I: Param<'world>,)*
+ Func: Fn(Observe<'world, ObservedT>, #(TParam~I,)*) + Copy + 'static,
{
type System = Stateful<Func>;
- fn into_system(self) -> Self::System
+ fn into_system(self) -> Stateful<Func>
{
- Self::System {
+ Stateful {
func: self,
- local_components: HashMap::new(),
+ local_components: Vec::new(), // TODO: Use Vec::with_capacity
}
}
}
@@ -153,5 +246,5 @@ macro_rules! impl_system {
}
seq!(C in 1..16 {
- impl_system!(C);
+ impl_observer!(C);
});
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 60167d3..bb393a1 100644
--- a/ecs/src/uid.rs
+++ b/ecs/src/uid.rs
@@ -1,23 +1,31 @@
+use std::fmt::{Debug, Display, Formatter};
use std::mem::transmute;
use std::sync::atomic::{AtomicU32, Ordering};
-use crate::util::{gen_mask_64, BitMask, NumberExt};
+use seq_macro::seq;
-static NEXT: AtomicU32 = AtomicU32::new(1);
+use crate::component::Component;
+use crate::util::{gen_mask_64, Array, BitMask, NumberExt};
+
+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,
@@ -25,21 +33,229 @@ 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 = NEXT.fetch_add(1, Ordering::Relaxed);
Self {
- inner: ID_BITS.field_prep(id as u64) | KIND_BITS.field_prep(kind as u64),
+ inner: ID_BITS.field_prep(u64::from(id)) | KIND_BITS.field_prep(kind as u64),
+ }
+ }
+
+ #[must_use]
+ pub fn wildcard() -> Self
+ {
+ Self {
+ 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.field_get(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
+{
+ fn uid() -> Uid;
}
+
+impl<ComponentT: Component> With for ComponentT
+{
+ fn uid() -> Uid
+ {
+ Self::id()
+ }
+}
+
+pub trait WithUidTuple
+{
+ type UidsArray: Array<Uid>;
+
+ fn uids() -> Self::UidsArray;
+}
+
+macro_rules! impl_with_uid_tuple {
+ ($c: tt) => {
+ seq!(I in 0..=$c {
+ impl<#(WithUid~I: With,)*> WithUidTuple for (#(WithUid~I,)*)
+ {
+ type UidsArray = [Uid; $c + 1];
+
+ fn uids() -> Self::UidsArray
+ {
+ [#(WithUid~I::uid(),)*]
+ }
+ }
+ });
+ };
+}
+
+seq!(C in 0..=16 {
+ impl_with_uid_tuple!(C);
+});
diff --git a/ecs/src/util.rs b/ecs/src/util.rs
index 4ba8597..27e9748 100644
--- a/ecs/src/util.rs
+++ b/ecs/src/util.rs
@@ -1,9 +1,177 @@
-use std::ops::BitAnd;
+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_part_pt_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_part_pt_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
{
@@ -76,6 +244,7 @@ impl BitMask<u64>
Self { mask }
}
+ #[must_use]
pub const fn value(self) -> u64
{
self.mask
@@ -85,6 +254,8 @@ impl BitMask<u64>
#[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)
}
}
@@ -102,6 +273,7 @@ impl BitAnd<u64> for BitMask<u64>
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;
}
@@ -128,6 +300,88 @@ macro_rules! gen_mask_64 {
pub(crate) use gen_mask_64;
+macro_rules! impl_multiple {
+ (
+ $type: ident,
+ ($(
+ impl$(<$($generic: tt$(: $bound: ident)?),*>)?
+ _<$($lt_param: lifetime),*><$($type_param: ty),*>
+ $(($($extra_cb_arg: expr),*))?
+ ),*)
+ cb=(
+ type_params=($($ty_param_matcher: ident),*)
+ $(, $($extra_matcher: ident),+)?
+ ) => {
+ $($item_tt: tt)*
+ }
+ ) => {
+ const _: () = {
+ $crate::util::impl_multiple!(
+ @(make_gen_item_macro)
+ _gen_multiple_impl_item,
+ ($($ty_param_matcher),*),
+ ($($($extra_matcher),+)?),
+ ($($item_tt)*)
+ );
+
+ $(
+ impl $(<$($generic$(: $bound)?,)*>)? $type<$($lt_param,)* $($type_param),*>
+ {
+ _gen_multiple_impl_item!(
+ type_params=($($type_param),*),
+ $($($extra_cb_arg),*)?
+ );
+ }
+ )*
+ };
+ };
+
+ (
+ @(make_gen_item_macro)
+ $name: ident,
+ ($($ty_param_matcher: ident),*),
+ ($($extra_matcher: ident),*),
+ ($($transcriber: tt)*)
+ ) => {
+ $crate::util::impl_multiple!(
+ @(make_gen_item_macro)
+ ($),
+ $name,
+ ($($ty_param_matcher),*),
+ ($($extra_matcher),*),
+ ($($transcriber)*)
+ );
+ };
+
+ (
+ @(make_gen_item_macro)
+ ($dollar: tt),
+ $name: ident,
+ ($($ty_param_matcher: ident),*),
+ ($($extra_matcher: ident),*),
+ ($($transcriber: tt)*)
+ ) => {
+ $crate::util::impl_multiple!(
+ @(make_gen_item_macro)
+ $name,
+ (
+ type_params=($($dollar$ty_param_matcher: ty),*),
+ $($dollar$extra_matcher: expr),*
+ ) => {
+ $($transcriber)*
+ }
+ );
+ };
+
+ (@(make_gen_item_macro) $name: ident, $($rule: tt)*) => {
+ macro_rules! $name {
+ $($rule)*
+ }
+ };
+}
+
+pub(crate) use impl_multiple;
+
mod sealed
{
pub trait Sealed {}
@@ -151,4 +405,11 @@ mod tests
{
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..5d0aac9
--- /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(
+ &raw const self.items[index],
+ &raw 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..7b218e3
--- /dev/null
+++ b/ecs/tests/query.rs
@@ -0,0 +1,413 @@
+use ecs::component::Component;
+use ecs::pair::{Pair, Wildcard};
+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],
+ );
+}
+
+#[test]
+fn query_with_wildcard_target_pair()
+{
+ setup();
+
+ let _test_lock = TEST_LOCK.lock();
+
+ let mut world = World::new();
+
+ let ent_1_id = world.create_entity((A, C));
+
+ world.create_entity((B,));
+
+ let ent_2_id = world.create_entity((
+ B,
+ Pair::builder().relation::<G>().target_id(ent_1_id).build(),
+ ));
+
+ world.create_entity((
+ B,
+ Pair::builder().relation::<F>().target_id(ent_1_id).build(),
+ ));
+ world.create_entity((
+ B,
+ A,
+ C,
+ Pair::builder().relation::<F>().target_id(ent_1_id).build(),
+ ));
+
+ let ent_3_id = world.create_entity((
+ B,
+ Pair::builder().relation::<G>().target_id(ent_2_id).build(),
+ ));
+
+ let ent_4_id = world.create_entity((
+ B,
+ E,
+ Pair::builder().relation::<G>().target_as_data(D).build(),
+ ));
+
+ assert_query_finds_ents(
+ world.query::<(&B, Pair<G, Wildcard>), ()>(),
+ vec![ent_2_id, ent_3_id, ent_4_id],
+ );
+}
+
+#[test]
+fn query_with_component_target_pair()
+{
+ setup();
+
+ let _test_lock = TEST_LOCK.lock();
+
+ let mut world = World::new();
+
+ let ent_1_id = world.create_entity((A, C));
+
+ world.create_entity((B,));
+
+ world.create_entity((
+ B,
+ Pair::builder().relation::<G>().target_id(ent_1_id).build(),
+ ));
+
+ world.create_entity((
+ B,
+ Pair::builder().relation::<F>().target_id(ent_1_id).build(),
+ ));
+ world.create_entity((
+ B,
+ A,
+ C,
+ Pair::builder().relation::<F>().target_id(ent_1_id).build(),
+ ));
+
+ let ent_2_id = world
+ .create_entity((B, Pair::builder().relation::<G>().target_as_data(F).build()));
+
+ let ent_3_id = world.create_entity((
+ B,
+ E,
+ Pair::builder().relation::<G>().target_as_data(F).build(),
+ ));
+
+ assert_query_finds_ents(
+ world.query::<(&B, Pair<G, &F>), ()>(),
+ vec![ent_2_id, ent_3_id],
+ );
+}
diff --git a/engine/Cargo.toml b/engine/Cargo.toml
index f6cd5cf..6ddcf12 100644
--- a/engine/Cargo.toml
+++ b/engine/Cargo.toml
@@ -4,17 +4,31 @@ version = "0.1.0"
edition = "2021"
[dependencies]
-glfw = { path = "../glfw", features = ["opengl"] }
+glutin = "0.32.3"
+raw-window-handle = "0.6.2"
thiserror = "1.0.49"
-gl = "0.14.0"
bitflags = "2.4.0"
tracing = "0.1.39"
seq-macro = "0.3.5"
paste = "1.0.14"
+parking_lot = "0.12.3"
+crossbeam-channel = "0.5.15"
+safer-ffi = "0.1.13"
+nu-ansi-term = "0.46.0"
ecs = { path = "../ecs" }
util-macros = { path = "../util-macros" }
+opengl-bindings = { path = "../opengl-bindings" }
-[dependencies.image]
+[dependencies.winit]
+version = "0.30.11"
+default-features = false
+features = ["rwh_06", "wayland", "wayland-dlopen", "x11"]
+
+[dependencies.image_rs]
version = "0.24.7"
default-features = false
features = ["png", "jpeg"]
+package = "image"
+
+[build-dependencies]
+cfg_aliases = "0.2.1"
diff --git a/engine/build.rs b/engine/build.rs
new file mode 100644
index 0000000..58029fc
--- /dev/null
+++ b/engine/build.rs
@@ -0,0 +1,63 @@
+// Original file:
+// https://github.com/rust-windowing/glutin/blob/
+// 0433af9018febe0696c485ed9d66c40dad41f2d4/glutin-winit/build.rs
+//
+// Copyright © 2022 Kirill Chibisov
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the “Software”), to deal
+// in the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+// Keep this in sync with glutin's build.rs.
+
+use cfg_aliases::cfg_aliases;
+
+fn main()
+{
+ // Setup alias to reduce `cfg` boilerplate.
+ cfg_aliases! {
+ // Systems.
+ android_platform: { target_os = "android" },
+ wasm_platform: { target_family = "wasm" },
+ macos_platform: { target_os = "macos" },
+ ios_platform: { target_os = "ios" },
+ apple: { any(ios_platform, macos_platform) },
+ free_unix: { all(unix, not(apple), not(android_platform)) },
+
+ // Native displays.
+ x11_platform: { all(free_unix, not(wasm_platform)) },
+ wayland_platform: { all(free_unix, not(wasm_platform)) },
+ // x11_platform: { all(feature = "x11", free_unix, not(wasm_platform)) },
+ // wayland_platform: { all(feature = "wayland", free_unix, not(wasm_platform)) },
+
+ // Backends.
+ egl_backend: {
+ all(any(windows, unix), not(apple), not(wasm_platform))
+ },
+ glx_backend: { all(x11_platform, not(wasm_platform)) },
+ wgl_backend: { all(windows, not(wasm_platform)) },
+ cgl_backend: { all(macos_platform, not(wasm_platform)) },
+
+ // Backends.
+ // egl_backend: {
+ // all(feature = "egl", any(windows, unix), not(apple), not(wasm_platform))
+ // },
+ // glx_backend: { all(feature = "glx", x11_platform, not(wasm_platform)) },
+ // wgl_backend: { all(feature = "wgl", windows, not(wasm_platform)) },
+ // cgl_backend: { all(macos_platform, not(wasm_platform)) },
+ }
+}
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 087f727..a034851 100644
--- a/engine/src/camera/fly.rs
+++ b/engine/src/camera/fly.rs
@@ -1,14 +1,16 @@
use ecs::component::local::Local;
use ecs::phase::UPDATE as UPDATE_PHASE;
use ecs::sole::Single;
-use ecs::system::{Into, System};
+use ecs::system::initializable::Initializable;
+use ecs::system::Into;
use ecs::{Component, Query};
+use crate::builder;
use crate::camera::{Active as ActiveCamera, Camera};
use crate::delta_time::DeltaTime;
-use crate::input::{Cursor, CursorFlags, Key, KeyState, Keys};
-use crate::transform::Position;
-use crate::util::builder;
+use crate::input::keyboard::{Key, KeyState, Keyboard};
+use crate::input::mouse::Motion as MouseMotion;
+use crate::transform::WorldPosition;
use crate::vector::{Vec2, Vec3};
builder! {
@@ -59,12 +61,7 @@ impl ecs::extension::Extension for Extension
{
fn collect(self, mut collector: ecs::extension::Collector<'_>)
{
- collector.add_system(
- *UPDATE_PHASE,
- update
- .into_system()
- .initialize((CursorState::default(), self.0)),
- );
+ collector.add_system(*UPDATE_PHASE, update.into_system().initialize((self.0,)));
}
}
@@ -75,36 +72,30 @@ pub struct Options
}
fn update(
- camera_query: Query<(&mut Camera, &mut Position, &mut Fly, &ActiveCamera)>,
- keys: Single<Keys>,
- cursor: Single<Cursor>,
- cursor_flags: Single<CursorFlags>,
+ camera_query: Query<(&mut Camera, &mut WorldPosition, &mut Fly, &ActiveCamera)>,
+ keyboard: Single<Keyboard>,
+ mouse_motion: Single<MouseMotion>,
delta_time: Single<DeltaTime>,
- mut cursor_state: Local<CursorState>,
options: Local<Options>,
)
{
- for (mut camera, mut camera_pos, mut fly_camera, _) in &camera_query {
- if cursor.has_moved && cursor_flags.is_first_move.flag {
- tracing::debug!("First cursor move");
-
- cursor_state.last_pos = cursor.position;
- }
-
+ for (mut camera, mut camera_world_pos, mut fly_camera, _) in &camera_query {
let delta_time = delta_time.duration;
- let mut x_offset = cursor.position.x - cursor_state.last_pos.x;
- let mut y_offset = cursor_state.last_pos.y - cursor.position.y;
+ // tracing::info!("Mouse motion: {:?}", mouse_motion.position_delta);
- cursor_state.last_pos = cursor.position;
+ if mouse_motion.position_delta != (Vec2 { x: 0.0, y: 0.0 }) {
+ let x_offset =
+ mouse_motion.position_delta.x * f64::from(options.mouse_sensitivity);
- x_offset *= f64::from(options.mouse_sensitivity);
- y_offset *= f64::from(options.mouse_sensitivity);
+ let y_offset =
+ (-mouse_motion.position_delta.y) * f64::from(options.mouse_sensitivity);
- fly_camera.current_yaw += x_offset;
- fly_camera.current_pitch += y_offset;
+ fly_camera.current_yaw += x_offset;
+ fly_camera.current_pitch += y_offset;
- fly_camera.current_pitch = fly_camera.current_pitch.clamp(-89.0, 89.0);
+ fly_camera.current_pitch = fly_camera.current_pitch.clamp(-89.0, 89.0);
+ }
// TODO: This casting to a f32 from a f64 is horrible. fix it
#[allow(clippy::cast_possible_truncation)]
@@ -121,35 +112,30 @@ fn update(
camera.global_up = cam_right.cross(&direction).normalize();
- if keys.get_key_state(Key::W) == KeyState::Pressed {
- camera_pos.position +=
+ if keyboard.get_key_state(Key::W) == KeyState::Pressed {
+ camera_world_pos.position +=
direction * fly_camera.speed * delta_time.as_secs_f32();
}
- if keys.get_key_state(Key::S) == KeyState::Pressed {
- camera_pos.position -=
+ if keyboard.get_key_state(Key::S) == KeyState::Pressed {
+ camera_world_pos.position -=
direction * fly_camera.speed * delta_time.as_secs_f32();
}
- if keys.get_key_state(Key::A) == KeyState::Pressed {
+ if keyboard.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 keys.get_key_state(Key::D) == KeyState::Pressed {
+ if keyboard.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;
}
}
-
-#[derive(Debug, Default, Component)]
-struct CursorState
-{
- last_pos: Vec2<f64>,
-}
diff --git a/engine/src/data_types/color.rs b/engine/src/data_types/color.rs
index cef3b92..c5316e6 100644
--- a/engine/src/data_types/color.rs
+++ b/engine/src/data_types/color.rs
@@ -1,7 +1,6 @@
use std::ops::{Add, Div, Mul, Neg, Sub};
#[derive(Debug, Clone, Default)]
-#[repr(C)]
pub struct Color<Value>
{
pub red: Value,
diff --git a/engine/src/data_types/dimens.rs b/engine/src/data_types/dimens.rs
index b395627..8bf239f 100644
--- a/engine/src/data_types/dimens.rs
+++ b/engine/src/data_types/dimens.rs
@@ -1,7 +1,70 @@
-/// Dimensions.
+use std::num::NonZeroU32;
+
+/// 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 }
+ }
+}
+
+impl Dimens<u32>
+{
+ #[must_use]
+ pub fn try_into_nonzero(self) -> Option<Dimens<NonZeroU32>>
+ {
+ Some(Dimens {
+ width: NonZeroU32::new(self.width)?,
+ height: NonZeroU32::new(self.height)?,
+ })
+ }
+}
+
+/// 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/matrix.rs b/engine/src/data_types/matrix.rs
index 3a29ae2..b754b62 100644
--- a/engine/src/data_types/matrix.rs
+++ b/engine/src/data_types/matrix.rs
@@ -4,7 +4,7 @@ use crate::vector::Vec3;
pub struct Matrix<Value, const ROWS: usize, const COLUMNS: usize>
{
/// Items must be layed out this way for it to work with OpenGL shaders.
- items: [[Value; ROWS]; COLUMNS],
+ pub items: [[Value; ROWS]; COLUMNS],
}
impl<Value, const ROWS: usize, const COLUMNS: usize> Matrix<Value, ROWS, COLUMNS>
diff --git a/engine/src/data_types/vector.rs b/engine/src/data_types/vector.rs
index 802a4a7..dc6df30 100644
--- a/engine/src/data_types/vector.rs
+++ b/engine/src/data_types/vector.rs
@@ -14,6 +14,29 @@ impl Vec2<u32>
pub const ZERO: Self = Self { x: 0, y: 0 };
}
+impl<Value> Add for Vec2<Value>
+where
+ Value: Add<Value, Output = Value>,
+{
+ type Output = Self;
+
+ fn add(self, rhs: Self) -> Self::Output
+ {
+ Self::Output { x: self.x + rhs.x, y: self.y + rhs.y }
+ }
+}
+
+impl<Value> AddAssign for Vec2<Value>
+where
+ Value: Add<Value, Output = Value> + Clone,
+{
+ fn add_assign(&mut self, rhs: Self)
+ {
+ self.x = self.x.clone() + rhs.x;
+ self.y = self.y.clone() + rhs.y;
+ }
+}
+
impl<Value> Add<Value> for Vec2<Value>
where
Value: Add<Output = Value> + Clone,
@@ -75,7 +98,6 @@ where
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
-#[repr(C)]
pub struct Vec3<Value>
{
pub x: 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/file_format/wavefront/mtl.rs b/engine/src/file_format/wavefront/mtl.rs
index d90dbcf..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`.
///
@@ -50,18 +48,41 @@ pub fn parse(obj_content: &str) -> Result<Vec<NamedMaterial>, Error>
}
#[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)]
@@ -70,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}"
@@ -100,59 +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 {
- 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)?;
- 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)?;
- 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)?;
- 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 {
@@ -165,51 +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))?;
-
- 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,
+ });
+ }
- 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,
+ });
+ }
- 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 {
- 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)
@@ -235,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 {
@@ -271,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 6ca11c2..88d566c 100644
--- a/engine/src/file_format/wavefront/obj.rs
+++ b/engine/src/file_format/wavefront/obj.rs
@@ -13,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`.
///
@@ -168,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 95de048..f8c9dfd 100644
--- a/engine/src/input.rs
+++ b/engine/src/input.rs
@@ -1,252 +1,32 @@
-use std::collections::HashMap;
-
+use ecs::declare_entity;
use ecs::extension::Collector as ExtensionCollector;
-use ecs::phase::{Phase, PRE_UPDATE as PRE_UPDATE_PHASE, START as START_PHASE};
-use ecs::relationship::{ChildOf, Relationship};
-use ecs::sole::Single;
-use ecs::{static_entity, Sole};
-
-use crate::vector::Vec2;
-use crate::window::{Window, UPDATE_PHASE as WINDOW_UPDATE_PHASE};
+use ecs::pair::{DependsOn, Pair};
+use ecs::phase::Phase;
-mod reexports
-{
- pub use crate::window::{Key, KeyState};
-}
+use crate::windowing::PHASE as WINDOWING_PHASE;
-pub use reexports::*;
+pub mod keyboard;
+pub mod mouse;
-static_entity!(
- SET_PREV_KEY_STATE_PHASE,
+declare_entity!(
+ pub PHASE,
(
Phase,
- <Relationship<ChildOf, Phase>>::new(*WINDOW_UPDATE_PHASE)
+ Pair::builder()
+ .relation::<DependsOn>()
+ .target_id(*WINDOWING_PHASE)
+ .build()
)
);
-#[derive(Debug, Sole)]
-pub struct Keys
-{
- map: HashMap<Key, KeyData>,
- pending: Vec<(Key, KeyState)>,
-}
-
-impl Keys
-{
- #[must_use]
- pub fn new() -> Self
- {
- Self {
- map: Key::KEYS
- .iter()
- .map(|key| {
- (
- *key,
- KeyData {
- state: KeyState::Released,
- prev_tick_state: KeyState::Released,
- },
- )
- })
- .collect(),
- pending: Vec::with_capacity(Key::KEYS.len()),
- }
- }
-
- #[must_use]
- pub fn get_key_state(&self, key: Key) -> KeyState
- {
- let Some(key_data) = self.map.get(&key) else {
- unreachable!();
- };
-
- key_data.state
- }
-
- #[must_use]
- pub fn get_prev_key_state(&self, key: Key) -> KeyState
- {
- let Some(key_data) = self.map.get(&key) else {
- unreachable!();
- };
-
- key_data.prev_tick_state
- }
-
- pub fn set_key_state(&mut self, key: Key, new_key_state: KeyState)
- {
- let Some(key_data) = self.map.get_mut(&key) else {
- unreachable!();
- };
-
- key_data.state = new_key_state;
- }
-
- #[must_use]
- pub fn is_anything_pressed(&self) -> bool
- {
- self.map
- .values()
- .any(|key_data| matches!(key_data.state, KeyState::Pressed))
- }
-}
-
-impl Default for Keys
-{
- fn default() -> Self
- {
- Self::new()
- }
-}
-
-#[derive(Debug, Default, Clone, Sole)]
-pub struct Cursor
-{
- pub position: Vec2<f64>,
- pub has_moved: bool,
-}
-
-#[derive(Debug, Clone, Sole)]
-pub struct CursorFlags
-{
- /// This flag is set in two situations:
- /// A: The window has just started
- /// B: The window has gained focus again after losing focus.
- ///
- /// This flag only lasts a single tick then it is cleared (at the beginning of the
- /// next tick).
- pub is_first_move: CursorFlag,
-}
-
-impl Default for CursorFlags
-{
- fn default() -> Self
- {
- Self {
- is_first_move: CursorFlag { flag: true, ..Default::default() },
- }
- }
-}
-
-#[derive(Debug, Default, Clone)]
-pub struct CursorFlag
-{
- pub flag: bool,
- pub pending_clear: bool,
-}
-
-impl CursorFlag
-{
- pub fn clear(&mut self)
- {
- self.flag = false;
- self.pending_clear = false;
- }
-}
-
/// Input extension.
#[derive(Debug, Default)]
pub struct Extension {}
impl ecs::extension::Extension for Extension
{
- fn collect(self, mut collector: ExtensionCollector<'_>)
+ fn collect(self, _collector: ExtensionCollector<'_>)
{
- 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();
- collector.add_sole(CursorFlags::default()).ok();
+ // TODO: Add input mapping
}
}
-
-fn initialize(
- keys: Single<Keys>,
- cursor: Single<Cursor>,
- cursor_flags: Single<CursorFlags>,
- window: Single<Window>,
-)
-{
- let keys_weak_ref = keys.to_weak_ref();
-
- window.set_key_callback(move |key, _scancode, key_state, _modifiers| {
- let keys_ref = keys_weak_ref.access().expect("No world");
-
- let mut keys = keys_ref.to_single();
-
- keys.pending.push((key, key_state));
- });
-
- let cursor_weak_ref = cursor.to_weak_ref();
-
- window.set_cursor_pos_callback(move |cursor_position| {
- let cursor_ref = cursor_weak_ref.access().expect("No world");
-
- let mut cursor = cursor_ref.to_single();
-
- cursor.position = Vec2 {
- x: cursor_position.x,
- y: cursor_position.y,
- };
-
- cursor.has_moved = true;
- });
-
- let cursor_flags_weak_ref = cursor_flags.to_weak_ref();
-
- window.set_focus_callback(move |is_focused| {
- tracing::trace!("Window is focused: {is_focused}");
-
- let cursor_flags_ref = cursor_flags_weak_ref.access().expect("No world");
-
- cursor_flags_ref.to_single().is_first_move.flag = is_focused;
- });
-}
-
-fn maybe_clear_cursor_is_first_move(
- cursor: Single<Cursor>,
- mut cursor_flags: Single<CursorFlags>,
-)
-{
- if cursor_flags.is_first_move.pending_clear {
- tracing::trace!("Clearing is_first_move");
-
- // This flag was set for the whole previous tick so it can be cleared now
- cursor_flags.is_first_move.clear();
-
- return;
- }
-
- if cursor.has_moved && cursor_flags.is_first_move.flag {
- tracing::trace!("Setting flag to clear is_first_move next tick");
-
- // Make this system clear is_first_move the next time it runs
- cursor_flags.is_first_move.pending_clear = true;
- }
-}
-
-fn set_pending_key_states(mut keys: Single<Keys>)
-{
- 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)]
-struct KeyData
-{
- state: KeyState,
- prev_tick_state: KeyState,
-}
diff --git a/engine/src/input/keyboard.rs b/engine/src/input/keyboard.rs
new file mode 100644
index 0000000..d226df0
--- /dev/null
+++ b/engine/src/input/keyboard.rs
@@ -0,0 +1,6 @@
+mod reexports
+{
+ pub use crate::windowing::keyboard::{Key, KeyState, Keyboard};
+}
+
+pub use reexports::*;
diff --git a/engine/src/input/mouse.rs b/engine/src/input/mouse.rs
new file mode 100644
index 0000000..90091f3
--- /dev/null
+++ b/engine/src/input/mouse.rs
@@ -0,0 +1,6 @@
+mod reexports
+{
+ pub use crate::windowing::mouse::{Button, ButtonState, Buttons, Motion};
+}
+
+pub use reexports::*;
diff --git a/engine/src/lib.rs b/engine/src/lib.rs
index a9a5a97..d5531c1 100644
--- a/engine/src/lib.rs
+++ b/engine/src/lib.rs
@@ -1,42 +1,50 @@
#![deny(clippy::all, clippy::pedantic)]
#![allow(clippy::needless_pass_by_value)]
-use ecs::component::{Component, Sequence as ComponentSequence};
+use ecs::component::Sequence as ComponentSequence;
use ecs::extension::Extension;
use ecs::phase::PRE_UPDATE as PRE_UPDATE_PHASE;
use ecs::sole::Sole;
+use ecs::system::initializable::Initializable;
+use ecs::system::observer::Observer;
use ecs::system::{Into, System};
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};
mod opengl;
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 file_format;
+pub mod image;
pub mod input;
pub mod lighting;
pub mod material;
pub mod math;
pub mod mesh;
+pub mod model;
pub mod projection;
pub mod renderer;
pub mod texture;
pub mod transform;
-pub mod vertex;
-pub mod window;
+pub mod windowing;
pub extern crate ecs;
pub(crate) use crate::data_types::matrix;
pub use crate::data_types::{color, vector};
+const INITIAL_ASSET_CAPACITY: usize = 128;
+
#[derive(Debug)]
pub struct Engine
{
@@ -49,6 +57,9 @@ impl Engine
#[must_use]
pub fn new() -> Self
{
+ #[cfg(windows)]
+ nu_ansi_term::enable_ansi_support().unwrap();
+
let mut world = World::new();
world.add_sole(DeltaTime::default()).ok();
@@ -60,6 +71,13 @@ impl Engine
.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 }
}
@@ -79,14 +97,12 @@ impl Engine
self.world.register_system(phase_euid, system);
}
- pub fn register_observer_system<'this, SystemImpl, Event>(
+ pub fn register_observer<'this, SystemImpl>(
&'this mut self,
- system: impl System<'this, SystemImpl>,
- event: Event,
- ) where
- Event: Component,
+ observer: impl Observer<'this, SystemImpl>,
+ )
{
- self.world.register_observer_system(system, event);
+ self.world.register_observer(observer);
}
/// Adds a globally shared singleton value.
@@ -104,7 +120,7 @@ impl Engine
}
/// Runs the event loop.
- pub fn start(&self)
+ pub fn start(&mut self)
{
self.world.start_loop();
}
diff --git a/engine/src/lighting.rs b/engine/src/lighting.rs
index 48adb0e..9ab2ca8 100644
--- a/engine/src/lighting.rs
+++ b/engine/src/lighting.rs
@@ -1,8 +1,8 @@
use ecs::{Component, Sole};
+use crate::builder;
use crate::color::Color;
use crate::data_types::vector::Vec3;
-use crate::util::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(),
@@ -58,7 +59,6 @@ pub struct AttenuationParams
impl Default for AttenuationParams
{
- #[must_use]
fn default() -> Self
{
Self {
diff --git a/engine/src/material.rs b/engine/src/material.rs
index e368519..56ff15f 100644
--- a/engine/src/material.rs
+++ b/engine/src/material.rs
@@ -1,21 +1,19 @@
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,
}
@@ -27,17 +25,24 @@ impl Material
}
}
+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,
}
@@ -47,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,
}
}
@@ -61,7 +65,7 @@ impl Builder
#[must_use]
pub fn ambient(mut self, ambient: Color<f32>) -> Self
{
- self.ambient = Some(ambient);
+ self.ambient = ambient;
self
}
@@ -69,7 +73,7 @@ impl Builder
#[must_use]
pub fn diffuse(mut self, diffuse: Color<f32>) -> Self
{
- self.diffuse = Some(diffuse);
+ self.diffuse = diffuse;
self
}
@@ -77,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);
@@ -91,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);
@@ -99,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);
@@ -107,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;
@@ -135,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,
}
}
@@ -206,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/mesh.rs b/engine/src/mesh.rs
index 917e7f7..fb977af 100644
--- a/engine/src/mesh.rs
+++ b/engine/src/mesh.rs
@@ -1,11 +1,9 @@
-use ecs::Component;
-
-use crate::vector::Vec3;
-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>,
@@ -77,6 +75,27 @@ impl Mesh
}
}
+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>
{
diff --git a/engine/src/mesh/cube.rs b/engine/src/mesh/cube.rs
index c29ce0b..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::mesh::{Mesh, Vertex};
+use crate::builder;
use crate::vector::{Vec2, Vec3};
-use crate::vertex::{Builder as VertexBuilder, Vertex};
builder! {
/// Cube mesh creation specification.
@@ -27,6 +27,18 @@ impl CreationSpec
}
}
+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
@@ -192,7 +204,7 @@ impl FaceVertices
.expect("Vertex normal index is out of bounds")
.clone();
- VertexBuilder::default()
+ Vertex::builder()
.pos(vertex_pos)
.normal(vertex_normal)
.build()
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
deleted file mode 100644
index 2be7f12..0000000
--- a/engine/src/opengl/buffer.rs
+++ /dev/null
@@ -1,92 +0,0 @@
-use std::marker::PhantomData;
-use std::mem::size_of_val;
-
-#[derive(Debug)]
-pub struct Buffer<Item>
-{
- buf: gl::types::GLuint,
- _pd: PhantomData<Item>,
-}
-
-impl<Item> Buffer<Item>
-{
- pub fn new() -> Self
- {
- let mut buffer = gl::types::GLuint::default();
-
- unsafe {
- gl::CreateBuffers(1, &mut buffer);
- };
-
- Self { buf: buffer, _pd: PhantomData }
- }
-
- /// Stores items in the currently bound buffer.
- pub fn store(&mut self, items: &[Item], usage: Usage)
- {
- unsafe {
- #[allow(clippy::cast_possible_wrap)]
- gl::NamedBufferData(
- self.buf,
- size_of_val(items) as gl::types::GLsizeiptr,
- items.as_ptr().cast(),
- usage.into_gl(),
- );
- }
- }
-
- pub fn object(&self) -> gl::types::GLuint
- {
- self.buf
- }
-
- /// 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 }
- }
-}
-
-impl<Item> Drop for Buffer<Item>
-{
- fn drop(&mut self)
- {
- unsafe {
- gl::DeleteBuffers(1, &self.buf);
- }
- }
-}
-
-/// Buffer usage.
-#[derive(Debug)]
-#[allow(dead_code)]
-pub enum Usage
-{
- /// The buffer data is set only once and used by the GPU at most a few times.
- Stream,
-
- /// The buffer data is set only once and used many times.
- Static,
-
- /// The buffer data is changed a lot and used many times.
- Dynamic,
-}
-
-impl Usage
-{
- fn into_gl(self) -> gl::types::GLenum
- {
- match self {
- Self::Stream => gl::STREAM_DRAW,
- Self::Static => gl::STATIC_DRAW,
- Self::Dynamic => gl::DYNAMIC_DRAW,
- }
- }
-}
diff --git a/engine/src/opengl/debug.rs b/engine/src/opengl/debug.rs
deleted file mode 100644
index 203590a..0000000
--- a/engine/src/opengl/debug.rs
+++ /dev/null
@@ -1,145 +0,0 @@
-use std::ffi::c_void;
-use std::io::{stderr, Write};
-use std::panic::catch_unwind;
-use std::ptr::null_mut;
-use std::sync::Mutex;
-
-use crate::opengl::util::gl_enum;
-
-pub type MessageCallback = fn(
- source: MessageSource,
- ty: MessageType,
- id: u32,
- severity: MessageSeverity,
- message: &str,
-);
-
-pub fn enable_debug_output()
-{
- unsafe {
- gl::Enable(gl::DEBUG_OUTPUT);
- gl::Enable(gl::DEBUG_OUTPUT_SYNCHRONOUS);
- }
-}
-
-pub fn set_debug_message_callback(cb: MessageCallback)
-{
- *DEBUG_MESSAGE_CB.lock().unwrap() = Some(cb);
-
- unsafe {
- gl::DebugMessageCallback(Some(debug_message_cb), null_mut());
- }
-}
-
-pub fn set_debug_message_control(
- source: Option<MessageSource>,
- ty: Option<MessageType>,
- severity: Option<MessageSeverity>,
- ids: &[u32],
- ids_action: MessageIdsAction,
-)
-{
- // Ids shouldn't realistically be large enough to cause a panic here
- let ids_len: i32 = ids.len().try_into().unwrap();
-
- unsafe {
- gl::DebugMessageControl(
- source.map_or(gl::DONT_CARE, |source| source as u32),
- ty.map_or(gl::DONT_CARE, |ty| ty as u32),
- severity.map_or(gl::DONT_CARE, |severity| severity as u32),
- ids_len,
- ids.as_ptr(),
- ids_action as u8,
- );
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-#[allow(dead_code)]
-pub enum MessageIdsAction
-{
- Enable = 1,
- Disable = 0,
-}
-
-gl_enum! {
-pub enum MessageSource
-{
- Api = gl::DEBUG_SOURCE_API,
- WindowSystem = gl::DEBUG_SOURCE_WINDOW_SYSTEM,
- ShaderCompiler = gl::DEBUG_SOURCE_SHADER_COMPILER,
- ThirdParty = gl::DEBUG_SOURCE_THIRD_PARTY,
- Application = gl::DEBUG_SOURCE_APPLICATION,
- Other = gl::DEBUG_SOURCE_OTHER,
-}
-}
-
-gl_enum! {
-pub enum MessageType
-{
- DeprecatedBehavior = gl::DEBUG_TYPE_DEPRECATED_BEHAVIOR,
- Error = gl::DEBUG_TYPE_ERROR,
- Marker = gl::DEBUG_TYPE_MARKER,
- Other = gl::DEBUG_TYPE_OTHER,
- Performance = gl::DEBUG_TYPE_PERFORMANCE,
- PopGroup = gl::DEBUG_TYPE_POP_GROUP,
- PushGroup = gl::DEBUG_TYPE_PUSH_GROUP,
- Portability = gl::DEBUG_TYPE_PORTABILITY,
- UndefinedBehavior = gl::DEBUG_TYPE_UNDEFINED_BEHAVIOR,
-}
-}
-
-gl_enum! {
-pub enum MessageSeverity
-{
- High = gl::DEBUG_SEVERITY_HIGH,
- Medium = gl::DEBUG_SEVERITY_MEDIUM,
- Low = gl::DEBUG_SEVERITY_LOW,
- Notification = gl::DEBUG_SEVERITY_NOTIFICATION,
-}
-}
-
-static DEBUG_MESSAGE_CB: Mutex<Option<MessageCallback>> = Mutex::new(None);
-
-extern "system" fn debug_message_cb(
- source: gl::types::GLenum,
- ty: gl::types::GLenum,
- id: gl::types::GLuint,
- severity: gl::types::GLenum,
- message_length: gl::types::GLsizei,
- message: *const gl::types::GLchar,
- _user_param: *mut c_void,
-)
-{
- // Unwinds are catched because unwinding from Rust code into foreign code is UB.
- let res = catch_unwind(|| {
- let cb_lock = DEBUG_MESSAGE_CB.lock().unwrap();
-
- if let Some(cb) = *cb_lock {
- let msg_source = MessageSource::from_gl(source).unwrap();
- let msg_type = MessageType::from_gl(ty).unwrap();
- let msg_severity = MessageSeverity::from_gl(severity).unwrap();
-
- let msg_length = usize::try_from(message_length).unwrap();
-
- // SAFETY: The received message should be a valid ASCII string
- let message = unsafe {
- std::str::from_utf8_unchecked(std::slice::from_raw_parts(
- message.cast(),
- msg_length,
- ))
- };
-
- cb(msg_source, msg_type, id, msg_severity, message);
- }
- });
-
- if res.is_err() {
- // eprintln is not used since it can panic and unwinds are unwanted because
- // unwinding from Rust code into foreign code is UB.
- stderr()
- .write_all(b"ERROR: Panic in debug message callback")
- .ok();
- println!();
- }
-}
diff --git a/engine/src/opengl/mod.rs b/engine/src/opengl/mod.rs
index 53e0120..2208ac6 100644
--- a/engine/src/opengl/mod.rs
+++ b/engine/src/opengl/mod.rs
@@ -1,128 +1 @@
-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;
-
-pub mod debug;
-
-pub fn set_viewport(position: Vec2<u32>, size: Dimens<u32>)
-{
- unsafe {
- #[allow(clippy::cast_possible_wrap)]
- gl::Viewport(
- position.x as i32,
- position.y as i32,
- size.width as i32,
- size.height as i32,
- );
- }
-}
-
-pub fn clear_buffers(mask: BufferClearMask)
-{
- unsafe {
- gl::Clear(mask.bits());
- }
-}
-
-pub fn set_polygon_mode(face: impl Into<PolygonModeFace>, mode: impl Into<PolygonMode>)
-{
- unsafe {
- gl::PolygonMode(face.into() as u32, mode.into() as u32);
- }
-}
-
-pub fn enable(capacity: Capability)
-{
- unsafe {
- gl::Enable(capacity as u32);
- }
-}
-
-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 {
- const COLOR = gl::COLOR_BUFFER_BIT;
- const DEPTH = gl::DEPTH_BUFFER_BIT;
- const STENCIL = gl::STENCIL_BUFFER_BIT;
- }
-}
-
-#[derive(Debug)]
-#[repr(u32)]
-pub enum Capability
-{
- DepthTest = gl::DEPTH_TEST,
- MultiSample = gl::MULTISAMPLE,
-}
-
-#[derive(Debug)]
-#[repr(u32)]
-pub enum PolygonMode
-{
- Point = gl::POINT,
- Line = gl::LINE,
- Fill = gl::FILL,
-}
-
-impl From<crate::draw_flags::PolygonMode> for PolygonMode
-{
- fn from(mode: crate::draw_flags::PolygonMode) -> Self
- {
- match mode {
- crate::draw_flags::PolygonMode::Point => Self::Point,
- crate::draw_flags::PolygonMode::Fill => Self::Fill,
- crate::draw_flags::PolygonMode::Line => Self::Line,
- }
- }
-}
-
-#[derive(Debug)]
-#[repr(u32)]
-pub enum PolygonModeFace
-{
- Front = gl::FRONT,
- Back = gl::BACK,
- FrontAndBack = gl::FRONT_AND_BACK,
-}
-
-impl From<crate::draw_flags::PolygonModeFace> for PolygonModeFace
-{
- fn from(face: crate::draw_flags::PolygonModeFace) -> Self
- {
- match face {
- crate::draw_flags::PolygonModeFace::Front => Self::Front,
- crate::draw_flags::PolygonModeFace::Back => Self::Back,
- crate::draw_flags::PolygonModeFace::FrontAndBack => Self::FrontAndBack,
- }
- }
-}
-
-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
deleted file mode 100644
index 36dc1a4..0000000
--- a/engine/src/opengl/shader.rs
+++ /dev/null
@@ -1,247 +0,0 @@
-use std::ffi::CStr;
-use std::ptr::null_mut;
-
-use crate::matrix::Matrix;
-use crate::vector::Vec3;
-
-#[derive(Debug)]
-pub struct Shader
-{
- shader: gl::types::GLuint,
-}
-
-impl Shader
-{
- pub fn new(kind: Kind) -> Self
- {
- let shader = unsafe { gl::CreateShader(kind.into_gl()) };
-
- Self { shader }
- }
-
- pub fn set_source(&mut self, source: &str) -> Result<(), Error>
- {
- if !source.is_ascii() {
- return Err(Error::SourceNotAscii);
- }
-
- unsafe {
- #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
- gl::ShaderSource(
- self.shader,
- 1,
- &source.as_ptr().cast(),
- &(source.len() as gl::types::GLint),
- );
- }
-
- Ok(())
- }
-
- pub fn compile(&mut self) -> Result<(), Error>
- {
- unsafe {
- gl::CompileShader(self.shader);
- }
-
- let mut compile_success = gl::types::GLint::default();
-
- unsafe {
- gl::GetShaderiv(self.shader, gl::COMPILE_STATUS, &mut compile_success);
- }
-
- if compile_success == 0 {
- let info_log = self.get_info_log();
-
- return Err(Error::CompileFailed(info_log));
- }
-
- Ok(())
- }
-
- fn get_info_log(&self) -> String
- {
- let mut buf = vec![gl::types::GLchar::default(); 512];
-
- unsafe {
- #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
- gl::GetShaderInfoLog(
- self.shader,
- 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()) }
- }
-}
-
-impl Drop for Shader
-{
- fn drop(&mut self)
- {
- unsafe {
- gl::DeleteShader(self.shader);
- }
- }
-}
-
-/// Shader kind.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub enum Kind
-{
- Vertex,
- Fragment,
-}
-
-impl Kind
-{
- fn into_gl(self) -> gl::types::GLenum
- {
- match self {
- Self::Vertex => gl::VERTEX_SHADER,
- Self::Fragment => gl::FRAGMENT_SHADER,
- }
- }
-}
-
-/// Shader program
-#[derive(Debug, PartialEq, Eq, Hash)]
-pub struct Program
-{
- program: gl::types::GLuint,
-}
-
-impl Program
-{
- pub fn new() -> Self
- {
- let program = unsafe { gl::CreateProgram() };
-
- Self { program }
- }
-
- pub fn attach(&mut self, shader: &Shader)
- {
- unsafe {
- gl::AttachShader(self.program, shader.shader);
- }
- }
-
- pub fn link(&mut self) -> Result<(), Error>
- {
- unsafe {
- gl::LinkProgram(self.program);
- }
-
- let mut link_success = gl::types::GLint::default();
-
- unsafe {
- gl::GetProgramiv(self.program, gl::LINK_STATUS, &mut link_success);
- }
-
- if link_success == 0 {
- let info_log = self.get_info_log();
-
- return Err(Error::CompileFailed(info_log));
- }
-
- Ok(())
- }
-
- pub fn activate(&self)
- {
- unsafe {
- gl::UseProgram(self.program);
- }
- }
-
- pub fn set_uniform_matrix_4fv(&mut self, name: &CStr, matrix: &Matrix<f32, 4, 4>)
- {
- let uniform_location =
- unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) };
-
- unsafe {
- gl::ProgramUniformMatrix4fv(
- self.program,
- uniform_location,
- 1,
- gl::FALSE,
- matrix.as_ptr(),
- );
- }
- }
-
- pub fn set_uniform_vec_3fv(&mut self, name: &CStr, vec: &Vec3<f32>)
- {
- let uniform_location =
- unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) };
-
- unsafe {
- gl::ProgramUniform3fv(self.program, uniform_location, 1, vec.as_ptr());
- }
- }
-
- pub fn set_uniform_1fv(&mut self, name: &CStr, num: f32)
- {
- let uniform_location =
- unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) };
-
- unsafe {
- gl::ProgramUniform1fv(self.program, uniform_location, 1, &num);
- }
- }
-
- pub fn set_uniform_1i(&mut self, name: &CStr, num: i32)
- {
- let uniform_location =
- unsafe { gl::GetUniformLocation(self.program, name.as_ptr().cast()) };
-
- unsafe {
- gl::ProgramUniform1i(self.program, uniform_location, num);
- }
- }
-
- fn get_info_log(&self) -> String
- {
- 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(),
- );
- }
-
- 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
-{
- fn drop(&mut self)
- {
- unsafe {
- gl::DeleteProgram(self.program);
- }
- }
-}
-
-/// Shader error.
-#[derive(Debug, thiserror::Error)]
-pub enum Error
-{
- #[error("All characters in source are not within the ASCII range")]
- SourceNotAscii,
-
- #[error("Failed to compile: {0}")]
- CompileFailed(String),
-}
diff --git a/engine/src/opengl/texture.rs b/engine/src/opengl/texture.rs
deleted file mode 100644
index 52c8554..0000000
--- a/engine/src/opengl/texture.rs
+++ /dev/null
@@ -1,240 +0,0 @@
-use crate::data_types::dimens::Dimens;
-use crate::texture::Properties;
-
-#[derive(Debug)]
-pub struct Texture
-{
- texture: gl::types::GLuint,
-}
-
-impl Texture
-{
- pub fn new() -> Self
- {
- let mut texture = gl::types::GLuint::default();
-
- unsafe {
- gl::CreateTextures(gl::TEXTURE_2D, 1, &mut texture);
- };
-
- Self { texture }
- }
-
- pub fn bind(&self)
- {
- unsafe {
- gl::BindTexture(gl::TEXTURE_2D, self.texture);
- }
- }
-
- pub fn generate(
- &mut self,
- dimens: Dimens<u32>,
- data: &[u8],
- pixel_data_format: PixelDataFormat,
- )
- {
- self.alloc_image(pixel_data_format, dimens, data);
-
- unsafe {
- gl::GenerateTextureMipmap(self.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();
-
- #[allow(clippy::cast_possible_wrap)]
- unsafe {
- gl::TextureParameteri(self.texture, gl::TEXTURE_WRAP_S, wrapping_gl as i32);
- gl::TextureParameteri(self.texture, gl::TEXTURE_WRAP_T, wrapping_gl as i32);
- }
- }
-
- pub fn set_magnifying_filter(&mut self, filtering: Filtering)
- {
- let filtering_gl = filtering.to_gl();
-
- #[allow(clippy::cast_possible_wrap)]
- unsafe {
- gl::TextureParameteri(
- self.texture,
- gl::TEXTURE_MAG_FILTER,
- filtering_gl as i32,
- );
- }
- }
-
- pub fn set_minifying_filter(&mut self, filtering: Filtering)
- {
- let filtering_gl = filtering.to_gl();
-
- #[allow(clippy::cast_possible_wrap)]
- unsafe {
- gl::TextureParameteri(
- self.texture,
- gl::TEXTURE_MIN_FILTER,
- filtering_gl as i32,
- );
- }
- }
-
- fn alloc_image(
- &mut self,
- pixel_data_format: PixelDataFormat,
- dimens: Dimens<u32>,
- data: &[u8],
- )
- {
- unsafe {
- #[allow(clippy::cast_possible_wrap)]
- gl::TextureStorage2D(
- self.texture,
- 1,
- pixel_data_format.to_sized_internal_format(),
- dimens.width as i32,
- dimens.height as i32,
- );
-
- #[allow(clippy::cast_possible_wrap)]
- gl::TextureSubImage2D(
- self.texture,
- 0,
- 0,
- 0,
- dimens.width as i32,
- dimens.height as i32,
- pixel_data_format.to_format(),
- gl::UNSIGNED_BYTE,
- data.as_ptr().cast(),
- );
- }
- }
-}
-
-impl Drop for Texture
-{
- fn drop(&mut self)
- {
- unsafe {
- gl::DeleteTextures(1, &self.texture);
- }
- }
-}
-
-/// Texture wrapping.
-#[derive(Debug, Clone, Copy)]
-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,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub enum Filtering
-{
- Nearest,
- Linear,
-}
-
-impl Filtering
-{
- fn to_gl(self) -> gl::types::GLenum
- {
- match self {
- Self::Linear => gl::LINEAR,
- Self::Nearest => gl::NEAREST,
- }
- }
-}
-
-/// Texture pixel data format.
-#[derive(Debug, Clone, Copy)]
-pub enum PixelDataFormat
-{
- Rgb8,
- Rgba8,
-}
-
-impl PixelDataFormat
-{
- fn to_sized_internal_format(self) -> gl::types::GLenum
- {
- match self {
- Self::Rgb8 => gl::RGB8,
- Self::Rgba8 => gl::RGBA8,
- }
- }
-
- fn to_format(self) -> gl::types::GLenum
- {
- match self {
- Self::Rgb8 => gl::RGB,
- Self::Rgba8 => gl::RGBA,
- }
- }
-}
-
-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_num(num: usize) -> Option<Self> {
- match num {
- #(
- N => Some(Self::No~N),
- )*
- _ => None
- }
- }
- }
- });
- };
-}
-
-texture_unit_enum!(cnt = 31);
diff --git a/engine/src/opengl/util.rs b/engine/src/opengl/util.rs
deleted file mode 100644
index e60778f..0000000
--- a/engine/src/opengl/util.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-// May only be used when certain crate features are enabled
-#![allow(unused_macros, unused_imports)]
-
-macro_rules! gl_enum {
- (
- $visibility: vis enum $name: ident
- {$(
- $variant: ident = gl::$gl_enum: ident,
- )+}
- ) => {
- #[derive(Debug, Clone, Copy)]
- #[repr(u32)]
- $visibility enum $name
- {$(
- $variant = gl::$gl_enum,
- )+}
-
- impl $name {
- fn from_gl(num: gl::types::GLenum) -> Option<Self>
- {
- match num {
- $(gl::$gl_enum => Some(Self::$variant),)+
- _ => None
- }
- }
- }
- };
-}
-
-pub(crate) use gl_enum;
diff --git a/engine/src/opengl/vertex_array.rs b/engine/src/opengl/vertex_array.rs
deleted file mode 100644
index da5d91e..0000000
--- a/engine/src/opengl/vertex_array.rs
+++ /dev/null
@@ -1,183 +0,0 @@
-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
-{
- array: gl::types::GLuint,
-}
-
-impl VertexArray
-{
- pub fn new() -> Self
- {
- let mut array = 0;
-
- unsafe {
- gl::CreateVertexArrays(1, &mut array);
- }
-
- Self { array }
- }
-
- /// Draws the currently bound vertex array.
- pub fn draw_arrays(primitive_kind: PrimitiveKind, start_index: u32, cnt: u32)
- {
- unsafe {
- #[allow(clippy::cast_possible_wrap)]
- gl::DrawArrays(
- primitive_kind.into_gl(),
- start_index as gl::types::GLint,
- cnt as gl::types::GLsizei,
- );
- }
- }
-
- /// Draws the currently bound vertex array.
- pub fn draw_elements(primitive_kind: PrimitiveKind, offset: u32, cnt: u32)
- {
- unsafe {
- #[allow(clippy::cast_possible_wrap)]
- gl::DrawElements(
- primitive_kind.into_gl(),
- cnt as gl::types::GLsizei,
- gl::UNSIGNED_INT,
- (offset as gl::types::GLint) as *const _,
- );
- }
- }
-
- pub fn bind_element_buffer(&mut self, element_buffer: &Buffer<u32>)
- {
- unsafe {
- gl::VertexArrayElementBuffer(self.array, element_buffer.object());
- }
- }
-
- pub fn bind_vertex_buffer(
- &mut self,
- binding_index: u32,
- vertex_buffer: &Buffer<Vertex>,
- offset: isize,
- )
- {
- unsafe {
- gl::VertexArrayVertexBuffer(
- self.array,
- binding_index,
- vertex_buffer.object(),
- offset,
- VERTEX_STRIDE,
- );
- }
- }
-
- pub fn enable_attrib(&mut self, attrib_index: u32)
- {
- unsafe {
- gl::EnableVertexArrayAttrib(self.array, attrib_index as gl::types::GLuint);
- }
- }
-
- pub fn set_attrib_format(
- &mut self,
- attrib_index: u32,
- data_type: DataType,
- normalized: bool,
- offset: u32,
- )
- {
- unsafe {
- #[allow(clippy::cast_possible_wrap)]
- gl::VertexArrayAttribFormat(
- self.array,
- attrib_index,
- data_type.size() as gl::types::GLint,
- data_type as u32,
- if normalized { gl::TRUE } else { gl::FALSE },
- offset,
- );
- }
- }
-
- /// Associate a vertex attribute and a vertex buffer binding.
- pub fn set_attrib_vertex_buf_binding(
- &mut self,
- attrib_index: u32,
- vertex_buf_binding_index: u32,
- )
- {
- unsafe {
- gl::VertexArrayAttribBinding(
- self.array,
- attrib_index,
- vertex_buf_binding_index,
- );
- }
- }
-
- pub fn bind(&self)
- {
- 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)]
-pub enum PrimitiveKind
-{
- Triangles,
-}
-
-impl PrimitiveKind
-{
- fn into_gl(self) -> gl::types::GLenum
- {
- match self {
- Self::Triangles => gl::TRIANGLES,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-#[repr(u32)]
-pub enum DataType
-{
- Float = gl::FLOAT,
-}
-
-impl DataType
-{
- pub fn size(self) -> u32
- {
- #[allow(clippy::cast_possible_truncation)]
- match self {
- Self::Float => size_of::<gl::types::GLfloat>() as u32,
- }
- }
-}
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..6d25f6b 100644
--- a/engine/src/renderer.rs
+++ b/engine/src/renderer.rs
@@ -1 +1,80 @@
+use ecs::pair::{ChildOf, Pair};
+use ecs::phase::{Phase, POST_UPDATE as POST_UPDATE_PHASE};
+use ecs::{declare_entity, Component};
+
+use crate::builder;
+
pub mod opengl;
+
+declare_entity!(
+ pub RENDER_PHASE,
+ (
+ Phase,
+ Pair::builder()
+ .relation::<ChildOf>()
+ .target_id(*POST_UPDATE_PHASE)
+ .build()
+ )
+);
+
+builder! {
+/// Window graphics properties.
+#[builder(name=GraphicsPropertiesBuilder, derives=(Debug, Clone))]
+#[derive(Debug, Clone, Component)]
+#[non_exhaustive]
+pub struct GraphicsProperties
+{
+ /// Number of samples for multisampling. `None` means no multisampling.
+ #[builder(skip_generate_fn)]
+ pub multisampling_sample_cnt: Option<u8>,
+
+ /// Whether graphics API debugging is enabled.
+ pub debug: bool,
+
+ /// Whether depth testing is enabled
+ pub depth_test: bool,
+}
+}
+
+impl GraphicsProperties
+{
+ pub fn builder() -> GraphicsPropertiesBuilder
+ {
+ GraphicsPropertiesBuilder::default()
+ }
+}
+
+impl Default for GraphicsProperties
+{
+ fn default() -> Self
+ {
+ Self::builder().build()
+ }
+}
+
+impl GraphicsPropertiesBuilder
+{
+ pub fn multisampling_sample_cnt(mut self, multisampling_sample_cnt: u8) -> Self
+ {
+ self.multisampling_sample_cnt = Some(multisampling_sample_cnt);
+ self
+ }
+
+ pub fn no_multisampling(mut self) -> Self
+ {
+ self.multisampling_sample_cnt = None;
+ self
+ }
+}
+
+impl Default for GraphicsPropertiesBuilder
+{
+ fn default() -> Self
+ {
+ Self {
+ multisampling_sample_cnt: Some(8),
+ debug: false,
+ depth_test: true,
+ }
+ }
+}
diff --git a/engine/src/renderer/opengl.rs b/engine/src/renderer/opengl.rs
index 7220ddc..fb7dfbe 100644
--- a/engine/src/renderer/opengl.rs
+++ b/engine/src/renderer/opengl.rs
@@ -1,84 +1,173 @@
//! OpenGL renderer.
+use std::any::type_name;
use std::collections::HashMap;
-use std::ffi::{c_void, CString};
+use std::ffi::CString;
use std::io::{Error as IoError, ErrorKind as IoErrorKind};
-use std::ops::Deref;
use std::path::Path;
-use std::process::abort;
use ecs::actions::Actions;
-use ecs::component::local::Local;
-use ecs::phase::{PRESENT as PRESENT_PHASE, START as START_PHASE};
-use ecs::query::options::{Not, With};
+use ecs::component::Handle as ComponentHandle;
+use ecs::entity::obtainer::Obtainer as EntityObtainer;
+use ecs::event::component::{Changed, Removed};
+use ecs::pair::{ChildOf, Pair, Wildcard};
+use ecs::phase::Phase;
+use ecs::query::term::Without;
use ecs::sole::Single;
-use ecs::system::{Into as _, System};
-use ecs::{Component, Query};
-
-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::lighting::{DirectionalLight, GlobalLight, PointLight};
-use crate::material::{Flags as MaterialFlags, Material};
-use crate::matrix::Matrix;
-use crate::mesh::Mesh;
-use crate::opengl::buffer::{Buffer, Usage as BufferUsage};
-use crate::opengl::debug::{
- enable_debug_output,
+use ecs::system::observer::Observe;
+use ecs::{declare_entity, Component, Query};
+use glutin::display::GetGlDisplay;
+use glutin::prelude::GlDisplay;
+use glutin::surface::GlSurface;
+use opengl_bindings::debug::{
set_debug_message_callback,
set_debug_message_control,
MessageIdsAction,
MessageSeverity,
MessageSource,
MessageType,
+ SetDebugMessageControlError as GlSetDebugMessageControlError,
};
-use crate::opengl::glsl::{
- preprocess as glsl_preprocess,
- PreprocessingError as GlslPreprocessingError,
+use opengl_bindings::misc::{
+ clear_buffers,
+ enable,
+ set_enabled,
+ BufferClearMask,
+ Capability,
+ SetViewportError as GlSetViewportError,
};
-use crate::opengl::shader::{
+use opengl_bindings::shader::{
Error as GlShaderError,
Kind as ShaderKind,
Program as GlShaderProgram,
Shader as GlShader,
};
-use crate::opengl::texture::{
- set_active_texture_unit,
+use opengl_bindings::texture::{
+ Filtering as GlTextureFiltering,
+ GenerateError as GlTextureGenerateError,
+ PixelDataFormat as GlTexturePixelDataFormat,
Texture as GlTexture,
- TextureUnit,
+ Wrapping as GlTextureWrapping,
};
-use crate::opengl::vertex_array::{
- DataType as VertexArrayDataType,
+use opengl_bindings::vertex_array::{
+ DrawError as GlDrawError,
PrimitiveKind,
VertexArray,
};
-use crate::opengl::{
- clear_buffers,
- enable,
- get_context_flags as get_opengl_context_flags,
- BufferClearMask,
- Capability,
- ContextFlags,
+use opengl_bindings::{ContextWithFns, CurrentContextWithFns};
+use safer_ffi::layout::ReprC;
+
+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::image::{ColorType as ImageColorType, Image};
+use crate::lighting::{DirectionalLight, GlobalLight, PointLight};
+use crate::material::{Flags as MaterialFlags, Material};
+use crate::matrix::Matrix;
+use crate::model::Model;
+use crate::opengl::glsl::{
+ preprocess as glsl_preprocess,
+ PreprocessingError as GlslPreprocessingError,
+};
+use crate::projection::{ClipVolume, Projection};
+use crate::renderer::opengl::glutin_compat::{
+ DisplayBuilder,
+ Error as GlutinCompatError,
};
-use crate::projection::{new_perspective_matrix, Projection};
-use crate::texture::{Id as TextureId, Texture};
-use crate::transform::{Position, Scale};
-use crate::util::NeverDrop;
+use crate::renderer::opengl::graphics_mesh::GraphicsMesh;
+use crate::renderer::{GraphicsProperties, RENDER_PHASE};
+use crate::texture::{
+ Filtering as TextureFiltering,
+ Properties as TextureProperties,
+ Wrapping as TextureWrapping,
+};
+use crate::transform::{Scale, WorldPosition};
+use crate::util::MapVec;
use crate::vector::{Vec2, Vec3};
-use crate::vertex::{AttributeComponentType, Vertex};
-use crate::window::Window;
+use crate::windowing::window::{
+ Closed as WindowClosed,
+ CreationAttributes as WindowCreationAttributes,
+ CreationReady,
+ Window,
+};
+use crate::windowing::Context as WindowingContext;
+
+mod glutin_compat;
+mod graphics_mesh;
+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 Mesh,
- &'a Material,
- &'a Option<MaterialFlags>,
- &'a Option<Position>,
- &'a Option<Scale>,
- &'a Option<DrawFlags>,
- &'a Option<GlObjects>,
+ &'a Model,
+ Option<&'a MaterialFlags>,
+ Option<&'a WorldPosition>,
+ Option<&'a Scale>,
+ Option<&'a DrawFlags>,
+ &'a [Pair<DataInGraphicsContext, Wildcard>],
+);
+
+declare_entity!(
+ pub POST_RENDER_PHASE,
+ (Phase, Pair::builder().relation::<ChildOf>().target_id(*RENDER_PHASE).build())
);
+#[derive(Debug, Component)]
+struct WithGraphicsContext;
+
+#[derive(Debug, Component)]
+struct WindowGlConfig
+{
+ gl_config: glutin::config::Config,
+}
+
+#[derive(Debug, Component)]
+struct WindowGraphicsSurface
+{
+ surface: glutin::surface::Surface<glutin::surface::WindowSurface>,
+}
+
+#[derive(Component)]
+struct GraphicsContext
+{
+ context: ContextWithFns,
+ shader_program: Option<GlShaderProgram>,
+ textures_objs: HashMap<AssetId, GlTexture>,
+ default_1x1_texture_obj: Option<GlTexture>,
+ graphics_mesh_store: GraphicsMeshStore,
+}
+
+#[derive(Debug, Default)]
+struct GraphicsMeshStore
+{
+ graphics_meshes: MapVec<GraphicsMeshId, GraphicsMesh>,
+ next_id: GraphicsMeshId,
+}
+
+impl GraphicsMeshStore
+{
+ fn insert(&mut self, graphics_mesh: GraphicsMesh) -> GraphicsMeshId
+ {
+ let id = self.next_id;
+
+ self.graphics_meshes.insert(id, graphics_mesh);
+
+ self.next_id.inner += 1;
+
+ id
+ }
+}
+
+#[derive(Debug, Component)]
+struct DataInGraphicsContext
+{
+ graphics_mesh_id: GraphicsMeshId,
+}
+
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct Extension {}
@@ -87,214 +176,904 @@ impl ecs::extension::Extension for Extension
{
fn collect(self, mut collector: ecs::extension::Collector<'_>)
{
- collector.add_system(*START_PHASE, initialize);
+ collector.add_declared_entity(&RENDER_PHASE);
+ collector.add_declared_entity(&POST_RENDER_PHASE);
- collector.add_system(
- *PRESENT_PHASE,
- render
- .into_system()
- .initialize((GlobalGlObjects::default(),)),
- );
+ collector.add_system(*RENDER_PHASE, render);
+
+ collector.add_system(*POST_RENDER_PHASE, prepare_windows);
+ collector.add_system(*POST_RENDER_PHASE, init_window_graphics);
+
+ collector.add_observer(handle_model_removed);
+
+ collector.add_observer(handle_window_changed);
+ collector.add_observer(handle_window_removed);
}
}
-fn initialize(window: Single<Window>)
+#[tracing::instrument(skip_all)]
+fn handle_model_removed(observe: Observe<Pair<Removed, Model>>, mut actions: Actions)
{
- window
- .make_context_current()
- .expect("Failed to make window context current");
+ for evt_match in &observe {
+ let ent_id = evt_match.id();
+
+ tracing::debug!(entity_id=%ent_id, "Cleaning up after model");
+
+ let ent = evt_match.get_ent_infallible();
+
+ for data_in_graphics_ctx_pair in
+ ent.get_wildcard_pair_matches::<DataInGraphicsContext, Wildcard>()
+ {
+ actions.remove_components(ent_id, [data_in_graphics_ctx_pair.id()]);
+
+ let Some(graphics_context_ent) = data_in_graphics_ctx_pair.get_target_ent()
+ else {
+ tracing::trace!(
+ concat!(
+ "Graphics context referenced by pair ({}, {}) does not exist. ",
+ "Skipping cleanup of this model"
+ ),
+ type_name::<DataInGraphicsContext>(),
+ data_in_graphics_ctx_pair.id().target_entity()
+ );
+
+ continue;
+ };
- gl::load_with(|symbol| match window.get_proc_address(symbol) {
- Ok(addr) => addr as *const c_void,
- Err(err) => {
- println!(
- "FATAL ERROR: Failed to get adress of OpenGL function {symbol}: {err}",
- );
+ let Some(data_in_graphics_ctx) =
+ data_in_graphics_ctx_pair.get_data_as_relation()
+ else {
+ unreachable!();
+ };
+
+ let Some(mut graphics_context) =
+ graphics_context_ent.get_mut::<GraphicsContext>()
+ else {
+ tracing::trace!(
+ "Graphics context entity {} does not have a {} component",
+ graphics_context_ent.uid(),
+ type_name::<GraphicsContext>()
+ );
+ continue;
+ };
- abort();
+ graphics_context
+ .graphics_mesh_store
+ .graphics_meshes
+ .remove(data_in_graphics_ctx.graphics_mesh_id);
}
- });
+ }
+}
+
+#[tracing::instrument(skip_all)]
+fn handle_window_changed(
+ observe: Observe<Pair<Changed, Window>>,
+ entity_obtainer: EntityObtainer,
+)
+{
+ for evt_match in &observe {
+ let window_ent = evt_match.get_ent_infallible();
+
+ tracing::trace!(
+ new_state = ?evt_match.get_changed_comp(),
+ "Handling window change"
+ );
+
+ let Some(window_graphics_surface) = window_ent.get::<WindowGraphicsSurface>()
+ else {
+ continue;
+ };
+
+ let Some(graphics_context_ent_id) = window_ent
+ .get_matching_components(
+ Pair::builder()
+ .relation::<WithGraphicsContext>()
+ .target_id(Wildcard::uid())
+ .build()
+ .id(),
+ )
+ .next()
+ .map(|comp_ref| comp_ref.id().target_entity())
+ else {
+ continue;
+ };
+
+ let Some(graphics_context_ent) =
+ entity_obtainer.get_entity(graphics_context_ent_id)
+ else {
+ tracing::error!("Graphics context entity does not exist");
+ continue;
+ };
- if get_opengl_context_flags().contains(ContextFlags::DEBUG) {
- initialize_debug();
+ let Some(graphics_context) = graphics_context_ent.get::<GraphicsContext>() else {
+ tracing::error!(
+ "Graphics context entity does not have a GraphicsContext component"
+ );
+ continue;
+ };
+
+ let Ok(current_graphics_context) = graphics_context
+ .context
+ .make_current(&window_graphics_surface.surface)
+ else {
+ tracing::error!("Failed to make graphics context current");
+ continue;
+ };
+
+ if let Err(err) = set_viewport(
+ &current_graphics_context,
+ Vec2::default(),
+ evt_match.get_changed_comp().inner_size(),
+ ) {
+ tracing::error!("Failed to set viewport: {err}");
+ }
}
+}
- let window_size = window.size().expect("Failed to get window size");
+#[tracing::instrument(skip_all)]
+fn handle_window_removed(observe: Observe<Pair<Removed, Window>>, mut actions: Actions)
+{
+ for evt_match in &observe {
+ let window_ent_id = evt_match.id();
- set_viewport(Vec2 { x: 0, y: 0 }, window_size);
+ let window_ent = evt_match.get_ent_infallible();
- window.set_framebuffer_size_callback(|new_window_size| {
- set_viewport(Vec2::ZERO, new_window_size);
- });
+ tracing::debug!(
+ entity_id = %window_ent_id,
+ title = %evt_match.get_removed_comp().title,
+ "Handling removal of window"
+ );
- enable(Capability::DepthTest);
- enable(Capability::MultiSample);
+ actions.remove_comps::<(WindowGraphicsSurface, WindowGlConfig)>(window_ent_id);
+
+ let Some(with_graphics_ctx_pair_handle) =
+ window_ent.get_first_wildcard_pair_match::<WithGraphicsContext, Wildcard>()
+ else {
+ tracing::warn!("Window entity is missing a (WithGraphicsContext, *) pair");
+ continue;
+ };
+
+ let graphics_context_ent_id = with_graphics_ctx_pair_handle.id().target_entity();
+
+ actions.remove_comps::<(GraphicsContext,)>(graphics_context_ent_id);
+
+ actions.remove_components(window_ent_id, [with_graphics_ctx_pair_handle.id()]);
+ }
}
-#[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)>,
- window: Single<Window>,
- global_light: Single<GlobalLight>,
- mut gl_objects: Local<GlobalGlObjects>,
+#[derive(Debug, Component)]
+struct SetupFailed;
+
+// fn on_window_creation_attrs_added(
+// observe: Observe<Pair<Added, WindowCreationAttributes>>,
+// windowing: Single<Windowing>,
+// window_store: Single<WindowStore>,
+// mut actions: Actions,
+// )
+// {
+// for evt_match in &observe {
+// let Some(ent) = evt_match.get_entity() else {
+// unreachable!();
+// };
+//
+// if ent.has_component(WindowGlConfig::id()) ||
+// ent.has_component(WindowClosed::id()) || ent.has_component() {} }
+// }
+
+fn prepare_windows(
+ window_query: Query<
+ (
+ Option<&Window>,
+ &mut WindowCreationAttributes,
+ Option<&GraphicsProperties>,
+ ),
+ (
+ Without<CreationReady>,
+ Without<WindowGlConfig>,
+ Without<WindowClosed>,
+ Without<SetupFailed>,
+ ),
+ >,
+ windowing_context: Single<WindowingContext>,
mut actions: Actions,
)
{
- let Some((camera, camera_pos, _)) = camera_query.iter().next() else {
- tracing::warn!("No current camera. Nothing will be rendered");
+ let Some(display_handle) = windowing_context.display_handle() else {
return;
};
- let point_lights = point_light_query
- .iter()
- .map(|(point_light,)| point_light)
- .collect::<Vec<_>>();
+ for (window_ent_id, (window, mut window_creation_attrs, graphics_props)) in
+ window_query.iter_with_euids()
+ {
+ tracing::debug!("Preparing window entity {window_ent_id} for use in rendering");
- let directional_lights = directional_lights.iter().collect::<Vec<_>>();
+ let mut glutin_config_template_builder =
+ glutin::config::ConfigTemplateBuilder::new();
- let GlobalGlObjects {
- shader_program,
- textures: gl_textures,
- } = &mut *gl_objects;
+ let graphics_props = match graphics_props.as_ref() {
+ Some(graphics_props) => &*graphics_props,
+ None => {
+ actions.add_components(window_ent_id, (GraphicsProperties::default(),));
- let shader_program =
- shader_program.get_or_insert_with(|| create_default_shader_program().unwrap());
+ &GraphicsProperties::default()
+ }
+ };
- clear_buffers(BufferClearMask::COLOR | BufferClearMask::DEPTH);
+ if let Some(multisampling_sample_cnt) = graphics_props.multisampling_sample_cnt {
+ glutin_config_template_builder = glutin_config_template_builder
+ .with_multisampling(multisampling_sample_cnt);
+ }
- for (
- entity_index,
- (mesh, material, material_flags, position, scale, draw_flags, gl_objects),
- ) in query.iter().enumerate()
- {
- let material_flags = material_flags
- .map(|material_flags| material_flags.clone())
- .unwrap_or_default();
+ let window_handle = match window
+ .as_ref()
+ .map(|window| unsafe {
+ windowing_context.get_window_as_handle(&window.wid())
+ })
+ .flatten()
+ .transpose()
+ {
+ Ok(window_handle) => window_handle,
+ Err(err) => {
+ tracing::error!("Failed to get window handle: {err}");
+ actions.add_components(window_ent_id, (SetupFailed,));
+ continue;
+ }
+ };
- let new_gl_objects;
+ let (new_window_creation_attrs, gl_config) = match DisplayBuilder::new()
+ .with_window_attributes(window_creation_attrs.clone())
+ .build(
+ window_handle,
+ &display_handle,
+ glutin_config_template_builder,
+ |mut cfgs| cfgs.next(),
+ ) {
+ Ok((new_window_creation_attrs, gl_config)) => {
+ (new_window_creation_attrs, gl_config)
+ }
+ Err(GlutinCompatError::WindowRequired) => {
+ actions.add_components(window_ent_id, (CreationReady,));
+ continue;
+ }
+ Err(err) => {
+ tracing::error!("Failed to create platform graphics display: {err}");
+ actions.add_components(window_ent_id, (SetupFailed,));
+ continue;
+ }
+ };
- 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);
+ *window_creation_attrs = new_window_creation_attrs;
+
+ // let gl_config_template = glutin_config_template_builder.build();
+ //
+ // let display = match glutin_winit_compat::create_display(
+ // unsafe { engine_display.as_display_handle() },
+ // glutin_winit_compat::ApiPreference::default(),
+ // None,
+ // ) {
+ // Ok(gl_display) => gl_display,
+ // Err(err) => {
+ // tracing::error!("Failed to create graphics platform display: {err}");
+ // continue;
+ // }
+ // };
+ //
+ // let mut gl_configs = match unsafe { display.find_configs(gl_config_template) }
+ // { Ok(gl_configs) => gl_configs,
+ // Err(err) => {
+ // tracing::error!("Failed to find GL configs: {err:?}");
+ // continue;
+ // }
+ // };
+ //
+ // let Some(first_gl_config) = gl_configs.next() else {
+ // tracing::error!("No matching GL configuration exists");
+ // continue;
+ // };
+ //
+ // *window_creation_attrs = finalize_window_creation_attrs(
+ // window_creation_attrs.clone(),
+ // &first_gl_config,
+ // );
+
+ actions.add_components(window_ent_id, (WindowGlConfig { gl_config },));
+
+ if window.is_none() {
+ actions.add_components(window_ent_id, (CreationReady,));
+ }
+ }
+}
- new_gl_objects = Some(gl_objects.clone());
+#[tracing::instrument(skip_all)]
+fn init_window_graphics(
+ window_query: Query<
+ (&Window, &WindowGlConfig, &GraphicsProperties),
+ (Without<WindowGraphicsSurface>, Without<SetupFailed>),
+ >,
+ mut actions: Actions,
+ windowing_context: Single<WindowingContext>,
+)
+{
+ for (window_ent_id, (window, window_gl_config, graphics_props)) in
+ window_query.iter_with_euids()
+ {
+ tracing::info!("Initializing graphics for window {window_ent_id}");
+
+ let display = window_gl_config.gl_config.display();
+
+ let window_handle =
+ match unsafe { windowing_context.get_window_as_handle(&window.wid()) }
+ .transpose()
+ {
+ Ok(Some(window_handle)) => window_handle,
+ Ok(None) => {
+ tracing::error!(
+ wid = ?window.wid(),
+ entity_id = %window_ent_id,
+ "Windowing context does not contain window"
+ );
+ actions.add_components(window_ent_id, (SetupFailed,));
+ continue;
+ }
+ Err(err) => {
+ tracing::error!("Failed to get window handle: {err}");
+ actions.add_components(window_ent_id, (SetupFailed,));
+ continue;
+ }
+ };
- actions.add_components(
- query.get_entity_uid(entity_index).unwrap(),
- (gl_objects,),
+ let Some(window_inner_size) = window.inner_size().try_into_nonzero() else {
+ tracing::error!(
+ "Cannot create a surface for a window with a width/height of 0",
);
+ continue;
+ };
- &*new_gl_objects.unwrap()
+ let surface = match unsafe {
+ display.create_window_surface(
+ &window_gl_config.gl_config,
+ &glutin::surface::SurfaceAttributesBuilder::<
+ glutin::surface::WindowSurface,
+ >::new()
+ .build(
+ window_handle.as_raw(),
+ window_inner_size.width,
+ window_inner_size.height,
+ ),
+ )
+ } {
+ Ok(surface) => surface,
+ Err(err) => {
+ tracing::error!("Failed to create window surface: {err}");
+ continue;
+ }
};
- apply_transformation_matrices(
- Transformation {
- position: position.map(|pos| *pos).unwrap_or_default().position,
- scale: scale.map(|scale| *scale).unwrap_or_default().scale,
- },
- shader_program,
- &camera,
- &camera_pos,
- window.size().expect("Failed to get window size"),
- );
+ let context = match unsafe {
+ display.create_context(
+ &window_gl_config.gl_config,
+ &glutin::context::ContextAttributesBuilder::new()
+ .with_debug(graphics_props.debug)
+ .build(Some(window_handle.as_raw())),
+ )
+ } {
+ Ok(context) => context,
+ Err(err) => {
+ tracing::error!("Failed to create graphics context: {err}");
+ continue;
+ }
+ };
- apply_light(
- &material,
- &material_flags,
- &global_light,
- shader_program,
- point_lights.as_slice(),
- directional_lights
- .iter()
- .map(|(dir_light,)| &**dir_light)
- .collect::<Vec<_>>()
- .as_slice(),
- &camera_pos,
+ let context = match ContextWithFns::new(context, &surface) {
+ Ok(context) => context,
+ Err(err) => {
+ tracing::error!("Failed to create graphics context: {err}");
+ continue;
+ }
+ };
+
+ let Ok(current_graphics_context) = context.make_current(&surface) else {
+ tracing::error!("Failed to make graphics context current");
+ continue;
+ };
+
+ if let Err(err) = set_viewport(
+ &current_graphics_context,
+ Vec2 { x: 0, y: 0 },
+ window.inner_size(),
+ ) {
+ tracing::error!("Failed to set viewport: {err}");
+ }
+
+ set_enabled(
+ &current_graphics_context,
+ Capability::DepthTest,
+ graphics_props.depth_test,
);
- for (index, texture) in material.textures.iter().enumerate() {
- let gl_texture = gl_textures
- .entry(texture.id())
- .or_insert_with(|| create_gl_texture(texture));
+ set_enabled(
+ &current_graphics_context,
+ Capability::MultiSample,
+ graphics_props.multisampling_sample_cnt.is_some(),
+ );
- let texture_unit = TextureUnit::from_num(index).expect("Too many textures");
+ if graphics_props.debug {
+ enable(&current_graphics_context, Capability::DebugOutput);
+ enable(
+ &current_graphics_context,
+ Capability::DebugOutputSynchronous,
+ );
- set_active_texture_unit(texture_unit);
+ set_debug_message_callback(
+ &current_graphics_context,
+ opengl_debug_message_cb,
+ );
- gl_texture.bind();
+ match set_debug_message_control(
+ &current_graphics_context,
+ None,
+ None,
+ None,
+ &[],
+ MessageIdsAction::Disable,
+ ) {
+ Ok(()) => {}
+ Err(GlSetDebugMessageControlError::TooManyIds {
+ id_cnt: _,
+ max_id_cnt: _,
+ }) => {
+ unreachable!() // No ids are given
+ }
+ }
}
- shader_program.activate();
+ let graphics_context_ent_id = actions.spawn((GraphicsContext {
+ context,
+ shader_program: None,
+ textures_objs: HashMap::new(),
+ default_1x1_texture_obj: None,
+ graphics_mesh_store: GraphicsMeshStore::default(),
+ },));
+
+ actions.add_components(
+ window_ent_id,
+ (
+ WindowGraphicsSurface { surface },
+ Pair::builder()
+ .relation::<WithGraphicsContext>()
+ .target_id(graphics_context_ent_id)
+ .build(),
+ ),
+ );
+ }
+}
- if let Some(draw_flags) = &draw_flags {
- crate::opengl::set_polygon_mode(
- draw_flags.polygon_mode_config.face,
- draw_flags.polygon_mode_config.mode,
+#[tracing::instrument(skip_all)]
+#[allow(clippy::too_many_arguments)]
+fn render(
+ query: Query<RenderableEntity<'_>, (Without<NoDraw>,)>,
+ point_light_query: Query<(&PointLight, &WorldPosition)>,
+ directional_lights: Query<(&DirectionalLight,)>,
+ camera_query: Query<(&Camera, &WorldPosition, &ActiveCamera)>,
+ window_query: Query<(
+ &Window,
+ &WindowGraphicsSurface,
+ &GraphicsProperties,
+ Pair<WithGraphicsContext, Wildcard>,
+ )>,
+ global_light: Single<GlobalLight>,
+ assets: Single<Assets>,
+ mut actions: Actions,
+)
+{
+ for (
+ window_ent_id,
+ (window, window_graphics_surface, window_graphics_props, graphics_context_pair),
+ ) in window_query.iter_with_euids()
+ {
+ let Some(graphics_context_ent) = graphics_context_pair.get_target_ent() else {
+ tracing::error!("Window's associated graphics context entity does not exist");
+ actions.remove_components(window_ent_id, [graphics_context_pair.id()]);
+ continue;
+ };
+
+ let Some(mut graphics_context) =
+ graphics_context_ent.get_mut::<GraphicsContext>()
+ else {
+ tracing::error!(
+ "Graphics context entity does not have a GraphicsContext component"
);
- }
+ return;
+ };
- draw_mesh(gl_objects);
+ let GraphicsContext {
+ ref context,
+ ref mut shader_program,
+ ref mut textures_objs,
+ ref mut default_1x1_texture_obj,
+ ref mut graphics_mesh_store,
+ } = *graphics_context;
+
+ let Some((camera, camera_world_pos, _)) = camera_query.iter().next() else {
+ tracing::warn!("No current camera. Nothing will be rendered");
+ return;
+ };
- if draw_flags.is_some() {
- let default_polygon_mode_config = PolygonModeConfig::default();
+ let Ok(current_graphics_context) =
+ context.make_current(&window_graphics_surface.surface)
+ else {
+ tracing::error!("Failed to make graphics context current");
+ continue;
+ };
+
+ let directional_lights = directional_lights.iter().collect::<Vec<_>>();
+
+ let shader_program = shader_program.get_or_insert_with(|| {
+ create_default_shader_program(&current_graphics_context).unwrap()
+ });
+
+ let mut clear_mask = BufferClearMask::COLOR;
+
+ clear_mask.set(BufferClearMask::DEPTH, window_graphics_props.depth_test);
+
+ clear_buffers(&current_graphics_context, clear_mask);
+
+ for (
+ euid,
+ (
+ model,
+ material_flags,
+ position,
+ scale,
+ draw_flags,
+ data_in_graphics_ctx_pairs,
+ ),
+ ) 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 graphics_mesh_id = match data_in_graphics_ctx_pairs
+ .get_with_target_id(graphics_context_ent.uid())
+ {
+ Some(data_in_graphics_ctx_pair) => {
+ let Some(data_in_graphics_ctx) =
+ data_in_graphics_ctx_pair.get_data::<DataInGraphicsContext>()
+ else {
+ tracing::warn!(
+ concat!(
+ "Pair with relation {} ({}) has no data or data with a ",
+ "wrong type. This pair will be removed"
+ ),
+ type_name::<DataInGraphicsContext>(),
+ data_in_graphics_ctx_pair.id()
+ );
+
+ actions.remove_components(euid, [data_in_graphics_ctx_pair.id()]);
+ continue;
+ };
+
+ data_in_graphics_ctx.graphics_mesh_id
+ }
+ None => {
+ let graphics_mesh = match GraphicsMesh::new(
+ &current_graphics_context,
+ &model_data.mesh,
+ ) {
+ Ok(graphics_mesh) => graphics_mesh,
+ Err(err) => {
+ tracing::error!(
+ "Failed to create {}: {err}",
+ type_name::<GraphicsMesh>()
+ );
+
+ // This system should not try again
+ actions.add_components(euid, (NoDraw,));
+
+ continue;
+ }
+ };
+
+ let graphics_mesh_id = graphics_mesh_store.insert(graphics_mesh);
+
+ actions.add_components(
+ euid,
+ (Pair::builder()
+ .relation_as_data(DataInGraphicsContext { graphics_mesh_id })
+ .target_id(graphics_context_ent.uid())
+ .build(),),
+ );
+
+ graphics_mesh_id
+ }
+ };
- crate::opengl::set_polygon_mode(
- default_polygon_mode_config.face,
- default_polygon_mode_config.mode,
+ let Some(graphics_mesh) =
+ graphics_mesh_store.graphics_meshes.get(&graphics_mesh_id)
+ else {
+ tracing::error!("Graphics mesh with ID: {graphics_mesh_id:?} not found");
+ continue;
+ };
+
+ apply_transformation_matrices(
+ &current_graphics_context,
+ Transformation {
+ position: position.map(|pos| *pos).unwrap_or_default().position,
+ scale: scale.map(|scale| *scale).unwrap_or_default().scale,
+ },
+ shader_program,
+ &camera,
+ &camera_world_pos,
+ window.inner_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(
+ &current_graphics_context,
+ &material,
+ &material_flags,
+ &global_light,
+ shader_program,
+ (point_light_query.iter(), point_light_query.iter().count()),
+ directional_lights
+ .iter()
+ .map(|(dir_light,)| &**dir_light)
+ .collect::<Vec<_>>()
+ .as_slice(),
+ &camera_world_pos,
);
+
+ match create_bind_material_textures(
+ &current_graphics_context,
+ &material,
+ &assets,
+ textures_objs,
+ default_1x1_texture_obj,
+ ) {
+ Ok(()) => {}
+ Err(CreateBindMaterialTexturesError::MissingTextureAsset) => {
+ continue;
+ }
+ Err(
+ err @ CreateBindMaterialTexturesError::CreateTextureFailed { .. },
+ ) => {
+ tracing::error!(
+ "Creating &/ binding material textures failed: {err}"
+ );
+
+ // This system should not try again
+ actions.add_components(euid, (NoDraw,));
+
+ continue;
+ }
+ }
+
+ shader_program.activate(&current_graphics_context);
+
+ if let Some(draw_flags) = &draw_flags {
+ opengl_bindings::misc::set_polygon_mode(
+ &current_graphics_context,
+ draw_flags.polygon_mode_config.face,
+ draw_flags.polygon_mode_config.mode,
+ );
+ }
+
+ if let Err(err) = draw_mesh(&current_graphics_context, &graphics_mesh) {
+ tracing::error!(
+ entity_id = %euid,
+ graphics_context_entity_id = %graphics_context_ent.uid(),
+ "Failed to draw mesh: {err}",
+ );
+
+ // This system should not try again
+ actions.add_components(euid, (NoDraw,));
+
+ continue;
+ };
+
+ if draw_flags.is_some() {
+ let default_polygon_mode_config = PolygonModeConfig::default();
+
+ opengl_bindings::misc::set_polygon_mode(
+ &current_graphics_context,
+ default_polygon_mode_config.face,
+ default_polygon_mode_config.mode,
+ );
+ }
+ }
+
+ if let Err(err) = window_graphics_surface
+ .surface
+ .swap_buffers(context.context())
+ {
+ tracing::error!("Failed to swap buffers: {err}");
}
}
}
-#[derive(Debug, Default, Component)]
-struct GlobalGlObjects
+fn create_default_texture(current_context: &CurrentContextWithFns<'_>) -> GlTexture
{
- shader_program: Option<GlShaderProgram>,
- textures: HashMap<TextureId, GlTexture>,
+ match create_gl_texture(
+ current_context,
+ &Image::from_color(Dimens { width: 1, height: 1 }, Color::WHITE_U8),
+ &TextureProperties::default(),
+ ) {
+ Ok(gl_texture) => gl_texture,
+ Err(
+ GlTextureGenerateError::SizeWidthValueTooLarge { value: _, max_value: _ }
+ | GlTextureGenerateError::SizeHeightValueTooLarge { value: _, max_value: _ },
+ ) => unreachable!(),
+ }
}
-fn set_viewport(position: Vec2<u32>, size: Dimens<u32>)
+fn create_bind_material_textures(
+ current_context: &CurrentContextWithFns<'_>,
+ material: &Material,
+ assets: &Assets,
+ texture_objs: &mut HashMap<AssetId, GlTexture>,
+ default_1x1_texture_obj: &mut Option<GlTexture>,
+) -> Result<(), CreateBindMaterialTexturesError>
{
- crate::opengl::set_viewport(position, size);
+ 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_texture_obj
+ .get_or_insert_with(|| create_default_texture(current_context));
+
+ gl_texture.bind_to_texture_unit(current_context, texture_unit);
+
+ continue;
+ };
+
+ let texture_image_asset_id = texture.asset_handle.id();
+
+ let gl_texture = match texture_objs.get(&texture_image_asset_id) {
+ Some(gl_texture) => gl_texture,
+ None => {
+ let Some(image) = assets.get::<Image>(&texture.asset_handle) else {
+ tracing::trace!(handle=?texture.asset_handle, "Missing texture asset");
+ return Err(CreateBindMaterialTexturesError::MissingTextureAsset);
+ };
+
+ texture_objs.entry(texture_image_asset_id).or_insert(
+ create_gl_texture(current_context, image, &texture.properties)
+ .map_err(|err| {
+ CreateBindMaterialTexturesError::CreateTextureFailed {
+ err,
+ image_asset_id: texture_image_asset_id,
+ }
+ })?,
+ )
+ }
+ };
+
+ gl_texture.bind_to_texture_unit(current_context, texture_unit);
+ }
+
+ Ok(())
}
-fn initialize_debug()
+#[derive(Debug, thiserror::Error)]
+enum CreateBindMaterialTexturesError
{
- enable_debug_output();
+ #[error("Missing texture asset")]
+ MissingTextureAsset,
- set_debug_message_callback(opengl_debug_message_cb);
- set_debug_message_control(None, None, None, &[], MessageIdsAction::Disable);
+ #[error("Failed to create texture from image asset with ID {image_asset_id:?}")]
+ CreateTextureFailed
+ {
+ #[source]
+ err: GlTextureGenerateError,
+ image_asset_id: AssetId,
+ },
}
-fn draw_mesh(gl_objects: &GlObjects)
+fn set_viewport(
+ current_context: &CurrentContextWithFns<'_>,
+ position: Vec2<u32>,
+ size: &Dimens<u32>,
+) -> Result<(), GlSetViewportError>
{
- gl_objects.vertex_arr.bind();
+ let position =
+ opengl_bindings::data_types::Vec2::<u32> { x: position.x, y: position.y };
+
+ let size = opengl_bindings::data_types::Dimens::<u32> {
+ width: size.width,
+ height: size.height,
+ };
+
+ opengl_bindings::misc::set_viewport(current_context, &position, &size)
+}
- if gl_objects.index_buffer.is_some() {
- VertexArray::draw_elements(PrimitiveKind::Triangles, 0, gl_objects.element_cnt);
+fn draw_mesh(
+ current_context: &CurrentContextWithFns<'_>,
+ graphics_mesh: &GraphicsMesh,
+) -> Result<(), GlDrawError>
+{
+ graphics_mesh.vertex_arr.bind(current_context);
+
+ if graphics_mesh.index_buffer.is_some() {
+ VertexArray::draw_elements(
+ current_context,
+ PrimitiveKind::Triangles,
+ 0,
+ graphics_mesh.element_cnt,
+ )?;
} else {
- VertexArray::draw_arrays(PrimitiveKind::Triangles, 0, gl_objects.element_cnt);
+ VertexArray::draw_arrays(
+ current_context,
+ PrimitiveKind::Triangles,
+ 0,
+ graphics_mesh.element_cnt,
+ )?;
}
+
+ Ok(())
}
-fn create_gl_texture(texture: &Texture) -> GlTexture
+fn create_gl_texture(
+ current_context: &CurrentContextWithFns<'_>,
+ image: &Image,
+ texture_properties: &TextureProperties,
+) -> Result<GlTexture, GlTextureGenerateError>
{
- let mut gl_texture = GlTexture::new();
+ let gl_texture = GlTexture::new(current_context);
gl_texture.generate(
- *texture.dimensions(),
- texture.image().as_bytes(),
- texture.pixel_data_format(),
+ current_context,
+ &image.dimensions().into(),
+ image.as_bytes(),
+ match image.color_type() {
+ ImageColorType::Rgb8 => GlTexturePixelDataFormat::Rgb8,
+ ImageColorType::Rgba8 => GlTexturePixelDataFormat::Rgba8,
+ _ => {
+ unimplemented!();
+ }
+ },
+ )?;
+
+ gl_texture.set_wrap(
+ current_context,
+ texture_wrapping_to_gl(texture_properties.wrap),
);
- gl_texture.apply_properties(texture.properties());
+ gl_texture.set_magnifying_filter(
+ current_context,
+ texture_filtering_to_gl(texture_properties.magnifying_filter),
+ );
+
+ gl_texture.set_minifying_filter(
+ current_context,
+ texture_filtering_to_gl(texture_properties.minifying_filter),
+ );
- gl_texture
+ Ok(gl_texture)
}
const VERTEX_GLSL_SHADER_SRC: &str = include_str!("opengl/glsl/vertex.glsl");
@@ -303,32 +1082,34 @@ 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>
+fn create_default_shader_program(
+ current_context: &CurrentContextWithFns<'_>,
+) -> Result<GlShaderProgram, CreateShaderError>
{
- let mut vertex_shader = GlShader::new(ShaderKind::Vertex);
+ let vertex_shader = GlShader::new(current_context, ShaderKind::Vertex);
- vertex_shader.set_source(&*glsl_preprocess(
- VERTEX_GLSL_SHADER_SRC,
- &get_glsl_shader_content,
- )?)?;
+ vertex_shader.set_source(
+ current_context,
+ &*glsl_preprocess(VERTEX_GLSL_SHADER_SRC, &get_glsl_shader_content)?,
+ )?;
- vertex_shader.compile()?;
+ vertex_shader.compile(current_context)?;
- let mut fragment_shader = GlShader::new(ShaderKind::Fragment);
+ let fragment_shader = GlShader::new(current_context, ShaderKind::Fragment);
- fragment_shader.set_source(&*glsl_preprocess(
- FRAGMENT_GLSL_SHADER_SRC,
- &get_glsl_shader_content,
- )?)?;
+ fragment_shader.set_source(
+ current_context,
+ &*glsl_preprocess(FRAGMENT_GLSL_SHADER_SRC, &get_glsl_shader_content)?,
+ )?;
- fragment_shader.compile()?;
+ fragment_shader.compile(current_context)?;
- let mut gl_shader_program = GlShaderProgram::new();
+ let gl_shader_program = GlShaderProgram::new(current_context);
- gl_shader_program.attach(&vertex_shader);
- gl_shader_program.attach(&fragment_shader);
+ gl_shader_program.attach(current_context, &vertex_shader);
+ gl_shader_program.attach(current_context, &fragment_shader);
- gl_shader_program.link()?;
+ gl_shader_program.link(current_context)?;
Ok(gl_shader_program)
}
@@ -359,144 +1140,75 @@ fn get_glsl_shader_content(path: &Path) -> Result<Vec<u8>, std::io::Error>
))
}
-#[derive(Debug, Component)]
-struct GlObjects
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+struct GraphicsMeshId
{
- /// Vertex and index buffer has to live as long as the vertex array
- vertex_buffer: Buffer<Vertex>,
- index_buffer: Option<Buffer<u32>>,
- element_cnt: u32,
-
- vertex_arr: VertexArray,
-}
-
-impl GlObjects
-{
- #[tracing::instrument(skip_all)]
- fn new(mesh: &Mesh) -> Self
- {
- tracing::trace!(
- "Creating vertex array, vertex buffer{}",
- if mesh.indices().is_some() {
- " and index buffer"
- } else {
- ""
- }
- );
-
- let mut vertex_arr = VertexArray::new();
- let mut vertex_buffer = Buffer::new();
-
- vertex_buffer.store(mesh.vertices(), BufferUsage::Static);
-
- vertex_arr.bind_vertex_buffer(0, &vertex_buffer, 0);
-
- let mut offset = 0u32;
-
- for attrib in Vertex::attrs() {
- vertex_arr.enable_attrib(attrib.index);
-
- vertex_arr.set_attrib_format(
- attrib.index,
- match attrib.component_type {
- AttributeComponentType::Float => VertexArrayDataType::Float,
- },
- false,
- offset,
- );
-
- vertex_arr.set_attrib_vertex_buf_binding(attrib.index, 0);
-
- offset += attrib.component_size * attrib.component_cnt as u32;
- }
-
- if let Some(indices) = mesh.indices() {
- let mut index_buffer = Buffer::new();
-
- index_buffer.store(indices, BufferUsage::Static);
-
- vertex_arr.bind_element_buffer(&index_buffer);
-
- return Self {
- vertex_buffer,
- index_buffer: Some(index_buffer),
- element_cnt: indices
- .len()
- .try_into()
- .expect("Mesh index count does not fit into a 32-bit unsigned int"),
- vertex_arr,
- };
- }
-
- Self {
- vertex_buffer,
- index_buffer: None,
- 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() }),
- element_cnt: self.element_cnt,
- // SAFETY: The vertex array will never become dropped (NeverDrop ensures it)
- vertex_arr: unsafe { self.vertex_arr.clone_unsafe() },
- })
- }
+ inner: usize,
}
fn apply_transformation_matrices(
+ current_context: &CurrentContextWithFns<'_>,
transformation: Transformation,
gl_shader_program: &mut GlShaderProgram,
camera: &Camera,
- camera_pos: &Position,
- window_size: Dimens<u32>,
+ camera_world_pos: &WorldPosition,
+ window_size: &Dimens<u32>,
)
{
- gl_shader_program
- .set_uniform_matrix_4fv(c"model", &create_transformation_matrix(transformation));
+ gl_shader_program.set_uniform(
+ current_context,
+ c"model",
+ &opengl_bindings::data_types::Matrix {
+ items: create_transformation_matrix(transformation).items,
+ },
+ );
- 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(
+ current_context,
+ c"view",
+ &opengl_bindings::data_types::Matrix { items: view_matrix.items },
+ );
#[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(
+ current_context,
+ c"projection",
+ &opengl_bindings::data_types::Matrix { items: proj_matrix.items },
+ );
}
-fn apply_light<PointLightHolder>(
+fn apply_light<'point_light>(
+ current_context: &CurrentContextWithFns<'_>,
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"
);
@@ -506,16 +1218,20 @@ fn apply_light<PointLightHolder>(
);
for (dir_light_index, dir_light) in directional_lights.iter().enumerate() {
- gl_shader_program.set_uniform_vec_3fv(
+ let direction: opengl_bindings::data_types::Vec3<_> = dir_light.direction.into();
+
+ gl_shader_program.set_uniform(
+ current_context,
&create_light_uniform_name(
"directional_lights",
dir_light_index,
"direction",
),
- &dir_light.direction,
+ &direction,
);
set_light_phong_uniforms(
+ current_context,
gl_shader_program,
"directional_lights",
dir_light_index,
@@ -525,127 +1241,163 @@ 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);
+ gl_shader_program.set_uniform(
+ current_context,
+ c"directional_light_cnt",
+ &(directional_lights.len() as i32),
+ );
+
+ for (point_light_index, (point_light, point_light_world_pos)) in
+ point_light_iter.enumerate()
+ {
+ let pos: opengl_bindings::data_types::Vec3<_> =
+ (point_light_world_pos.position + point_light.local_position).into();
- for (point_light_index, point_light) in point_lights.iter().enumerate() {
- gl_shader_program.set_uniform_vec_3fv(
+ gl_shader_program.set_uniform(
+ current_context,
&create_light_uniform_name("point_lights", point_light_index, "position"),
- &point_light.position,
+ &pos,
);
set_light_phong_uniforms(
+ current_context,
gl_shader_program,
"point_lights",
point_light_index,
- &**point_light,
+ &*point_light,
);
set_light_attenuation_uniforms(
+ current_context,
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(
+ current_context,
+ c"point_light_cnt",
+ &(point_light_cnt as i32),
+ );
- gl_shader_program.set_uniform_vec_3fv(
- c"material.ambient",
- &if material_flags.use_ambient_color {
+ let ambient: opengl_bindings::data_types::Vec3<_> =
+ Vec3::from(if material_flags.use_ambient_color {
material.ambient.clone()
} else {
global_light.ambient.clone()
- }
- .into(),
- );
+ })
+ .into();
- gl_shader_program
- .set_uniform_vec_3fv(c"material.diffuse", &material.diffuse.clone().into());
+ gl_shader_program.set_uniform(current_context, c"material.ambient", &ambient);
- #[allow(clippy::cast_possible_wrap)]
- gl_shader_program
- .set_uniform_vec_3fv(c"material.specular", &material.specular.clone().into());
+ let diffuse: opengl_bindings::data_types::Vec3<_> =
+ Vec3::from(material.diffuse.clone()).into();
+
+ gl_shader_program.set_uniform(current_context, c"material.diffuse", &diffuse);
+
+ let specular: opengl_bindings::data_types::Vec3<_> =
+ Vec3::from(material.specular.clone()).into();
- let texture_map = material
- .textures
- .iter()
- .enumerate()
- .map(|(index, texture)| (texture.id(), index))
- .collect::<HashMap<_, _>>();
+ #[allow(clippy::cast_possible_wrap)]
+ gl_shader_program.set_uniform(current_context, c"material.specular", &specular);
#[allow(clippy::cast_possible_wrap)]
- gl_shader_program.set_uniform_1i(
+ gl_shader_program.set_uniform(
+ current_context,
c"material.ambient_map",
- *texture_map.get(&material.ambient_map).unwrap() as i32,
+ &(AMBIENT_MAP_TEXTURE_UNIT as i32),
);
#[allow(clippy::cast_possible_wrap)]
- gl_shader_program.set_uniform_1i(
+ gl_shader_program.set_uniform(
+ current_context,
c"material.diffuse_map",
- *texture_map.get(&material.diffuse_map).unwrap() as i32,
+ &(DIFFUSE_MAP_TEXTURE_UNIT as i32),
);
#[allow(clippy::cast_possible_wrap)]
- gl_shader_program.set_uniform_1i(
+ gl_shader_program.set_uniform(
+ current_context,
c"material.specular_map",
- *texture_map.get(&material.specular_map).unwrap() as i32,
+ &(SPECULAR_MAP_TEXTURE_UNIT as i32),
+ );
+
+ gl_shader_program.set_uniform(
+ current_context,
+ c"material.shininess",
+ &material.shininess,
);
- gl_shader_program.set_uniform_1fv(c"material.shininess", material.shininess);
+ let view_pos: opengl_bindings::data_types::Vec3<_> = camera_world_pos.position.into();
- gl_shader_program.set_uniform_vec_3fv(c"view_pos", &camera_pos.position);
+ gl_shader_program.set_uniform(current_context, c"view_pos", &view_pos);
}
fn set_light_attenuation_uniforms(
+ current_context: &CurrentContextWithFns<'_>,
gl_shader_program: &mut GlShaderProgram,
light_array: &str,
light_index: usize,
light: &PointLight,
)
{
- gl_shader_program.set_uniform_1fv(
+ gl_shader_program.set_uniform(
+ current_context,
&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(
+ current_context,
&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(
+ current_context,
&create_light_uniform_name(
light_array,
light_index,
"attenuation_props.quadratic",
),
- light.attenuation_params.quadratic,
+ &light.attenuation_params.quadratic,
);
}
fn set_light_phong_uniforms(
+ current_context: &CurrentContextWithFns<'_>,
gl_shader_program: &mut GlShaderProgram,
light_array: &str,
light_index: usize,
light: &impl Light,
)
{
- gl_shader_program.set_uniform_vec_3fv(
+ gl_shader_program.set_uniform(
+ current_context,
&create_light_uniform_name(light_array, light_index, "phong.diffuse"),
- &light.diffuse().clone().into(),
+ &opengl_bindings::data_types::Vec3 {
+ x: light.diffuse().red,
+ y: light.diffuse().green,
+ z: light.diffuse().blue,
+ },
);
- gl_shader_program.set_uniform_vec_3fv(
+ gl_shader_program.set_uniform(
+ current_context,
&create_light_uniform_name(light_array, light_index, "phong.specular"),
- &light.specular().clone().into(),
+ &opengl_bindings::data_types::Vec3 {
+ x: light.specular().red,
+ y: light.specular().green,
+ z: light.specular().blue,
+ },
);
}
@@ -694,11 +1446,11 @@ 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
}
@@ -733,7 +1485,8 @@ fn opengl_debug_message_cb(
let backtrace = Backtrace::capture();
if matches!(backtrace.status(), BacktraceStatus::Captured) {
- event!(Level::TRACE, "{backtrace}");
+ tracing::error!("{backtrace}");
+ // event!(Level::TRACE, "{backtrace}");
}
}
MessageType::Other => {
@@ -761,3 +1514,74 @@ 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,
+ }
+}
+
+impl<Value: ReprC + Copy> From<Vec2<Value>> for opengl_bindings::data_types::Vec2<Value>
+{
+ fn from(vec2: Vec2<Value>) -> Self
+ {
+ Self { x: vec2.x, y: vec2.y }
+ }
+}
+
+impl<Value: ReprC + Copy> From<Vec3<Value>> for opengl_bindings::data_types::Vec3<Value>
+{
+ fn from(vec3: Vec3<Value>) -> Self
+ {
+ Self { x: vec3.x, y: vec3.y, z: vec3.z }
+ }
+}
+
+impl<Value: Copy> From<Dimens<Value>> for opengl_bindings::data_types::Dimens<Value>
+{
+ fn from(dimens: Dimens<Value>) -> Self
+ {
+ Self {
+ width: dimens.width,
+ height: dimens.height,
+ }
+ }
+}
+
+impl From<crate::draw_flags::PolygonMode> for opengl_bindings::misc::PolygonMode
+{
+ fn from(mode: crate::draw_flags::PolygonMode) -> Self
+ {
+ match mode {
+ crate::draw_flags::PolygonMode::Point => Self::Point,
+ crate::draw_flags::PolygonMode::Fill => Self::Fill,
+ crate::draw_flags::PolygonMode::Line => Self::Line,
+ }
+ }
+}
+
+impl From<crate::draw_flags::PolygonModeFace> for opengl_bindings::misc::PolygonModeFace
+{
+ fn from(face: crate::draw_flags::PolygonModeFace) -> Self
+ {
+ match face {
+ crate::draw_flags::PolygonModeFace::Front => Self::Front,
+ crate::draw_flags::PolygonModeFace::Back => Self::Back,
+ crate::draw_flags::PolygonModeFace::FrontAndBack => Self::FrontAndBack,
+ }
+ }
+}
diff --git a/engine/src/renderer/opengl/glsl/light.glsl b/engine/src/renderer/opengl/glsl/light.glsl
index 1bc23a4..f12b5fe 100644
--- a/engine/src/renderer/opengl/glsl/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/src/renderer/opengl/glutin_compat.rs b/engine/src/renderer/opengl/glutin_compat.rs
new file mode 100644
index 0000000..cfd6ea7
--- /dev/null
+++ b/engine/src/renderer/opengl/glutin_compat.rs
@@ -0,0 +1,268 @@
+// Original file:
+// https://github.com/rust-windowing/glutin/blob/
+// 0433af9018febe0696c485ed9d66c40dad41f2d4/glutin-winit/src/lib.rs
+//
+// Copyright © 2022 Kirill Chibisov
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the “Software”), to deal
+// in the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+// of the Software, and to permit persons to whom the Software is furnished to do
+// so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+//! This library provides helpers for cross-platform [`glutin`] bootstrapping
+//! with [`winit`].
+
+#![deny(rust_2018_idioms)]
+#![deny(rustdoc::broken_intra_doc_links)]
+#![deny(clippy::all)]
+#![deny(missing_debug_implementations)]
+#![deny(missing_docs)]
+#![cfg_attr(clippy, deny(warnings))]
+
+use glutin::config::{Config, ConfigTemplateBuilder};
+use glutin::display::{Display, DisplayApiPreference};
+use glutin::error::Error as GlutinError;
+#[cfg(x11_platform)]
+use glutin::platform::x11::X11GlConfigExt;
+use glutin::prelude::*;
+use raw_window_handle::{DisplayHandle, RawWindowHandle, WindowHandle};
+
+use crate::windowing::window::CreationAttributes as WindowCreationAttributes;
+
+#[cfg(all(not(egl_backend), not(glx_backend), not(wgl_backend), not(cgl_backend)))]
+compile_error!("Please select at least one api backend");
+
+/// The helper to perform [`Display`] creation and OpenGL platform
+/// bootstrapping with the help of [`winit`] with little to no platform specific
+/// code.
+///
+/// This is only required for the initial setup. If you want to create
+/// additional windows just use the [`finalize_window`] function and the
+/// configuration you've used either for the original window or picked with the
+/// existing [`Display`].
+///
+/// [`winit`]: winit
+/// [`Display`]: glutin::display::Display
+#[derive(Default, Debug, Clone)]
+pub struct DisplayBuilder
+{
+ preference: ApiPreference,
+ window_attributes: WindowCreationAttributes,
+}
+
+impl DisplayBuilder
+{
+ /// Create new display builder.
+ pub fn new() -> Self
+ {
+ Default::default()
+ }
+
+ /// The preference in picking the configuration.
+ #[allow(dead_code)]
+ pub fn with_preference(mut self, preference: ApiPreference) -> Self
+ {
+ self.preference = preference;
+ self
+ }
+
+ /// The window attributes to use when building a window.
+ ///
+ /// By default no window is created.
+ pub fn with_window_attributes(
+ mut self,
+ window_creation_attrs: WindowCreationAttributes,
+ ) -> Self
+ {
+ self.window_attributes = window_creation_attrs;
+ self
+ }
+
+ /// Initialize the OpenGL platform and create a compatible window to use
+ /// with it when the [`WindowAttributes`] was passed with
+ /// [`Self::with_window_attributes()`]. It's optional, since on some
+ /// platforms like `Android` it is not available early on, so you want to
+ /// find configuration and later use it with the [`finalize_window`].
+ /// But if you don't care about such platform you can always pass
+ /// [`WindowAttributes`].
+ ///
+ /// # Api-specific
+ ///
+ /// **WGL:** - [`WindowAttributes`] **must** be passed in
+ /// [`Self::with_window_attributes()`] if modern OpenGL(ES) is desired,
+ /// otherwise only builtin functions like `glClear` will be available.
+ pub fn build<ConfigPickerFn>(
+ self,
+ window_handle: Option<WindowHandle<'_>>,
+ display_handle: &DisplayHandle<'_>,
+ template_builder: ConfigTemplateBuilder,
+ config_picker_fn: ConfigPickerFn,
+ ) -> Result<(WindowCreationAttributes, Config), Error>
+ where
+ ConfigPickerFn: FnOnce(Box<dyn Iterator<Item = Config> + '_>) -> Option<Config>,
+ {
+ // XXX with WGL backend window should be created first.
+ let raw_window_handle = if cfg!(wgl_backend) {
+ let Some(window_handle) = window_handle else {
+ return Err(Error::WindowRequired);
+ };
+
+ Some(window_handle.as_raw())
+ } else {
+ None
+ };
+
+ let gl_display =
+ create_display(display_handle, self.preference, raw_window_handle)
+ .map_err(Error::CreateDisplayFailed)?;
+
+ // XXX the native window must be passed to config picker when WGL is used
+ // otherwise very limited OpenGL features will be supported.
+ #[cfg(wgl_backend)]
+ let template_builder = if let Some(raw_window_handle) = raw_window_handle {
+ template_builder.compatible_with_native_window(raw_window_handle)
+ } else {
+ template_builder
+ };
+
+ let template = template_builder.build();
+
+ // SAFETY: The RawWindowHandle passed on the config template
+ // (when cfg(wgl_backend)) will always point to a valid object since it is
+ // derived from the window_handle argument which when Some is a WindowHandle and
+ // WindowHandles always point to a valid object
+ let gl_configs = unsafe { gl_display.find_configs(template) }
+ .map_err(Error::FindConfigsFailed)?;
+
+ let picked_gl_config =
+ config_picker_fn(gl_configs).ok_or(Error::NoConfigPicked)?;
+
+ #[cfg(not(wgl_backend))]
+ let window_attrs =
+ { finalize_window_creation_attrs(self.window_attributes, &picked_gl_config) };
+
+ #[cfg(wgl_backend)]
+ let window_attrs = self.window_attributes;
+
+ Ok((window_attrs, picked_gl_config))
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error
+{
+ #[error("Failed to create display")]
+ CreateDisplayFailed(#[source] GlutinError),
+
+ #[error("Failed to find configs")]
+ FindConfigsFailed(#[source] GlutinError),
+
+ #[error("No config was picked by config picker function")]
+ NoConfigPicked,
+
+ #[error("Window required for building display on current platform")]
+ WindowRequired,
+}
+
+fn create_display(
+ display_handle: &DisplayHandle<'_>,
+ _api_preference: ApiPreference,
+ _raw_window_handle: Option<RawWindowHandle>,
+) -> Result<Display, GlutinError>
+{
+ #[cfg(egl_backend)]
+ let _preference = DisplayApiPreference::Egl;
+
+ #[cfg(glx_backend)]
+ let _preference = DisplayApiPreference::Glx(Box::new(
+ crate::windowing::window::platform::x11::register_xlib_error_hook,
+ ));
+
+ #[cfg(cgl_backend)]
+ let _preference = DisplayApiPreference::Cgl;
+
+ #[cfg(wgl_backend)]
+ let _preference = DisplayApiPreference::Wgl(_raw_window_handle);
+
+ #[cfg(all(egl_backend, glx_backend))]
+ let _preference = match _api_preference {
+ ApiPreference::PreferEgl => DisplayApiPreference::EglThenGlx(Box::new(
+ crate::windowing::window::platform::x11::register_xlib_error_hook,
+ )),
+ ApiPreference::FallbackEgl => DisplayApiPreference::GlxThenEgl(Box::new(
+ crate::windowing::window::platform::x11::register_xlib_error_hook,
+ )),
+ };
+
+ #[cfg(all(wgl_backend, egl_backend))]
+ let _preference = match _api_preference {
+ ApiPreference::PreferEgl => DisplayApiPreference::EglThenWgl(_raw_window_handle),
+ ApiPreference::FallbackEgl => {
+ DisplayApiPreference::WglThenEgl(_raw_window_handle)
+ }
+ };
+
+ let handle = display_handle.as_raw();
+ unsafe { Ok(Display::new(handle, _preference)?) }
+}
+
+/// Finalize [`Window`] creation by applying the options from the [`Config`], be
+/// aware that it could remove incompatible options from the window builder like
+/// `transparency`, when the provided config doesn't support it.
+///
+/// [`Window`]: winit::window::Window
+/// [`Config`]: glutin::config::Config
+#[cfg(not(wgl_backend))]
+fn finalize_window_creation_attrs(
+ mut attributes: WindowCreationAttributes,
+ gl_config: &Config,
+) -> WindowCreationAttributes
+{
+ // Disable transparency if the end config doesn't support it.
+ if gl_config.supports_transparency() == Some(false) {
+ attributes = attributes.with_transparent(false);
+ }
+
+ #[cfg(x11_platform)]
+ let attributes = if let Some(x11_visual) = gl_config.x11_visual() {
+ attributes.with_x11_visual(x11_visual.visual_id() as _)
+ } else {
+ attributes
+ };
+
+ attributes
+}
+
+/// Simplified version of the [`DisplayApiPreference`] which is used to simplify
+/// cross platform window creation.
+///
+/// To learn about platform differences the [`DisplayApiPreference`] variants.
+///
+/// [`DisplayApiPreference`]: glutin::display::DisplayApiPreference
+#[allow(dead_code)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum ApiPreference
+{
+ /// Prefer `EGL` over system provider like `GLX` and `WGL`.
+ PreferEgl,
+
+ /// Fallback to `EGL` when failed to create the system profile.
+ ///
+ /// This behavior is used by default. However consider using
+ /// [`Self::PreferEgl`] if you don't care about missing EGL features.
+ #[default]
+ FallbackEgl,
+}
diff --git a/engine/src/renderer/opengl/graphics_mesh.rs b/engine/src/renderer/opengl/graphics_mesh.rs
new file mode 100644
index 0000000..9b929c0
--- /dev/null
+++ b/engine/src/renderer/opengl/graphics_mesh.rs
@@ -0,0 +1,120 @@
+use opengl_bindings::buffer::{Buffer as GlBuffer, Usage as GlBufferUsage};
+use opengl_bindings::vertex_array::{
+ DataType as GlVertexArrayDataType,
+ VertexArray as GlVertexArray,
+};
+use opengl_bindings::CurrentContextWithFns as GlCurrentContextWithFns;
+
+use crate::mesh::Mesh;
+use crate::renderer::opengl::vertex::{
+ AttributeComponentType as VertexAttributeComponentType,
+ Vertex as RendererVertex,
+};
+
+#[derive(Debug)]
+pub struct GraphicsMesh
+{
+ /// Vertex and index buffer has to live as long as the vertex array
+ _vertex_buffer: GlBuffer<RendererVertex>,
+ pub index_buffer: Option<GlBuffer<u32>>,
+ pub element_cnt: u32,
+ pub vertex_arr: GlVertexArray,
+}
+
+impl GraphicsMesh
+{
+ #[tracing::instrument(skip_all)]
+ pub fn new(
+ current_context: &GlCurrentContextWithFns<'_>,
+ mesh: &Mesh,
+ ) -> Result<Self, Error>
+ {
+ tracing::trace!(
+ "Creating vertex array, vertex buffer{}",
+ if mesh.indices().is_some() {
+ " and index buffer"
+ } else {
+ ""
+ }
+ );
+
+ let vertex_arr = GlVertexArray::new(current_context);
+ let vertex_buffer = GlBuffer::new(current_context);
+
+ vertex_buffer
+ .store_mapped(
+ current_context,
+ mesh.vertices(),
+ GlBufferUsage::Static,
+ |vertex| RendererVertex {
+ pos: vertex.pos.into(),
+ texture_coords: vertex.texture_coords.into(),
+ normal: vertex.normal.into(),
+ },
+ )
+ .map_err(Error::StoreVerticesFailed)?;
+
+ vertex_arr.bind_vertex_buffer(current_context, 0, &vertex_buffer, 0);
+
+ let mut offset = 0u32;
+
+ for attrib in RendererVertex::attrs() {
+ vertex_arr.enable_attrib(current_context, attrib.index);
+
+ vertex_arr.set_attrib_format(
+ current_context,
+ attrib.index,
+ match attrib.component_type {
+ VertexAttributeComponentType::Float => GlVertexArrayDataType::Float,
+ },
+ false,
+ offset,
+ );
+
+ vertex_arr.set_attrib_vertex_buf_binding(current_context, attrib.index, 0);
+
+ offset += attrib.component_size * attrib.component_cnt as u32;
+ }
+
+ if let Some(indices) = mesh.indices() {
+ let index_buffer = GlBuffer::new(current_context);
+
+ index_buffer
+ .store(current_context, indices, GlBufferUsage::Static)
+ .map_err(Error::StoreIndicesFailed)?;
+
+ vertex_arr.bind_element_buffer(current_context, &index_buffer);
+
+ return Ok(Self {
+ _vertex_buffer: vertex_buffer,
+ index_buffer: Some(index_buffer),
+ element_cnt: indices
+ .len()
+ .try_into()
+ .expect("Mesh index count does not fit into a 32-bit unsigned int"),
+ vertex_arr,
+ });
+ }
+
+ Ok(Self {
+ _vertex_buffer: vertex_buffer,
+ index_buffer: None,
+ element_cnt: mesh
+ .vertices()
+ .len()
+ .try_into()
+ .expect("Mesh vertex count does not fit into a 32-bit unsigned int"),
+ vertex_arr,
+ })
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error
+{
+ #[error("Failed to store vertices in vertex buffer")]
+ StoreVerticesFailed(#[source] opengl_bindings::buffer::Error),
+
+ #[error("Failed to store indices in index buffer")]
+ StoreIndicesFailed(#[source] opengl_bindings::buffer::Error),
+}
diff --git a/engine/src/vertex.rs b/engine/src/renderer/opengl/vertex.rs
index 30640c4..5a1593e 100644
--- a/engine/src/vertex.rs
+++ b/engine/src/renderer/opengl/vertex.rs
@@ -1,24 +1,18 @@
-use std::mem::size_of;
+use safer_ffi::derive_ReprC;
-use crate::util::builder;
-use crate::vector::{Vec2, Vec3};
-
-builder! {
-#[builder(name = Builder, derives = (Debug, Default))]
-#[derive(Debug, Clone, Default, PartialEq)]
+#[derive(Debug, Clone)]
+#[derive_ReprC]
#[repr(C)]
-#[non_exhaustive]
pub struct Vertex
{
- pub pos: Vec3<f32>,
- pub texture_coords: Vec2<f32>,
- pub normal: Vec3<f32>,
-}
+ pub pos: opengl_bindings::data_types::Vec3<f32>,
+ pub texture_coords: opengl_bindings::data_types::Vec2<f32>,
+ pub normal: opengl_bindings::data_types::Vec3<f32>,
}
impl Vertex
{
- pub(crate) fn attrs() -> &'static [Attribute]
+ pub fn attrs() -> &'static [Attribute]
{
#[allow(clippy::cast_possible_truncation)]
&[
@@ -44,15 +38,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,
}
@@ -60,7 +56,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/texture.rs b/engine/src/texture.rs
index 16c1941..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
+ pub fn with_properties(
+ asset_handle: AssetHandle<Image>,
+ properties: Properties,
+ ) -> Self
{
- &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)
- {
- 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,25 +62,29 @@ impl Default for Properties
}
}
-/// Texture ID.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct Id
+impl Default for PropertiesBuilder
{
- id: u32,
+ fn default() -> Self
+ {
+ Properties::default().into()
+ }
}
-impl Id
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[non_exhaustive]
+pub enum Filtering
{
- fn new(id: u32) -> Self
- {
- Self { id }
- }
+ 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..9174734 100644
--- a/engine/src/util.rs
+++ b/engine/src/util.rs
@@ -1,3 +1,73 @@
+use ecs::util::VecExt;
+
+#[derive(Debug)]
+pub struct MapVec<Key: Ord, Value>
+{
+ inner: Vec<(Key, Value)>,
+}
+
+impl<Key: Ord, Value> MapVec<Key, Value>
+{
+ pub fn insert(&mut self, key: Key, value: Value)
+ {
+ self.inner
+ .insert_at_part_pt_by_key((key, value), |(a_key, _)| a_key);
+ }
+
+ pub fn remove(&mut self, key: Key) -> Option<Value>
+ {
+ let index = self
+ .inner
+ .binary_search_by_key(&&key, |(a_key, _)| a_key)
+ .ok()?;
+
+ let (_, value) = self.inner.remove(index);
+
+ Some(value)
+ }
+
+ pub fn get(&self, key: &Key) -> Option<&Value>
+ {
+ let index = self
+ .inner
+ .binary_search_by_key(&key, |(a_key, _)| a_key)
+ .ok()?;
+
+ let Some((_, value)) = self.inner.get(index) else {
+ unreachable!(); // Reason: Index from binary search cannot be OOB
+ };
+
+ Some(value)
+ }
+
+ pub fn get_mut(&mut self, key: &Key) -> Option<&mut Value>
+ {
+ let index = self
+ .inner
+ .binary_search_by_key(&key, |(a_key, _)| a_key)
+ .ok()?;
+
+ let Some((_, value)) = self.inner.get_mut(index) else {
+ unreachable!(); // Reason: Index from binary search cannot be OOB
+ };
+
+ Some(value)
+ }
+
+ pub fn values(&self) -> impl Iterator<Item = &Value>
+ {
+ self.inner.iter().map(|(_, value)| value)
+ }
+}
+
+impl<Key: Ord, Value> Default for MapVec<Key, Value>
+{
+ fn default() -> Self
+ {
+ Self { inner: Vec::new() }
+ }
+}
+
macro_rules! try_option {
($expr: expr) => {
match $expr {
@@ -9,9 +79,6 @@ macro_rules! try_option {
};
}
-use std::mem::ManuallyDrop;
-use std::ops::{Deref, DerefMut};
-
pub(crate) use try_option;
macro_rules! or {
@@ -26,6 +93,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 +116,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 +127,7 @@ macro_rules! builder {
$visibility struct $name
{
$(
- $(#[$field_attr])*
+ $(#[doc = $field_doc])*
$field_visibility $field: $field_type,
)*
}
@@ -63,12 +143,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 +169,7 @@ macro_rules! builder {
impl From<$name> for $builder_name
{
+ #[allow(unused_variables)]
fn from(built: $name) -> Self
{
Self {
@@ -94,39 +181,3 @@ macro_rules! builder {
}
};
}
-
-pub(crate) use builder;
-
-/// Wrapper that ensures the contained value will never be dropped.
-#[derive(Debug)]
-pub struct NeverDrop<Value>
-{
- value: ManuallyDrop<Value>,
-}
-
-impl<Value> NeverDrop<Value>
-{
- #[must_use]
- pub fn new(value: Value) -> Self
- {
- Self { value: ManuallyDrop::new(value) }
- }
-}
-
-impl<Value> Deref for NeverDrop<Value>
-{
- type Target = Value;
-
- fn deref(&self) -> &Self::Target
- {
- &self.value
- }
-}
-
-impl<Value> DerefMut for NeverDrop<Value>
-{
- fn deref_mut(&mut self) -> &mut Self::Target
- {
- &mut self.value
- }
-}
diff --git a/engine/src/window.rs b/engine/src/window.rs
deleted file mode 100644
index 00c360e..0000000
--- a/engine/src/window.rs
+++ /dev/null
@@ -1,752 +0,0 @@
-use std::borrow::Cow;
-use std::ffi::{CStr, CString};
-
-use bitflags::bitflags;
-use ecs::actions::Actions;
-use ecs::extension::Collector as ExtensionCollector;
-use ecs::phase::{Phase, PRESENT as PRESENT_PHASE, START as START_PHASE};
-use ecs::relationship::{ChildOf, Relationship};
-use ecs::sole::Single;
-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::vector::Vec2;
-
-static_entity!(
- pub UPDATE_PHASE,
- (Phase, <Relationship<ChildOf, Phase>>::new(*PRESENT_PHASE))
-);
-
-#[derive(Debug, Sole)]
-/// Has to be dropped last since it holds the OpenGL context.
-#[sole(drop_last)]
-pub struct Window
-{
- inner: glfw::Window,
-}
-
-impl Window
-{
- /// Returns a new Window builder.
- #[must_use]
- pub fn builder() -> Builder
- {
- Builder::default()
- }
-
- /// Sets the value of a input mode.
- ///
- /// # Errors
- /// Returns `Err` if the input mode is unsupported on the current system.
- pub fn set_input_mode(
- &self,
- input_mode: InputMode,
- enabled: bool,
- ) -> Result<(), Error>
- {
- Ok(self
- .inner
- .set_input_mode(input_mode.to_glfw_input_mode(), enabled)?)
- }
-
- /// Sets the cursor mode.
- ///
- /// # Errors
- /// If a platform error occurs.
- pub fn set_cursor_mode(&self, cursor_mode: CursorMode) -> Result<(), Error>
- {
- 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
- /// attempted to close the window.
- #[must_use]
- pub fn should_close(&self) -> bool
- {
- self.inner.should_close()
- }
-
- /// Processes all pending events.
- ///
- /// # Errors
- /// If a platform error occurs.
- pub fn poll_events(&self) -> Result<(), Error>
- {
- Ok(self.inner.poll_events()?)
- }
-
- /// Swaps the front and back buffers of the window.
- ///
- /// # Errors
- /// Will return `Err` if a platform error occurs or if no OpenGL window context
- /// is present.
- pub fn swap_buffers(&self) -> Result<(), Error>
- {
- Ok(self.inner.swap_buffers()?)
- }
-
- /// Returns the size of the window.
- ///
- /// # Errors
- /// Will return `Err` if a platform error occurs.
- pub fn size(&self) -> Result<Dimens<u32>, Error>
- {
- let size = self.inner.size()?;
-
- Ok(Dimens {
- width: size.width,
- height: size.height,
- })
- }
-
- /// Returns the address of the specified OpenGL function, if it is supported by the
- /// current OpenGL context.
- ///
- /// # Errors
- /// Will return `Err` if a platform error occurs or if no current context has
- /// been set.
- ///
- /// # Panics
- /// Will panic if the `proc_name` argument contains a nul byte.
- pub fn get_proc_address(
- &self,
- proc_name: &str,
- ) -> Result<unsafe extern "C" fn(), Error>
- {
- let proc_name_c: Cow<CStr> = CStr::from_bytes_with_nul(proc_name.as_bytes())
- .map(Cow::Borrowed)
- .or_else(|_| CString::new(proc_name).map(Cow::Owned))
- .expect("OpenGL function name contains a nul byte");
-
- Ok(self.inner.get_proc_address(&proc_name_c)?)
- }
-
- /// Makes the OpenGL context of the window current for the calling thread.
- ///
- /// # Errors
- /// Will return `Err` if a platform error occurs or if no OpenGL context is
- /// present.
- pub fn make_context_current(&self) -> Result<(), Error>
- {
- Ok(self.inner.make_context_current()?)
- }
-
- /// Sets the window's framebuffer size callback.
- pub fn set_framebuffer_size_callback(&self, callback: impl Fn(Dimens<u32>) + 'static)
- {
- self.inner.set_framebuffer_size_callback(move |size| {
- callback(Dimens {
- width: size.width,
- height: size.height,
- });
- });
- }
-
- /// Sets the window's key size callback.
- pub fn set_key_callback(
- &self,
- callback: impl Fn(Key, i32, KeyState, KeyModifiers) + 'static,
- )
- {
- 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.
- pub fn set_cursor_pos_callback(&self, callback: impl Fn(Vec2<f64>) + 'static)
- {
- self.inner
- .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)
- {
- self.inner.set_close_callback(callback);
- }
-
- /// Sets the window's focus callback. The callback is called when the window loses or
- /// gains input focus.
- pub fn set_focus_callback(&self, callback: impl Fn(bool) + 'static)
- {
- self.inner.set_focus_callback(callback);
- }
-}
-
-/// [`Window`] builder.
-#[derive(Debug, Clone, Default)]
-pub struct Builder
-{
- inner: glfw::WindowBuilder,
-}
-
-impl Builder
-{
- /// 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(
- WindowCreationHint::Samples,
- WindowCreationHintValue::Number(sample_count as i32),
- );
-
- self
- }
-
- /// Creates a new window.
- ///
- /// # Errors
- /// Will return `Err` if the title contains a internal nul byte or if a platform error
- /// occurs.
- pub fn create(&self, size: Dimens<u32>, title: &str) -> Result<Window, Error>
- {
- let builder = self.inner.clone().hint(
- WindowCreationHint::OpenGLDebugContext,
- WindowCreationHintValue::Bool(true),
- );
-
- let window = builder.create(
- &WindowSize {
- width: size.width,
- height: size.height,
- },
- title,
- )?;
-
- Ok(Window { inner: window })
- }
-}
-
-#[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
-{
- window_builder: Builder,
- window_size: Dimens<u32>,
- window_title: String,
-}
-
-impl Extension
-{
- #[must_use]
- pub fn new(window_builder: Builder) -> Self
- {
- Self { window_builder, ..Default::default() }
- }
-
- #[must_use]
- pub fn window_size(mut self, window_size: Dimens<u32>) -> Self
- {
- self.window_size = window_size;
-
- self
- }
-
- #[must_use]
- pub fn window_title(mut self, window_title: impl Into<String>) -> Self
- {
- self.window_title = window_title.into();
-
- self
- }
-}
-
-impl ecs::extension::Extension for Extension
-{
- fn collect(self, mut collector: ExtensionCollector<'_>)
- {
- collector.add_system(*START_PHASE, initialize);
- collector.add_system(*UPDATE_PHASE, update);
-
- let window = self
- .window_builder
- .create(self.window_size, &self.window_title)
- .unwrap();
-
- window.set_cursor_mode(CursorMode::Normal).unwrap();
-
- collector.add_sole(window).ok();
- }
-}
-
-impl Default for Extension
-{
- fn default() -> Self
- {
- Self {
- window_builder: Builder::default(),
- window_size: Dimens { width: 1920, height: 1080 },
- window_title: String::new(),
- }
- }
-}
-
-#[derive(Debug, thiserror::Error)]
-#[error(transparent)]
-pub struct Error(glfw::Error);
-
-impl From<glfw::Error> for Error
-{
- fn from(err: glfw::Error) -> Self
- {
- Self(err)
- }
-}
-
-fn initialize(window: Single<Window>, actions: Actions)
-{
- let actions_weak_ref = actions.to_weak_ref();
-
- window.set_close_callback(move || {
- let actions_weak_ref = actions_weak_ref.clone();
-
- let actions_ref = actions_weak_ref.access().expect("No world");
-
- actions_ref.to_actions().stop();
- });
-}
-
-fn update(window: Single<Window>)
-{
- window
- .swap_buffers()
- .expect("Failed to swap window buffers");
-
- window.poll_events().expect("Failed to poll window events");
-}
diff --git a/engine/src/windowing.rs b/engine/src/windowing.rs
new file mode 100644
index 0000000..69adae9
--- /dev/null
+++ b/engine/src/windowing.rs
@@ -0,0 +1,669 @@
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::{Arc, Weak};
+use std::thread::{spawn, JoinHandle as ThreadJoinHandle};
+
+use crossbeam_channel::{
+ bounded as bounded_channel,
+ Receiver as ChannelReceiver,
+ Sender as ChannelSender,
+ TrySendError,
+};
+use ecs::actions::Actions;
+use ecs::component::Component;
+use ecs::entity::obtainer::Obtainer as EntityObtainer;
+use ecs::event::component::{Added, Changed, Removed};
+use ecs::pair::{ChildOf, Pair};
+use ecs::phase::{Phase, UPDATE as UPDATE_PHASE};
+use ecs::sole::Single;
+use ecs::system::observer::Observe;
+use ecs::uid::Uid;
+use ecs::{declare_entity, Query, Sole};
+use raw_window_handle::{DisplayHandle, HandleError, HasDisplayHandle, WindowHandle};
+use winit::application::ApplicationHandler;
+use winit::dpi::PhysicalPosition;
+use winit::error::EventLoopError;
+use winit::event::{DeviceEvent, DeviceId, StartCause, WindowEvent};
+use winit::event_loop::{
+ ActiveEventLoop,
+ ControlFlow as EventLoopControlFlow,
+ EventLoop,
+ OwnedDisplayHandle,
+};
+use winit::keyboard::PhysicalKey;
+use winit::window::{Window as WinitWindow, WindowId as WinitWindowId};
+
+use crate::data_types::dimens::Dimens;
+use crate::util::MapVec;
+use crate::vector::Vec2;
+use crate::windowing::keyboard::{Key, KeyState, Keyboard, UnknownKeyCodeError};
+use crate::windowing::mouse::{
+ Button as MouseButton,
+ ButtonState as MouseButtonState,
+ Buttons as MouseButtons,
+ Motion as MouseMotion,
+};
+use crate::windowing::window::{
+ Closed as WindowClosed,
+ CreationAttributes as WindowCreationAttributes,
+ CreationReady as WindowCreationReady,
+ CursorGrabMode,
+ Id as WindowId,
+ Window,
+};
+
+pub mod keyboard;
+pub mod mouse;
+pub mod window;
+
+const MESSAGE_FROM_APP_CHANNEL_CAP: usize = 128;
+
+const MESSAGE_TO_APP_CHANNEL_CAP: usize = 16; // Increase if more messages are added
+
+declare_entity!(
+ pub PHASE,
+ (
+ Phase,
+ Pair::builder()
+ .relation::<ChildOf>()
+ .target_id(*UPDATE_PHASE)
+ .build()
+ )
+);
+
+#[derive(Debug, Default)]
+#[non_exhaustive]
+pub struct Extension {}
+
+impl ecs::extension::Extension for Extension
+{
+ fn collect(self, mut collector: ecs::extension::Collector<'_>)
+ {
+ collector.add_sole(Context::default()).ok();
+ collector.add_sole(Keyboard::default()).ok();
+ collector.add_sole(MouseMotion::default()).ok();
+ collector.add_sole(MouseButtons::default()).ok();
+
+ collector.add_declared_entity(&PHASE);
+
+ collector.add_system(*PHASE, update_stuff);
+
+ collector.add_observer(handle_window_changed);
+ collector.add_observer(handle_window_removed);
+ collector.add_observer(handle_window_creation_ready);
+ }
+}
+
+fn handle_window_creation_ready(
+ observe: Observe<Pair<Added, WindowCreationReady>>,
+ context: Single<Context>,
+)
+{
+ for evt_match in &observe {
+ let Some(ent) = evt_match.get_entity() else {
+ unreachable!();
+ };
+
+ if ent.has_component(Window::id()) || ent.has_component(WindowClosed::id()) {
+ continue;
+ }
+
+ let Some(window_creation_attrs) = ent.get::<WindowCreationAttributes>() else {
+ unreachable!();
+ };
+
+ context.try_send_message_to_app(MessageToApp::CreateWindow(
+ ent.uid(),
+ window_creation_attrs.clone(),
+ ));
+ }
+}
+
+#[tracing::instrument(skip_all)]
+fn update_stuff(
+ mut context: Single<Context>,
+ mut keyboard: Single<Keyboard>,
+ mut mouse_motion: Single<MouseMotion>,
+ mut mouse_buttons: Single<MouseButtons>,
+ mut actions: Actions,
+ entity_obtainer: EntityObtainer,
+)
+{
+ keyboard.make_key_states_previous();
+ mouse_buttons.make_states_previous();
+ mouse_motion.position_delta = Vec2::default();
+
+ let Context {
+ ref message_from_app_receiver,
+ ref mut display_handle,
+ ref mut windows,
+ ..
+ } = *context;
+
+ for message in message_from_app_receiver.try_iter() {
+ match message {
+ MessageFromApp::Init(new_display_handle) => {
+ *display_handle = Some(new_display_handle);
+ }
+ MessageFromApp::WindowCreated(
+ window_ent_id,
+ winit_window,
+ window_creation_attrs,
+ ) => {
+ actions.add_components(
+ window_ent_id,
+ (Window::new(&winit_window, &window_creation_attrs),),
+ );
+
+ actions.remove_comps::<(WindowCreationReady,)>(window_ent_id);
+
+ tracing::debug!("Added window component to window entity");
+
+ windows.insert(
+ WindowId::from_inner(winit_window.id()),
+ (winit_window, window_ent_id),
+ );
+ }
+ MessageFromApp::WindowResized(window_id, new_window_size) => {
+ let Some(window_ent_id) =
+ windows.get(&window_id).map(|(_, ent_id)| ent_id)
+ else {
+ continue;
+ };
+
+ let Some(window_ent) = entity_obtainer.get_entity(*window_ent_id) else {
+ continue;
+ };
+
+ let Some(mut window) = window_ent.get_mut::<Window>() else {
+ continue;
+ };
+
+ window.set_inner_size(new_window_size);
+
+ window.set_changed();
+ }
+ MessageFromApp::WindowCloseRequested(window_id) => {
+ let Some(window_ent_id) =
+ windows.get(&window_id).map(|(_, ent_id)| ent_id)
+ else {
+ tracing::error!(
+ wid = ?window_id,
+ "Window does not exist in windowing context"
+ );
+ continue;
+ };
+
+ actions.remove_comps::<(Window,)>(*window_ent_id);
+ }
+ MessageFromApp::KeyboardKeyStateChanged(key, key_state) => {
+ keyboard.set_key_state(key, key_state);
+ }
+ MessageFromApp::MouseMoved { position_delta } => {
+ mouse_motion.position_delta += position_delta;
+ }
+ MessageFromApp::MouseButtonStateChanged(mouse_button, mouse_button_state) => {
+ mouse_buttons.set(mouse_button, mouse_button_state);
+ }
+ }
+ }
+}
+
+fn handle_window_changed(
+ observe: Observe<'_, Pair<Changed, Window>>,
+ context: Single<Context>,
+)
+{
+ for evt_match in &observe {
+ let window_ent_id = evt_match.id();
+
+ let window = evt_match.get_changed_comp();
+
+ let Some((winit_window, _)) = context.windows.get(&window.wid()) else {
+ tracing::error!(
+ wid = ?window.wid(),
+ entity_id = %window_ent_id,
+ "Window does not exist in windowing context",
+ );
+ continue;
+ };
+
+ window.apply(winit_window);
+
+ context.try_send_message_to_app(MessageToApp::SetWindowCursorGrabMode(
+ window.wid(),
+ window.cursor_grab_mode,
+ ));
+ }
+}
+
+fn handle_window_removed(
+ observe: Observe<Pair<Removed, Window>>,
+ window_query: Query<(&Window,)>,
+ mut context: Single<Context>,
+ mut actions: Actions,
+)
+{
+ for evt_match in &observe {
+ let window = evt_match.get_removed_comp();
+
+ context.windows.remove(window.wid());
+
+ actions.add_components(evt_match.id(), (WindowClosed,));
+ }
+
+ if window_query.iter().count() == 1 {
+ actions.stop();
+ }
+}
+
+#[derive(Debug, Sole)]
+pub struct Context
+{
+ _thread: ThreadJoinHandle<()>,
+ is_dropped: Arc<AtomicBool>,
+ message_from_app_receiver: ChannelReceiver<MessageFromApp>,
+ message_to_app_sender: ChannelSender<MessageToApp>,
+ display_handle: Option<OwnedDisplayHandle>,
+ windows: MapVec<WindowId, (Arc<winit::window::Window>, Uid)>,
+}
+
+impl Context
+{
+ pub fn display_handle(&self) -> Option<DisplayHandle<'_>>
+ {
+ let display_handle = self.display_handle.as_ref()?;
+
+ display_handle.display_handle().ok()
+ }
+
+ /// Returns the specified window as a window handle, if it exists.
+ ///
+ /// # Safety
+ /// The Window handle must only be used with thread safe APIs.
+ pub unsafe fn get_window_as_handle(
+ &self,
+ window_id: &WindowId,
+ ) -> Option<Result<WindowHandle<'_>, HandleError>>
+ {
+ self.windows.get(window_id).map(|(winit_window, _)| {
+ #[cfg(windows)]
+ {
+ use winit::platform::windows::WindowExtWindows;
+
+ // SAFETY: I don't care
+ unsafe { winit_window.window_handle_any_thread() }
+ }
+
+ #[cfg(not(windows))]
+ {
+ use raw_window_handle::HasWindowHandle;
+
+ winit_window.window_handle()
+ }
+ })
+ }
+
+ fn try_send_message_to_app(&self, message: MessageToApp)
+ {
+ if let Err(err) = self.message_to_app_sender.try_send(message) {
+ let error = match &err {
+ TrySendError::Full(_) => TrySendError::Full(()),
+ TrySendError::Disconnected(_) => TrySendError::Disconnected(()),
+ };
+
+ let message = err.into_inner();
+
+ tracing::error!("Failed to send message {error}: {message:?}");
+ }
+ }
+}
+
+impl Default for Context
+{
+ fn default() -> Self
+ {
+ let is_dropped = Arc::new(AtomicBool::new(false));
+
+ let is_dropped_b = is_dropped.clone();
+
+ let (message_from_app_sender, message_from_app_receiver) =
+ bounded_channel::<MessageFromApp>(MESSAGE_FROM_APP_CHANNEL_CAP);
+
+ let message_from_app_receiver_b = message_from_app_receiver.clone();
+
+ let (message_to_app_sender, message_to_app_receiver) =
+ bounded_channel::<MessageToApp>(MESSAGE_TO_APP_CHANNEL_CAP);
+
+ Self {
+ _thread: spawn(move || {
+ let mut app = App {
+ message_from_app_sender,
+ message_from_app_receiver: message_from_app_receiver_b,
+ message_to_app_receiver,
+ is_dropped: is_dropped_b,
+ windows: MapVec::default(),
+ focused_window_id: None,
+ };
+
+ let event_loop = match create_event_loop() {
+ Ok(event_loop) => event_loop,
+ Err(err) => {
+ tracing::error!("Failed to create event loop: {err}");
+ return;
+ }
+ };
+
+ event_loop.set_control_flow(EventLoopControlFlow::Poll);
+
+ if let Err(err) = event_loop.run_app(&mut app) {
+ tracing::error!("Event loop error occurred: {err}");
+ }
+ }),
+ is_dropped,
+ message_from_app_receiver,
+ message_to_app_sender,
+ display_handle: None,
+ windows: MapVec::default(),
+ }
+ }
+}
+
+impl Drop for Context
+{
+ fn drop(&mut self)
+ {
+ self.is_dropped.store(true, Ordering::Relaxed);
+ }
+}
+
+fn create_event_loop() -> Result<EventLoop<()>, EventLoopError>
+{
+ let mut event_loop_builder = EventLoop::builder();
+
+ #[cfg(any(x11_platform, wayland_platform))]
+ winit::platform::x11::EventLoopBuilderExtX11::with_any_thread(
+ &mut event_loop_builder,
+ true,
+ );
+
+ #[cfg(windows)]
+ winit::platform::windows::EventLoopBuilderExtWindows::with_any_thread(
+ &mut event_loop_builder,
+ true,
+ );
+
+ #[cfg(not(any(x11_platform, wayland_platform, windows)))]
+ compile_error!("Unsupported platform");
+
+ event_loop_builder.build()
+}
+
+#[derive(Debug)]
+enum MessageFromApp
+{
+ Init(OwnedDisplayHandle),
+ WindowCreated(Uid, Arc<WinitWindow>, WindowCreationAttributes),
+ WindowResized(WindowId, Dimens<u32>),
+ WindowCloseRequested(WindowId),
+ KeyboardKeyStateChanged(Key, KeyState),
+ MouseMoved
+ {
+ position_delta: Vec2<f64>,
+ },
+ MouseButtonStateChanged(MouseButton, MouseButtonState),
+}
+
+#[derive(Debug)]
+enum MessageToApp
+{
+ CreateWindow(Uid, WindowCreationAttributes),
+ SetWindowCursorGrabMode(WindowId, CursorGrabMode),
+}
+
+#[derive(Debug)]
+struct App
+{
+ message_from_app_sender: ChannelSender<MessageFromApp>,
+ message_from_app_receiver: ChannelReceiver<MessageFromApp>,
+ message_to_app_receiver: ChannelReceiver<MessageToApp>,
+ is_dropped: Arc<AtomicBool>,
+ windows: MapVec<WindowId, (Weak<WinitWindow>, WindowSettings)>,
+ focused_window_id: Option<WindowId>,
+}
+
+impl App
+{
+ fn handle_received_messages(&mut self, event_loop: &ActiveEventLoop)
+ {
+ for message in self.message_to_app_receiver.try_iter() {
+ match message {
+ MessageToApp::CreateWindow(window_ent_id, window_creation_attrs) => {
+ tracing::info!(
+ "Creating window with title {}",
+ window_creation_attrs.title()
+ );
+
+ let winit_window = Arc::new(
+ match event_loop
+ .create_window(window_creation_attrs.clone().into_inner())
+ {
+ Ok(window) => window,
+ Err(err) => {
+ tracing::error!("Failed to create window: {err}");
+ continue;
+ }
+ },
+ );
+
+ tracing::info!("Created window has title {}", winit_window.title());
+
+ self.windows.insert(
+ WindowId::from_inner(winit_window.id()),
+ (Arc::downgrade(&winit_window), WindowSettings::default()),
+ );
+
+ self.send_message(MessageFromApp::WindowCreated(
+ window_ent_id,
+ winit_window,
+ window_creation_attrs,
+ ));
+ }
+ MessageToApp::SetWindowCursorGrabMode(window_id, cursor_grab_mode) => {
+ let Some((_, window_settings)) = self.windows.get_mut(&window_id)
+ else {
+ tracing::warn!(
+ window_id=?window_id,
+ "Cannot set window cursor grab mode. Window not found"
+ );
+
+ continue;
+ };
+
+ window_settings.cursor_grab_mode = cursor_grab_mode;
+ }
+ }
+ }
+ }
+
+ fn send_message(&self, message: MessageFromApp)
+ {
+ if self.message_from_app_sender.is_full() {
+ tracing::warn!(
+ "Message channel is full! Dropping oldest message from channel"
+ );
+
+ self.message_from_app_receiver.try_recv().ok();
+ }
+
+ if let Err(err) = self.message_from_app_sender.try_send(message) {
+ let error = match &err {
+ TrySendError::Full(_) => TrySendError::Full(()),
+ TrySendError::Disconnected(_) => TrySendError::Disconnected(()),
+ };
+
+ let message = err.into_inner();
+
+ tracing::error!("Failed to send message {error}: {message:?}");
+ }
+ }
+}
+
+impl ApplicationHandler for App
+{
+ fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause)
+ {
+ match cause {
+ StartCause::Init => {
+ self.send_message(MessageFromApp::Init(
+ event_loop.owned_display_handle(),
+ ));
+ }
+ StartCause::Poll => {
+ if self.is_dropped.load(Ordering::Relaxed) {
+ event_loop.exit();
+ return;
+ }
+
+ self.handle_received_messages(event_loop);
+ }
+ _ => {}
+ }
+ }
+
+ fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop)
+ {
+ for (window, _) in self.windows.values() {
+ let Some(window) = window.upgrade() else {
+ continue;
+ };
+
+ window.request_redraw();
+ }
+ }
+
+ fn resumed(&mut self, _event_loop: &ActiveEventLoop) {}
+
+ fn window_event(
+ &mut self,
+ _event_loop: &ActiveEventLoop,
+ window_id: WinitWindowId,
+ event: WindowEvent,
+ )
+ {
+ match event {
+ WindowEvent::Resized(new_window_size) => {
+ self.send_message(MessageFromApp::WindowResized(
+ WindowId::from_inner(window_id),
+ new_window_size.into(),
+ ));
+ }
+ WindowEvent::CloseRequested => {
+ self.send_message(MessageFromApp::WindowCloseRequested(
+ WindowId::from_inner(window_id),
+ ));
+ }
+ WindowEvent::KeyboardInput {
+ device_id: _,
+ event: keyboard_event,
+ is_synthetic: _,
+ } => {
+ if keyboard_event.repeat {
+ return;
+ }
+
+ let key_code = match keyboard_event.physical_key {
+ PhysicalKey::Code(key_code) => key_code,
+ PhysicalKey::Unidentified(native_key) => {
+ tracing::warn!("Ignoring unidentified key: {native_key:?}");
+ return;
+ }
+ };
+
+ let key: Key = match key_code.try_into() {
+ Ok(key) => key,
+ Err(UnknownKeyCodeError) => {
+ tracing::warn!("Ignoring key with unknown key code {key_code:?}");
+ return;
+ }
+ };
+
+ self.send_message(MessageFromApp::KeyboardKeyStateChanged(
+ key,
+ keyboard_event.state.into(),
+ ));
+ }
+ WindowEvent::MouseInput { device_id: _, state, button } => {
+ self.send_message(MessageFromApp::MouseButtonStateChanged(
+ button.into(),
+ state.into(),
+ ));
+ }
+ WindowEvent::Focused(is_focused) => {
+ if is_focused {
+ self.focused_window_id = Some(WindowId::from_inner(window_id));
+ } else {
+ self.focused_window_id = None;
+ }
+ }
+ _ => {}
+ }
+ }
+
+ fn device_event(
+ &mut self,
+ _event_loop: &ActiveEventLoop,
+ _device_id: DeviceId,
+ device_event: DeviceEvent,
+ )
+ {
+ match device_event {
+ DeviceEvent::MouseMotion { delta } => {
+ self.send_message(MessageFromApp::MouseMoved {
+ position_delta: Vec2 { x: delta.0, y: delta.1 },
+ });
+
+ let Some(focused_window_id) = self.focused_window_id else {
+ return;
+ };
+
+ let Some((focused_window, focused_window_settings)) =
+ self.windows.get(&focused_window_id)
+ else {
+ tracing::error!(
+ window_id=?focused_window_id,
+ "Focused window not found"
+ );
+ return;
+ };
+
+ if focused_window_settings.cursor_grab_mode != CursorGrabMode::Locked {
+ return;
+ }
+
+ // TODO: This might need to be optimized
+ let Some(focused_window) = focused_window.upgrade() else {
+ return;
+ };
+
+ let focused_window_size = focused_window.inner_size();
+
+ if let Err(err) = focused_window.set_cursor_position(PhysicalPosition {
+ x: focused_window_size.width / 2,
+ y: focused_window_size.height / 2,
+ }) {
+ tracing::error!(
+ window_id=?focused_window_id,
+ "Failed to set cursor position in focused window: {err}"
+ );
+ };
+ }
+ _ => {}
+ }
+ }
+}
+
+#[derive(Debug, Default)]
+struct WindowSettings
+{
+ cursor_grab_mode: CursorGrabMode,
+}
diff --git a/engine/src/windowing/keyboard.rs b/engine/src/windowing/keyboard.rs
new file mode 100644
index 0000000..e4fffe5
--- /dev/null
+++ b/engine/src/windowing/keyboard.rs
@@ -0,0 +1,763 @@
+use std::collections::HashMap;
+
+use ecs::Sole;
+
+#[derive(Debug, Default, Sole)]
+pub struct Keyboard
+{
+ map: HashMap<Key, KeyData>,
+}
+
+impl Keyboard
+{
+ #[must_use]
+ pub fn get_key_state(&self, key: Key) -> KeyState
+ {
+ let Some(key_data) = self.map.get(&key) else {
+ return KeyState::Released;
+ };
+
+ key_data.curr_state
+ }
+
+ #[must_use]
+ pub fn get_prev_key_state(&self, key: Key) -> KeyState
+ {
+ let Some(key_data) = self.map.get(&key) else {
+ return KeyState::Released;
+ };
+
+ key_data.previous_state
+ }
+
+ pub fn set_key_state(&mut self, key: Key, key_state: KeyState)
+ {
+ let key_data = self.map.entry(key).or_default();
+
+ key_data.curr_state = key_state;
+ }
+
+ pub fn make_key_states_previous(&mut self)
+ {
+ for key_data in self.map.values_mut() {
+ key_data.previous_state = key_data.curr_state;
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[non_exhaustive]
+pub enum Key
+{
+ /// <kbd>`</kbd> on a US keyboard. This is also called a backtick or grave.
+ /// This is the <kbd>半角</kbd>/<kbd>全角</kbd>/<kbd>漢字</kbd>
+ /// (hankaku/zenkaku/kanji) key on Japanese keyboards
+ Backquote,
+ /// Used for both the US <kbd>\\</kbd> (on the 101-key layout) and also for the key
+ /// located between the <kbd>"</kbd> and <kbd>Enter</kbd> keys on row C of the 102-,
+ /// 104- and 106-key layouts.
+ /// Labeled <kbd>#</kbd> on a UK (102) keyboard.
+ Backslash,
+ /// <kbd>[</kbd> on a US keyboard.
+ BracketLeft,
+ /// <kbd>]</kbd> on a US keyboard.
+ BracketRight,
+ /// <kbd>,</kbd> on a US keyboard.
+ Comma,
+ /// <kbd>0</kbd> on a US keyboard.
+ Digit0,
+ /// <kbd>1</kbd> on a US keyboard.
+ Digit1,
+ /// <kbd>2</kbd> on a US keyboard.
+ Digit2,
+ /// <kbd>3</kbd> on a US keyboard.
+ Digit3,
+ /// <kbd>4</kbd> on a US keyboard.
+ Digit4,
+ /// <kbd>5</kbd> on a US keyboard.
+ Digit5,
+ /// <kbd>6</kbd> on a US keyboard.
+ Digit6,
+ /// <kbd>7</kbd> on a US keyboard.
+ Digit7,
+ /// <kbd>8</kbd> on a US keyboard.
+ Digit8,
+ /// <kbd>9</kbd> on a US keyboard.
+ Digit9,
+ /// <kbd>=</kbd> on a US keyboard.
+ Equal,
+ /// Located between the left <kbd>Shift</kbd> and <kbd>Z</kbd> keys.
+ /// Labeled <kbd>\\</kbd> on a UK keyboard.
+ IntlBackslash,
+ /// Located between the <kbd>/</kbd> and right <kbd>Shift</kbd> keys.
+ /// Labeled <kbd>\\</kbd> (ro) on a Japanese keyboard.
+ IntlRo,
+ /// Located between the <kbd>=</kbd> and <kbd>Backspace</kbd> keys.
+ /// Labeled <kbd>¥</kbd> (yen) on a Japanese keyboard. <kbd>\\</kbd> on a
+ /// Russian keyboard.
+ IntlYen,
+ /// <kbd>a</kbd> on a US keyboard.
+ /// Labeled <kbd>q</kbd> on an AZERTY (e.g., French) keyboard.
+ A,
+ /// <kbd>b</kbd> on a US keyboard.
+ B,
+ /// <kbd>c</kbd> on a US keyboard.
+ C,
+ /// <kbd>d</kbd> on a US keyboard.
+ D,
+ /// <kbd>e</kbd> on a US keyboard.
+ E,
+ /// <kbd>f</kbd> on a US keyboard.
+ F,
+ /// <kbd>g</kbd> on a US keyboard.
+ G,
+ /// <kbd>h</kbd> on a US keyboard.
+ H,
+ /// <kbd>i</kbd> on a US keyboard.
+ I,
+ /// <kbd>j</kbd> on a US keyboard.
+ J,
+ /// <kbd>k</kbd> on a US keyboard.
+ K,
+ /// <kbd>l</kbd> on a US keyboard.
+ L,
+ /// <kbd>m</kbd> on a US keyboard.
+ M,
+ /// <kbd>n</kbd> on a US keyboard.
+ N,
+ /// <kbd>o</kbd> on a US keyboard.
+ O,
+ /// <kbd>p</kbd> on a US keyboard.
+ P,
+ /// <kbd>q</kbd> on a US keyboard.
+ /// Labeled <kbd>a</kbd> on an AZERTY (e.g., French) keyboard.
+ Q,
+ /// <kbd>r</kbd> on a US keyboard.
+ R,
+ /// <kbd>s</kbd> on a US keyboard.
+ S,
+ /// <kbd>t</kbd> on a US keyboard.
+ T,
+ /// <kbd>u</kbd> on a US keyboard.
+ U,
+ /// <kbd>v</kbd> on a US keyboard.
+ V,
+ /// <kbd>w</kbd> on a US keyboard.
+ /// Labeled <kbd>z</kbd> on an AZERTY (e.g., French) keyboard.
+ W,
+ /// <kbd>x</kbd> on a US keyboard.
+ X,
+ /// <kbd>y</kbd> on a US keyboard.
+ /// Labeled <kbd>z</kbd> on a QWERTZ (e.g., German) keyboard.
+ Y,
+ /// <kbd>z</kbd> on a US keyboard.
+ /// Labeled <kbd>w</kbd> on an AZERTY (e.g., French) keyboard, and <kbd>y</kbd> on a
+ /// QWERTZ (e.g., German) keyboard.
+ Z,
+ /// <kbd>-</kbd> on a US keyboard.
+ Minus,
+ /// <kbd>.</kbd> on a US keyboard.
+ Period,
+ /// <kbd>'</kbd> on a US keyboard.
+ Quote,
+ /// <kbd>;</kbd> on a US keyboard.
+ Semicolon,
+ /// <kbd>/</kbd> on a US keyboard.
+ Slash,
+ /// <kbd>Alt</kbd>, <kbd>Option</kbd>, or <kbd>⌥</kbd>.
+ AltLeft,
+ /// <kbd>Alt</kbd>, <kbd>Option</kbd>, or <kbd>⌥</kbd>.
+ /// This is labeled <kbd>AltGr</kbd> on many keyboard layouts.
+ AltRight,
+ /// <kbd>Backspace</kbd> or <kbd>⌫</kbd>.
+ /// Labeled <kbd>Delete</kbd> on Apple keyboards.
+ Backspace,
+ /// <kbd>CapsLock</kbd> or <kbd>⇪</kbd>
+ CapsLock,
+ /// The application context menu key, which is typically found between the right
+ /// <kbd>Super</kbd> key and the right <kbd>Control</kbd> key.
+ ContextMenu,
+ /// <kbd>Control</kbd> or <kbd>⌃</kbd>
+ ControlLeft,
+ /// <kbd>Control</kbd> or <kbd>⌃</kbd>
+ ControlRight,
+ /// <kbd>Enter</kbd> or <kbd>↵</kbd>. Labeled <kbd>Return</kbd> on Apple keyboards.
+ Enter,
+ /// The Windows, <kbd>⌘</kbd>, <kbd>Command</kbd>, or other OS symbol key.
+ SuperLeft,
+ /// The Windows, <kbd>⌘</kbd>, <kbd>Command</kbd>, or other OS symbol key.
+ SuperRight,
+ /// <kbd>Shift</kbd> or <kbd>⇧</kbd>
+ ShiftLeft,
+ /// <kbd>Shift</kbd> or <kbd>⇧</kbd>
+ ShiftRight,
+ /// <kbd> </kbd> (space)
+ Space,
+ /// <kbd>Tab</kbd> or <kbd>⇥</kbd>
+ Tab,
+ /// Japanese: <kbd>変</kbd> (henkan)
+ Convert,
+ /// Japanese: <kbd>カタカナ</kbd>/<kbd>ひらがな</kbd>/<kbd>ローマ字</kbd>
+ /// (katakana/hiragana/romaji)
+ KanaMode,
+ /// Korean: HangulMode <kbd>한/영</kbd> (han/yeong)
+ ///
+ /// Japanese (Mac keyboard): <kbd>か</kbd> (kana)
+ Lang1,
+ /// Korean: Hanja <kbd>한</kbd> (hanja)
+ ///
+ /// Japanese (Mac keyboard): <kbd>英</kbd> (eisu)
+ Lang2,
+ /// Japanese (word-processing keyboard): Katakana
+ Lang3,
+ /// Japanese (word-processing keyboard): Hiragana
+ Lang4,
+ /// Japanese (word-processing keyboard): Zenkaku/Hankaku
+ Lang5,
+ /// Japanese: <kbd>無変換</kbd> (muhenkan)
+ NonConvert,
+ /// <kbd>⌦</kbd>. The forward delete key.
+ /// Note that on Apple keyboards, the key labelled <kbd>Delete</kbd> on the main part
+ /// of the keyboard is encoded as [`Backspace`].
+ ///
+ /// [`Backspace`]: Self::Backspace
+ Delete,
+ /// <kbd>Page Down</kbd>, <kbd>End</kbd>, or <kbd>↘</kbd>
+ End,
+ /// <kbd>Help</kbd>. Not present on standard PC keyboards.
+ Help,
+ /// <kbd>Home</kbd> or <kbd>↖</kbd>
+ Home,
+ /// <kbd>Insert</kbd> or <kbd>Ins</kbd>. Not present on Apple keyboards.
+ Insert,
+ /// <kbd>Page Down</kbd>, <kbd>PgDn</kbd>, or <kbd>⇟</kbd>
+ PageDown,
+ /// <kbd>Page Up</kbd>, <kbd>PgUp</kbd>, or <kbd>⇞</kbd>
+ PageUp,
+ /// <kbd>↓</kbd>
+ ArrowDown,
+ /// <kbd>←</kbd>
+ ArrowLeft,
+ /// <kbd>→</kbd>
+ ArrowRight,
+ /// <kbd>↑</kbd>
+ ArrowUp,
+ /// On the Mac, this is used for the numpad <kbd>Clear</kbd> key.
+ NumLock,
+ /// <kbd>0 Ins</kbd> on a keyboard. <kbd>0</kbd> on a phone or remote control
+ Numpad0,
+ /// <kbd>1 End</kbd> on a keyboard. <kbd>1</kbd> or <kbd>1 QZ</kbd> on a phone or
+ /// remote control
+ Numpad1,
+ /// <kbd>2 ↓</kbd> on a keyboard. <kbd>2 ABC</kbd> on a phone or remote control
+ Numpad2,
+ /// <kbd>3 PgDn</kbd> on a keyboard. <kbd>3 DEF</kbd> on a phone or remote control
+ Numpad3,
+ /// <kbd>4 ←</kbd> on a keyboard. <kbd>4 GHI</kbd> on a phone or remote control
+ Numpad4,
+ /// <kbd>5</kbd> on a keyboard. <kbd>5 JKL</kbd> on a phone or remote control
+ Numpad5,
+ /// <kbd>6 →</kbd> on a keyboard. <kbd>6 MNO</kbd> on a phone or remote control
+ Numpad6,
+ /// <kbd>7 Home</kbd> on a keyboard. <kbd>7 PQRS</kbd> or <kbd>7 PRS</kbd> on a phone
+ /// or remote control
+ Numpad7,
+ /// <kbd>8 ↑</kbd> on a keyboard. <kbd>8 TUV</kbd> on a phone or remote control
+ Numpad8,
+ /// <kbd>9 PgUp</kbd> on a keyboard. <kbd>9 WXYZ</kbd> or <kbd>9 WXY</kbd> on a phone
+ /// or remote control
+ Numpad9,
+ /// <kbd>+</kbd>
+ NumpadAdd,
+ /// Found on the Microsoft Natural Keyboard.
+ NumpadBackspace,
+ /// <kbd>C</kbd> or <kbd>A</kbd> (All Clear). Also for use with numpads that have a
+ /// <kbd>Clear</kbd> key that is separate from the <kbd>NumLock</kbd> key. On the
+ /// Mac, the numpad <kbd>Clear</kbd> key is encoded as [`NumLock`].
+ ///
+ /// [`NumLock`]: Self::NumLock
+ NumpadClear,
+ /// <kbd>C</kbd> (Clear Entry)
+ NumpadClearEntry,
+ /// <kbd>,</kbd> (thousands separator). For locales where the thousands separator
+ /// is a "." (e.g., Brazil), this key may generate a <kbd>.</kbd>.
+ NumpadComma,
+ /// <kbd>. Del</kbd>. For locales where the decimal separator is "," (e.g.,
+ /// Brazil), this key may generate a <kbd>,</kbd>.
+ NumpadDecimal,
+ /// <kbd>/</kbd>
+ NumpadDivide,
+ NumpadEnter,
+ /// <kbd>=</kbd>
+ NumpadEqual,
+ /// <kbd>#</kbd> on a phone or remote control device. This key is typically found
+ /// below the <kbd>9</kbd> key and to the right of the <kbd>0</kbd> key.
+ NumpadHash,
+ /// <kbd>M</kbd> Add current entry to the value stored in memory.
+ NumpadMemoryAdd,
+ /// <kbd>M</kbd> Clear the value stored in memory.
+ NumpadMemoryClear,
+ /// <kbd>M</kbd> Replace the current entry with the value stored in memory.
+ NumpadMemoryRecall,
+ /// <kbd>M</kbd> Replace the value stored in memory with the current entry.
+ NumpadMemoryStore,
+ /// <kbd>M</kbd> Subtract current entry from the value stored in memory.
+ NumpadMemorySubtract,
+ /// <kbd>*</kbd> on a keyboard. For use with numpads that provide mathematical
+ /// operations (<kbd>+</kbd>, <kbd>-</kbd> <kbd>*</kbd> and <kbd>/</kbd>).
+ ///
+ /// Use `NumpadStar` for the <kbd>*</kbd> key on phones and remote controls.
+ NumpadMultiply,
+ /// <kbd>(</kbd> Found on the Microsoft Natural Keyboard.
+ NumpadParenLeft,
+ /// <kbd>)</kbd> Found on the Microsoft Natural Keyboard.
+ NumpadParenRight,
+ /// <kbd>*</kbd> on a phone or remote control device.
+ ///
+ /// This key is typically found below the <kbd>7</kbd> key and to the left of
+ /// the <kbd>0</kbd> key.
+ ///
+ /// Use <kbd>"NumpadMultiply"</kbd> for the <kbd>*</kbd> key on
+ /// numeric keypads.
+ NumpadStar,
+ /// <kbd>-</kbd>
+ NumpadSubtract,
+ /// <kbd>Esc</kbd> or <kbd>⎋</kbd>
+ Escape,
+ /// <kbd>Fn</kbd> This is typically a hardware key that does not generate a separate
+ /// code.
+ Fn,
+ /// <kbd>FLock</kbd> or <kbd>FnLock</kbd>. Function Lock key. Found on the Microsoft
+ /// Natural Keyboard.
+ FnLock,
+ /// <kbd>PrtScr SysRq</kbd> or <kbd>Print Screen</kbd>
+ PrintScreen,
+ /// <kbd>Scroll Lock</kbd>
+ ScrollLock,
+ /// <kbd>Pause Break</kbd>
+ Pause,
+ /// Some laptops place this key to the left of the <kbd>↑</kbd> key.
+ ///
+ /// This also the "back" button (triangle) on Android.
+ BrowserBack,
+ BrowserFavorites,
+ /// Some laptops place this key to the right of the <kbd>↑</kbd> key.
+ BrowserForward,
+ /// The "home" button on Android.
+ BrowserHome,
+ BrowserRefresh,
+ BrowserSearch,
+ BrowserStop,
+ /// <kbd>Eject</kbd> or <kbd>⏏</kbd>. This key is placed in the function section on
+ /// some Apple keyboards.
+ Eject,
+ /// Sometimes labelled <kbd>My Computer</kbd> on the keyboard
+ LaunchApp1,
+ /// Sometimes labelled <kbd>Calculator</kbd> on the keyboard
+ LaunchApp2,
+ LaunchMail,
+ MediaPlayPause,
+ MediaSelect,
+ MediaStop,
+ MediaTrackNext,
+ MediaTrackPrevious,
+ /// This key is placed in the function section on some Apple keyboards, replacing the
+ /// <kbd>Eject</kbd> key.
+ Power,
+ Sleep,
+ AudioVolumeDown,
+ AudioVolumeMute,
+ AudioVolumeUp,
+ WakeUp,
+ // Legacy modifier key. Also called "Super" in certain places.
+ Meta,
+ // Legacy modifier key.
+ Hyper,
+ Turbo,
+ Abort,
+ Resume,
+ Suspend,
+ /// Found on Sun’s USB keyboard.
+ Again,
+ /// Found on Sun’s USB keyboard.
+ Copy,
+ /// Found on Sun’s USB keyboard.
+ Cut,
+ /// Found on Sun’s USB keyboard.
+ Find,
+ /// Found on Sun’s USB keyboard.
+ Open,
+ /// Found on Sun’s USB keyboard.
+ Paste,
+ /// Found on Sun’s USB keyboard.
+ Props,
+ /// Found on Sun’s USB keyboard.
+ Select,
+ /// Found on Sun’s USB keyboard.
+ Undo,
+ /// Use for dedicated <kbd>ひらがな</kbd> key found on some Japanese word processing
+ /// keyboards.
+ Hiragana,
+ /// Use for dedicated <kbd>カタカナ</kbd> key found on some Japanese word processing
+ /// keyboards.
+ Katakana,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F1,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F2,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F3,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F4,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F5,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F6,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F7,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F8,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F9,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F10,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F11,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F12,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F13,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F14,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F15,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F16,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F17,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F18,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F19,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F20,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F21,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F22,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F23,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F24,
+ /// General-purpose function key.
+ F25,
+ /// General-purpose function key.
+ F26,
+ /// General-purpose function key.
+ F27,
+ /// General-purpose function key.
+ F28,
+ /// General-purpose function key.
+ F29,
+ /// General-purpose function key.
+ F30,
+ /// General-purpose function key.
+ F31,
+ /// General-purpose function key.
+ F32,
+ /// General-purpose function key.
+ F33,
+ /// General-purpose function key.
+ F34,
+ /// General-purpose function key.
+ F35,
+}
+
+impl TryFrom<winit::keyboard::KeyCode> for Key
+{
+ type Error = UnknownKeyCodeError;
+
+ fn try_from(key_code: winit::keyboard::KeyCode) -> Result<Self, Self::Error>
+ {
+ match key_code {
+ winit::keyboard::KeyCode::Backquote => Ok(Self::Backquote),
+ winit::keyboard::KeyCode::Backslash => Ok(Self::Backslash),
+ winit::keyboard::KeyCode::BracketLeft => Ok(Self::BracketLeft),
+ winit::keyboard::KeyCode::BracketRight => Ok(Self::BracketRight),
+ winit::keyboard::KeyCode::Comma => Ok(Self::Comma),
+ winit::keyboard::KeyCode::Digit0 => Ok(Self::Digit0),
+ winit::keyboard::KeyCode::Digit1 => Ok(Self::Digit1),
+ winit::keyboard::KeyCode::Digit2 => Ok(Self::Digit2),
+ winit::keyboard::KeyCode::Digit3 => Ok(Self::Digit3),
+ winit::keyboard::KeyCode::Digit4 => Ok(Self::Digit4),
+ winit::keyboard::KeyCode::Digit5 => Ok(Self::Digit5),
+ winit::keyboard::KeyCode::Digit6 => Ok(Self::Digit6),
+ winit::keyboard::KeyCode::Digit7 => Ok(Self::Digit7),
+ winit::keyboard::KeyCode::Digit8 => Ok(Self::Digit8),
+ winit::keyboard::KeyCode::Digit9 => Ok(Self::Digit9),
+ winit::keyboard::KeyCode::Equal => Ok(Self::Equal),
+ winit::keyboard::KeyCode::IntlBackslash => Ok(Self::IntlBackslash),
+ winit::keyboard::KeyCode::IntlRo => Ok(Self::IntlRo),
+ winit::keyboard::KeyCode::IntlYen => Ok(Self::IntlYen),
+ winit::keyboard::KeyCode::KeyA => Ok(Self::A),
+ winit::keyboard::KeyCode::KeyB => Ok(Self::B),
+ winit::keyboard::KeyCode::KeyC => Ok(Self::C),
+ winit::keyboard::KeyCode::KeyD => Ok(Self::D),
+ winit::keyboard::KeyCode::KeyE => Ok(Self::E),
+ winit::keyboard::KeyCode::KeyF => Ok(Self::F),
+ winit::keyboard::KeyCode::KeyG => Ok(Self::G),
+ winit::keyboard::KeyCode::KeyH => Ok(Self::H),
+ winit::keyboard::KeyCode::KeyI => Ok(Self::I),
+ winit::keyboard::KeyCode::KeyJ => Ok(Self::J),
+ winit::keyboard::KeyCode::KeyK => Ok(Self::K),
+ winit::keyboard::KeyCode::KeyL => Ok(Self::L),
+ winit::keyboard::KeyCode::KeyM => Ok(Self::M),
+ winit::keyboard::KeyCode::KeyN => Ok(Self::N),
+ winit::keyboard::KeyCode::KeyO => Ok(Self::O),
+ winit::keyboard::KeyCode::KeyP => Ok(Self::P),
+ winit::keyboard::KeyCode::KeyQ => Ok(Self::Q),
+ winit::keyboard::KeyCode::KeyR => Ok(Self::R),
+ winit::keyboard::KeyCode::KeyS => Ok(Self::S),
+ winit::keyboard::KeyCode::KeyT => Ok(Self::T),
+ winit::keyboard::KeyCode::KeyU => Ok(Self::U),
+ winit::keyboard::KeyCode::KeyV => Ok(Self::V),
+ winit::keyboard::KeyCode::KeyW => Ok(Self::W),
+ winit::keyboard::KeyCode::KeyX => Ok(Self::X),
+ winit::keyboard::KeyCode::KeyY => Ok(Self::Y),
+ winit::keyboard::KeyCode::KeyZ => Ok(Self::Z),
+ winit::keyboard::KeyCode::Minus => Ok(Self::Minus),
+ winit::keyboard::KeyCode::Period => Ok(Self::Period),
+ winit::keyboard::KeyCode::Quote => Ok(Self::Quote),
+ winit::keyboard::KeyCode::Semicolon => Ok(Self::Semicolon),
+ winit::keyboard::KeyCode::Slash => Ok(Self::Slash),
+ winit::keyboard::KeyCode::AltLeft => Ok(Self::AltLeft),
+ winit::keyboard::KeyCode::AltRight => Ok(Self::AltRight),
+ winit::keyboard::KeyCode::Backspace => Ok(Self::Backspace),
+ winit::keyboard::KeyCode::CapsLock => Ok(Self::CapsLock),
+ winit::keyboard::KeyCode::ContextMenu => Ok(Self::ContextMenu),
+ winit::keyboard::KeyCode::ControlLeft => Ok(Self::ControlLeft),
+ winit::keyboard::KeyCode::ControlRight => Ok(Self::ControlRight),
+ winit::keyboard::KeyCode::Enter => Ok(Self::Enter),
+ winit::keyboard::KeyCode::SuperLeft => Ok(Self::SuperLeft),
+ winit::keyboard::KeyCode::SuperRight => Ok(Self::SuperRight),
+ winit::keyboard::KeyCode::ShiftLeft => Ok(Self::ShiftLeft),
+ winit::keyboard::KeyCode::ShiftRight => Ok(Self::ShiftRight),
+ winit::keyboard::KeyCode::Space => Ok(Self::Space),
+ winit::keyboard::KeyCode::Tab => Ok(Self::Tab),
+ winit::keyboard::KeyCode::Convert => Ok(Self::Convert),
+ winit::keyboard::KeyCode::KanaMode => Ok(Self::KanaMode),
+ winit::keyboard::KeyCode::Lang1 => Ok(Self::Lang1),
+ winit::keyboard::KeyCode::Lang2 => Ok(Self::Lang2),
+ winit::keyboard::KeyCode::Lang3 => Ok(Self::Lang3),
+ winit::keyboard::KeyCode::Lang4 => Ok(Self::Lang4),
+ winit::keyboard::KeyCode::Lang5 => Ok(Self::Lang5),
+ winit::keyboard::KeyCode::NonConvert => Ok(Self::NonConvert),
+ winit::keyboard::KeyCode::Delete => Ok(Self::Delete),
+ winit::keyboard::KeyCode::End => Ok(Self::End),
+ winit::keyboard::KeyCode::Help => Ok(Self::Help),
+ winit::keyboard::KeyCode::Home => Ok(Self::Home),
+ winit::keyboard::KeyCode::Insert => Ok(Self::Insert),
+ winit::keyboard::KeyCode::PageDown => Ok(Self::PageDown),
+ winit::keyboard::KeyCode::PageUp => Ok(Self::PageUp),
+ winit::keyboard::KeyCode::ArrowDown => Ok(Self::ArrowDown),
+ winit::keyboard::KeyCode::ArrowLeft => Ok(Self::ArrowLeft),
+ winit::keyboard::KeyCode::ArrowRight => Ok(Self::ArrowRight),
+ winit::keyboard::KeyCode::ArrowUp => Ok(Self::ArrowUp),
+ winit::keyboard::KeyCode::NumLock => Ok(Self::NumLock),
+ winit::keyboard::KeyCode::Numpad0 => Ok(Self::Numpad0),
+ winit::keyboard::KeyCode::Numpad1 => Ok(Self::Numpad1),
+ winit::keyboard::KeyCode::Numpad2 => Ok(Self::Numpad2),
+ winit::keyboard::KeyCode::Numpad3 => Ok(Self::Numpad3),
+ winit::keyboard::KeyCode::Numpad4 => Ok(Self::Numpad4),
+ winit::keyboard::KeyCode::Numpad5 => Ok(Self::Numpad5),
+ winit::keyboard::KeyCode::Numpad6 => Ok(Self::Numpad6),
+ winit::keyboard::KeyCode::Numpad7 => Ok(Self::Numpad7),
+ winit::keyboard::KeyCode::Numpad8 => Ok(Self::Numpad8),
+ winit::keyboard::KeyCode::Numpad9 => Ok(Self::Numpad9),
+ winit::keyboard::KeyCode::NumpadAdd => Ok(Self::NumpadAdd),
+ winit::keyboard::KeyCode::NumpadBackspace => Ok(Self::NumpadBackspace),
+ winit::keyboard::KeyCode::NumpadClear => Ok(Self::NumpadClear),
+ winit::keyboard::KeyCode::NumpadClearEntry => Ok(Self::NumpadClearEntry),
+ winit::keyboard::KeyCode::NumpadComma => Ok(Self::NumpadComma),
+ winit::keyboard::KeyCode::NumpadDecimal => Ok(Self::NumpadDecimal),
+ winit::keyboard::KeyCode::NumpadDivide => Ok(Self::NumpadDivide),
+ winit::keyboard::KeyCode::NumpadEnter => Ok(Self::NumpadEnter),
+ winit::keyboard::KeyCode::NumpadEqual => Ok(Self::NumpadEqual),
+ winit::keyboard::KeyCode::NumpadHash => Ok(Self::NumpadHash),
+ winit::keyboard::KeyCode::NumpadMemoryAdd => Ok(Self::NumpadMemoryAdd),
+ winit::keyboard::KeyCode::NumpadMemoryClear => Ok(Self::NumpadMemoryClear),
+ winit::keyboard::KeyCode::NumpadMemoryRecall => Ok(Self::NumpadMemoryRecall),
+ winit::keyboard::KeyCode::NumpadMemoryStore => Ok(Self::NumpadMemoryStore),
+ winit::keyboard::KeyCode::NumpadMemorySubtract => {
+ Ok(Self::NumpadMemorySubtract)
+ }
+ winit::keyboard::KeyCode::NumpadMultiply => Ok(Self::NumpadMultiply),
+ winit::keyboard::KeyCode::NumpadParenLeft => Ok(Self::NumpadParenLeft),
+ winit::keyboard::KeyCode::NumpadParenRight => Ok(Self::NumpadParenRight),
+ winit::keyboard::KeyCode::NumpadStar => Ok(Self::NumpadStar),
+ winit::keyboard::KeyCode::NumpadSubtract => Ok(Self::NumpadSubtract),
+ winit::keyboard::KeyCode::Escape => Ok(Self::Escape),
+ winit::keyboard::KeyCode::Fn => Ok(Self::Fn),
+ winit::keyboard::KeyCode::FnLock => Ok(Self::FnLock),
+ winit::keyboard::KeyCode::PrintScreen => Ok(Self::PrintScreen),
+ winit::keyboard::KeyCode::ScrollLock => Ok(Self::ScrollLock),
+ winit::keyboard::KeyCode::Pause => Ok(Self::Pause),
+ winit::keyboard::KeyCode::BrowserBack => Ok(Self::BrowserBack),
+ winit::keyboard::KeyCode::BrowserFavorites => Ok(Self::BrowserFavorites),
+ winit::keyboard::KeyCode::BrowserForward => Ok(Self::BrowserForward),
+ winit::keyboard::KeyCode::BrowserHome => Ok(Self::BrowserHome),
+ winit::keyboard::KeyCode::BrowserRefresh => Ok(Self::BrowserRefresh),
+ winit::keyboard::KeyCode::BrowserSearch => Ok(Self::BrowserSearch),
+ winit::keyboard::KeyCode::BrowserStop => Ok(Self::BrowserStop),
+ winit::keyboard::KeyCode::Eject => Ok(Self::Eject),
+ winit::keyboard::KeyCode::LaunchApp1 => Ok(Self::LaunchApp1),
+ winit::keyboard::KeyCode::LaunchApp2 => Ok(Self::LaunchApp2),
+ winit::keyboard::KeyCode::LaunchMail => Ok(Self::LaunchMail),
+ winit::keyboard::KeyCode::MediaPlayPause => Ok(Self::MediaPlayPause),
+ winit::keyboard::KeyCode::MediaSelect => Ok(Self::MediaSelect),
+ winit::keyboard::KeyCode::MediaStop => Ok(Self::MediaStop),
+ winit::keyboard::KeyCode::MediaTrackNext => Ok(Self::MediaTrackNext),
+ winit::keyboard::KeyCode::MediaTrackPrevious => Ok(Self::MediaTrackPrevious),
+ winit::keyboard::KeyCode::Power => Ok(Self::Power),
+ winit::keyboard::KeyCode::Sleep => Ok(Self::Sleep),
+ winit::keyboard::KeyCode::AudioVolumeDown => Ok(Self::AudioVolumeDown),
+ winit::keyboard::KeyCode::AudioVolumeMute => Ok(Self::AudioVolumeMute),
+ winit::keyboard::KeyCode::AudioVolumeUp => Ok(Self::AudioVolumeUp),
+ winit::keyboard::KeyCode::WakeUp => Ok(Self::WakeUp),
+ winit::keyboard::KeyCode::Meta => Ok(Self::Meta),
+ winit::keyboard::KeyCode::Hyper => Ok(Self::Hyper),
+ winit::keyboard::KeyCode::Turbo => Ok(Self::Turbo),
+ winit::keyboard::KeyCode::Abort => Ok(Self::Abort),
+ winit::keyboard::KeyCode::Resume => Ok(Self::Resume),
+ winit::keyboard::KeyCode::Suspend => Ok(Self::Suspend),
+ winit::keyboard::KeyCode::Again => Ok(Self::Again),
+ winit::keyboard::KeyCode::Copy => Ok(Self::Copy),
+ winit::keyboard::KeyCode::Cut => Ok(Self::Cut),
+ winit::keyboard::KeyCode::Find => Ok(Self::Find),
+ winit::keyboard::KeyCode::Open => Ok(Self::Open),
+ winit::keyboard::KeyCode::Paste => Ok(Self::Paste),
+ winit::keyboard::KeyCode::Props => Ok(Self::Props),
+ winit::keyboard::KeyCode::Select => Ok(Self::Select),
+ winit::keyboard::KeyCode::Undo => Ok(Self::Undo),
+ winit::keyboard::KeyCode::Hiragana => Ok(Self::Hiragana),
+ winit::keyboard::KeyCode::Katakana => Ok(Self::Katakana),
+ winit::keyboard::KeyCode::F1 => Ok(Self::F1),
+ winit::keyboard::KeyCode::F2 => Ok(Self::F2),
+ winit::keyboard::KeyCode::F3 => Ok(Self::F3),
+ winit::keyboard::KeyCode::F4 => Ok(Self::F4),
+ winit::keyboard::KeyCode::F5 => Ok(Self::F5),
+ winit::keyboard::KeyCode::F6 => Ok(Self::F6),
+ winit::keyboard::KeyCode::F7 => Ok(Self::F7),
+ winit::keyboard::KeyCode::F8 => Ok(Self::F8),
+ winit::keyboard::KeyCode::F9 => Ok(Self::F9),
+ winit::keyboard::KeyCode::F10 => Ok(Self::F10),
+ winit::keyboard::KeyCode::F11 => Ok(Self::F11),
+ winit::keyboard::KeyCode::F12 => Ok(Self::F12),
+ winit::keyboard::KeyCode::F13 => Ok(Self::F13),
+ winit::keyboard::KeyCode::F14 => Ok(Self::F14),
+ winit::keyboard::KeyCode::F15 => Ok(Self::F15),
+ winit::keyboard::KeyCode::F16 => Ok(Self::F16),
+ winit::keyboard::KeyCode::F17 => Ok(Self::F17),
+ winit::keyboard::KeyCode::F18 => Ok(Self::F18),
+ winit::keyboard::KeyCode::F19 => Ok(Self::F19),
+ winit::keyboard::KeyCode::F20 => Ok(Self::F20),
+ winit::keyboard::KeyCode::F21 => Ok(Self::F21),
+ winit::keyboard::KeyCode::F22 => Ok(Self::F22),
+ winit::keyboard::KeyCode::F23 => Ok(Self::F23),
+ winit::keyboard::KeyCode::F24 => Ok(Self::F24),
+ winit::keyboard::KeyCode::F25 => Ok(Self::F25),
+ winit::keyboard::KeyCode::F26 => Ok(Self::F26),
+ winit::keyboard::KeyCode::F27 => Ok(Self::F27),
+ winit::keyboard::KeyCode::F28 => Ok(Self::F28),
+ winit::keyboard::KeyCode::F29 => Ok(Self::F29),
+ winit::keyboard::KeyCode::F30 => Ok(Self::F30),
+ winit::keyboard::KeyCode::F31 => Ok(Self::F31),
+ winit::keyboard::KeyCode::F32 => Ok(Self::F32),
+ winit::keyboard::KeyCode::F33 => Ok(Self::F33),
+ winit::keyboard::KeyCode::F34 => Ok(Self::F34),
+ winit::keyboard::KeyCode::F35 => Ok(Self::F35),
+ _ => Err(UnknownKeyCodeError),
+ }
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error("Unknown key code")]
+pub struct UnknownKeyCodeError;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum KeyState
+{
+ Pressed,
+ Released,
+}
+
+impl KeyState
+{
+ #[must_use]
+ #[inline]
+ pub fn is_pressed(&self) -> bool
+ {
+ matches!(self, Self::Pressed)
+ }
+
+ #[must_use]
+ #[inline]
+ pub fn is_released(&self) -> bool
+ {
+ matches!(self, Self::Released)
+ }
+}
+
+impl From<winit::event::ElementState> for KeyState
+{
+ fn from(element_state: winit::event::ElementState) -> Self
+ {
+ match element_state {
+ winit::event::ElementState::Pressed => Self::Pressed,
+ winit::event::ElementState::Released => Self::Released,
+ }
+ }
+}
+
+#[derive(Debug)]
+struct KeyData
+{
+ curr_state: KeyState,
+ previous_state: KeyState,
+}
+
+impl Default for KeyData
+{
+ fn default() -> Self
+ {
+ KeyData {
+ curr_state: KeyState::Released,
+ previous_state: KeyState::Released,
+ }
+ }
+}
diff --git a/engine/src/windowing/mouse.rs b/engine/src/windowing/mouse.rs
new file mode 100644
index 0000000..1afe594
--- /dev/null
+++ b/engine/src/windowing/mouse.rs
@@ -0,0 +1,136 @@
+use std::collections::HashMap;
+
+use ecs::Sole;
+
+use crate::vector::Vec2;
+
+#[derive(Debug, Default, Clone, Sole)]
+#[non_exhaustive]
+pub struct Motion
+{
+ pub position_delta: Vec2<f64>,
+}
+
+/// Mouse buttons.
+#[derive(Debug, Default, Sole)]
+pub struct Buttons
+{
+ map: HashMap<Button, ButtonData>,
+}
+
+impl Buttons
+{
+ pub fn get(&self, button: Button) -> ButtonState
+ {
+ let Some(button_data) = self.map.get(&button) else {
+ return ButtonState::Released;
+ };
+
+ button_data.current_state
+ }
+
+ pub fn get_previous(&self, button: Button) -> ButtonState
+ {
+ let Some(button_data) = self.map.get(&button) else {
+ return ButtonState::Released;
+ };
+
+ button_data.previous_state
+ }
+
+ pub fn set(&mut self, button: Button, button_state: ButtonState)
+ {
+ let button_data = self.map.entry(button).or_default();
+
+ button_data.current_state = button_state;
+ }
+
+ pub(crate) fn make_states_previous(&mut self)
+ {
+ for button_data in self.map.values_mut() {
+ button_data.previous_state = button_data.current_state;
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum Button
+{
+ Left,
+ Right,
+ Middle,
+ Back,
+ Forward,
+ Other(u16),
+}
+
+impl From<winit::event::MouseButton> for Button
+{
+ fn from(mouse_button: winit::event::MouseButton) -> Self
+ {
+ match mouse_button {
+ winit::event::MouseButton::Left => Self::Left,
+ winit::event::MouseButton::Right => Self::Right,
+ winit::event::MouseButton::Middle => Self::Middle,
+ winit::event::MouseButton::Back => Self::Back,
+ winit::event::MouseButton::Forward => Self::Forward,
+ winit::event::MouseButton::Other(other_mouse_button) => {
+ Self::Other(other_mouse_button)
+ }
+ }
+ }
+}
+
+/// Mouse button state.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum ButtonState
+{
+ Pressed,
+ Released,
+}
+
+impl ButtonState
+{
+ #[must_use]
+ #[inline]
+ pub fn is_pressed(&self) -> bool
+ {
+ matches!(self, Self::Pressed)
+ }
+
+ #[must_use]
+ #[inline]
+ pub fn is_released(&self) -> bool
+ {
+ matches!(self, Self::Released)
+ }
+}
+
+impl From<winit::event::ElementState> for ButtonState
+{
+ fn from(element_state: winit::event::ElementState) -> Self
+ {
+ match element_state {
+ winit::event::ElementState::Pressed => Self::Pressed,
+ winit::event::ElementState::Released => Self::Released,
+ }
+ }
+}
+
+#[derive(Debug)]
+struct ButtonData
+{
+ current_state: ButtonState,
+ previous_state: ButtonState,
+}
+
+impl Default for ButtonData
+{
+ fn default() -> Self
+ {
+ Self {
+ current_state: ButtonState::Released,
+ previous_state: ButtonState::Released,
+ }
+ }
+}
diff --git a/engine/src/windowing/window.rs b/engine/src/windowing/window.rs
new file mode 100644
index 0000000..79b2102
--- /dev/null
+++ b/engine/src/windowing/window.rs
@@ -0,0 +1,171 @@
+use std::borrow::Cow;
+
+use ecs::Component;
+
+use crate::data_types::dimens::Dimens;
+
+pub mod platform;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Id
+{
+ inner: winit::window::WindowId,
+}
+
+impl Id
+{
+ pub(crate) fn from_inner(inner: winit::window::WindowId) -> Self
+ {
+ Self { inner }
+ }
+}
+
+macro_rules! impl_creation_attributes_field_fns {
+ ($field: ident, ($($getter_ret_pre: tt)?), $getter_ret_type: ty, $field_type: ty) => {
+ impl CreationAttributes
+ {
+ pub fn $field(&self) -> $getter_ret_type
+ {
+ $($getter_ret_pre)? self.attrs.$field
+ }
+
+ paste::paste! {
+ pub fn [<with_ $field>](mut self, $field: impl Into<$field_type>) -> Self {
+ self.attrs.$field = $field.into();
+ self
+ }
+ }
+ }
+ };
+}
+
+#[derive(Debug, Component, Clone)]
+#[non_exhaustive]
+pub struct CreationAttributes
+{
+ attrs: winit::window::WindowAttributes,
+}
+
+impl_creation_attributes_field_fns!(title, (&), &str, String);
+impl_creation_attributes_field_fns!(transparent, (), bool, bool);
+
+impl CreationAttributes
+{
+ #[cfg(target_os = "linux")]
+ pub fn with_x11_visual(mut self, visual_id: XVisualID) -> Self
+ {
+ use winit::platform::x11::WindowAttributesExtX11;
+
+ self.attrs = self.attrs.with_x11_visual(visual_id);
+
+ self
+ }
+}
+
+impl CreationAttributes
+{
+ pub(crate) fn into_inner(self) -> winit::window::WindowAttributes
+ {
+ self.attrs
+ }
+}
+
+impl Default for CreationAttributes
+{
+ fn default() -> Self
+ {
+ CreationAttributes {
+ attrs: winit::window::WindowAttributes::default().with_title("Application"),
+ }
+ }
+}
+
+#[derive(Debug, Component, Clone, Copy)]
+pub struct CreationReady;
+
+#[derive(Debug, Component)]
+#[non_exhaustive]
+pub struct Window
+{
+ wid: Id,
+ pub title: Cow<'static, str>,
+ pub cursor_visible: bool,
+ pub cursor_grab_mode: CursorGrabMode,
+ inner_size: Dimens<u32>,
+}
+
+impl Window
+{
+ pub fn wid(&self) -> Id
+ {
+ self.wid
+ }
+
+ pub fn inner_size(&self) -> &Dimens<u32>
+ {
+ &self.inner_size
+ }
+
+ pub(crate) fn new(
+ winit_window: &winit::window::Window,
+ creation_attrs: &CreationAttributes,
+ ) -> Self
+ {
+ Self {
+ wid: Id::from_inner(winit_window.id()),
+ title: creation_attrs.title().to_string().into(),
+ cursor_visible: true,
+ cursor_grab_mode: CursorGrabMode::None,
+ inner_size: winit_window.inner_size().into(),
+ }
+ }
+
+ pub(crate) fn apply(&self, winit_window: &winit::window::Window)
+ {
+ winit_window.set_title(&self.title);
+ winit_window.set_cursor_visible(self.cursor_visible);
+ }
+
+ pub(crate) fn set_inner_size(&mut self, inner_size: Dimens<u32>)
+ {
+ self.inner_size = inner_size;
+ }
+}
+
+#[derive(Debug, Component)]
+pub struct Closed;
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum CursorGrabMode
+{
+ #[default]
+ None,
+
+ /// The cursor is locked to a specific position in the window.
+ Locked,
+}
+
+/// A unique identifier for an X11 visual.
+pub type XVisualID = u32;
+
+impl<P> From<winit::dpi::PhysicalSize<P>> for Dimens<P>
+{
+ fn from(size: winit::dpi::PhysicalSize<P>) -> Self
+ {
+ Self {
+ width: size.width,
+ height: size.height,
+ }
+ }
+}
+
+impl<P> From<Dimens<P>> for winit::dpi::PhysicalSize<P>
+{
+ fn from(dimens: Dimens<P>) -> Self
+ {
+ Self {
+ width: dimens.width,
+ height: dimens.height,
+ }
+ }
+}
diff --git a/engine/src/windowing/window/platform.rs b/engine/src/windowing/window/platform.rs
new file mode 100644
index 0000000..f3908a2
--- /dev/null
+++ b/engine/src/windowing/window/platform.rs
@@ -0,0 +1,12 @@
+#[cfg(x11_platform)]
+pub mod x11
+{
+ use std::ffi::c_void;
+
+ pub type XlibErrorHook = Box<dyn Fn(*mut c_void, *mut c_void) -> bool + Send + Sync>;
+
+ pub fn register_xlib_error_hook(hook: XlibErrorHook)
+ {
+ winit::platform::x11::register_xlib_error_hook(hook);
+ }
+}
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/Cargo.toml b/glfw/Cargo.toml
deleted file mode 100644
index 0a47e73..0000000
--- a/glfw/Cargo.toml
+++ /dev/null
@@ -1,18 +0,0 @@
-[package]
-name = "glfw"
-version = "0.1.0"
-edition = "2021"
-
-[features]
-opengl = []
-
-[dependencies]
-libc = "0.2.148"
-thiserror = "1.0.49"
-bitflags = "2.4.0"
-util-macros = { path = "../util-macros" }
-
-[build-dependencies.bindgen]
-version = "0.68.1"
-default-features = false
-features = ["runtime"]
diff --git a/glfw/build.rs b/glfw/build.rs
deleted file mode 100644
index 18ac677..0000000
--- a/glfw/build.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use std::env;
-use std::error::Error;
-use std::fs::OpenOptions;
-use std::io::Write;
-use std::path::PathBuf;
-
-use bindgen::{Abi, MacroTypeVariation};
-
-fn main() -> Result<(), Box<dyn Error>>
-{
- println!("cargo:rustc-link-lib=glfw");
-
- println!("cargo:rerun-if-changed=glfw.h");
-
- let bindings = bindgen::Builder::default()
- .header("glfw.h")
- .clang_arg("-fretain-comments-from-system-headers")
- .generate_comments(true)
- .allowlist_function("glfw.*")
- .allowlist_type("GLFW.*")
- .allowlist_var("GLFW.*")
- .blocklist_type("GLFWglproc")
- .default_macro_constant_type(MacroTypeVariation::Signed)
- .override_abi(Abi::CUnwind, ".*")
- .generate()?;
-
- let out_path = PathBuf::from(env::var("OUT_DIR")?);
-
- let bindings_file_path = out_path.join("bindings.rs");
-
- bindings.write_to_file(&bindings_file_path)?;
-
- let mut bindings_file = OpenOptions::new().append(true).open(bindings_file_path)?;
-
- // Cannot be C-unwind :(
- writeln!(
- bindings_file,
- "pub type GLFWglproc = ::std::option::Option<unsafe extern \"C\" fn()>;"
- )?;
-
- Ok(())
-}
diff --git a/glfw/glfw.h b/glfw/glfw.h
deleted file mode 100644
index 7b435c0..0000000
--- a/glfw/glfw.h
+++ /dev/null
@@ -1,6 +0,0 @@
-#include <GLFW/glfw3.h>
-
-#if GLFW_VERSION_MAJOR != 3
-#error "Unsupported major version of GLFW. Must be 3"
-#endif
-
diff --git a/glfw/src/ffi.rs b/glfw/src/ffi.rs
deleted file mode 100644
index d0affd0..0000000
--- a/glfw/src/ffi.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-#![allow(
- non_snake_case,
- non_camel_case_types,
- non_upper_case_globals,
- unused,
- clippy::unreadable_literal
-)]
-
-include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
diff --git a/glfw/src/init.rs b/glfw/src/init.rs
deleted file mode 100644
index df5a989..0000000
--- a/glfw/src/init.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-use std::ffi::c_int;
-use std::ptr::null_mut;
-
-use crate::util::is_main_thread;
-use crate::{get_glfw_error, Error};
-
-/// GLFW 3.x.x is supported
-static SUPPORTED_GLFW_VERSION_MAJOR: c_int = 3;
-
-/// Initializes GLFW and returns a initialization token.
-///
-/// # Errors
-/// Will return `Err` if
-/// - The current thread is not the main thread
-/// - A GLFW error occurs
-pub fn initialize() -> Result<Glfw, Error>
-{
- if !is_main_thread() {
- return Err(Error::NotInMainThread);
- }
-
- let mut major: c_int = 0;
-
- unsafe { crate::ffi::glfwGetVersion(&mut major, null_mut(), null_mut()) };
-
- if major != SUPPORTED_GLFW_VERSION_MAJOR {
- return Err(Error::UnsupportedVersion);
- }
-
- // SAFETY: The current thread is the main thread
- let success = unsafe { crate::ffi::glfwInit() };
-
- if success == crate::ffi::GLFW_FALSE {
- get_glfw_error()?;
- }
-
- Ok(Glfw { _priv: &() })
-}
-
-/// GLFW initialization token.
-#[derive(Debug)]
-pub struct Glfw
-{
- /// This field has two purposes
- /// - To make the struct not constructable without calling [`initialize`].
- /// - To make the struct `!Send` and `!Sync`.
- _priv: *const (),
-}
-
-impl Drop for Glfw
-{
- fn drop(&mut self)
- {
- // SAFETY: The current thread cannot be any other thread than the main thread
- // since the initialize function checks it and the GLFW initialization token is
- // neither Send or Sync
- unsafe { crate::ffi::glfwTerminate() };
- }
-}
diff --git a/glfw/src/lib.rs b/glfw/src/lib.rs
deleted file mode 100644
index f858766..0000000
--- a/glfw/src/lib.rs
+++ /dev/null
@@ -1,53 +0,0 @@
-#![deny(clippy::all, clippy::pedantic)]
-
-use std::ffi::{c_char, CStr};
-use std::ptr::null;
-
-mod ffi;
-mod init;
-mod util;
-
-pub mod window;
-
-pub use window::{Builder as WindowBuilder, Size as WindowSize, Window};
-
-#[derive(Debug, thiserror::Error)]
-pub enum Error
-{
- /// The current thread is not the main thread.
- #[error("The current thread is not the main thread")]
- NotInMainThread,
-
- /// An internal nul byte was found in a window title.
- #[error("An internal nul byte was found in the window title")]
- InternalNulByteInWindowTitle,
-
- #[error("This version of GLFW is unsupported")]
- UnsupportedVersion,
-
- /// GLFW error.
- #[error("GLFW error {0} occured. {1}")]
- GlfwError(i32, String),
-}
-
-fn get_glfw_error() -> Result<(), Error>
-{
- let mut description_ptr: *const c_char = null();
-
- let err = unsafe { crate::ffi::glfwGetError(&mut description_ptr) };
-
- if err == crate::ffi::GLFW_NO_ERROR {
- return Ok(());
- }
-
- // SAFETY: The description is guaranteed by GLFW to be valid UTF-8
- let desc_str = unsafe {
- std::str::from_utf8_unchecked(CStr::from_ptr(description_ptr).to_bytes())
- };
-
- // The description has to be copied because it is guaranteed to be valid only
- // until the next GLFW error occurs or the GLFW library is terminated.
- let description = desc_str.to_string();
-
- Err(Error::GlfwError(err, description))
-}
diff --git a/glfw/src/util.rs b/glfw/src/util.rs
deleted file mode 100644
index f77aaf8..0000000
--- a/glfw/src/util.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-use libc::{c_long, getpid, syscall, SYS_gettid};
-
-pub fn is_main_thread() -> bool
-{
- let ttid = unsafe { syscall(SYS_gettid) };
-
- let pid = c_long::from(unsafe { getpid() });
-
- ttid == pid
-}
diff --git a/glfw/src/window.rs b/glfw/src/window.rs
deleted file mode 100644
index 1e7e777..0000000
--- a/glfw/src/window.rs
+++ /dev/null
@@ -1,863 +0,0 @@
-use std::cell::RefCell;
-use std::ffi::{c_double, c_int, CString};
-use std::hint::unreachable_unchecked;
-use std::io::{stdout, Write};
-use std::ptr::null_mut;
-
-use bitflags::bitflags;
-use util_macros::{FromRepr, VariantArr};
-
-use crate::init::{initialize, Glfw};
-use crate::{get_glfw_error, Error};
-
-#[derive(Debug)]
-pub struct Window
-{
- _init: Glfw,
- handle: *mut crate::ffi::GLFWwindow,
-}
-
-impl Window
-{
- /// Makes the context of the window current for the calling thread.
- ///
- /// # Errors
- /// Will return `Err` if a GLFW platform error occurs or if no OpenGL context is
- /// present.
- #[cfg(feature = "opengl")]
- pub fn make_context_current(&self) -> Result<(), Error>
- {
- unsafe { crate::ffi::glfwMakeContextCurrent(self.handle) };
-
- get_glfw_error()?;
-
- Ok(())
- }
-
- /// Returns the address of the specified OpenGL function, if it is supported by the
- /// current context.
- ///
- /// # Errors
- /// Will return `Err` if a GLFW platform error occurs or if no current context has
- /// been set.
- #[cfg(feature = "opengl")]
- pub fn get_proc_address(
- &self,
- proc_name: &std::ffi::CStr,
- ) -> Result<unsafe extern "C" fn(), Error>
- {
- let proc_addr = unsafe { crate::ffi::glfwGetProcAddress(proc_name.as_ptr()) };
-
- get_glfw_error()?;
-
- // SAFETY: Is only None when a error has occured and that case is handled above
- Ok(unsafe { proc_addr.unwrap_unchecked() })
- }
-
- /// Processes all pending events.
- ///
- /// # Errors
- /// Will return `Err` if a GLFW platform error occurs.
- pub fn poll_events(&self) -> Result<(), Error>
- {
- // SAFETY: The initialize function (called when the window is created) makes sure
- // the current thread is the main thread
- unsafe { crate::ffi::glfwPollEvents() };
-
- get_glfw_error()?;
-
- Ok(())
- }
-
- /// Swaps the front and back buffers of the window.
- ///
- /// # Errors
- /// Will return `Err` if a GLFW platform error occurs or if no OpenGL window context
- /// is present.
- pub fn swap_buffers(&self) -> Result<(), Error>
- {
- unsafe {
- crate::ffi::glfwSwapBuffers(self.handle);
- };
-
- get_glfw_error()?;
-
- Ok(())
- }
-
- /// Returns whether or not the window should close.
- #[must_use]
- pub fn should_close(&self) -> bool
- {
- let should_close = unsafe { crate::ffi::glfwWindowShouldClose(self.handle) };
-
- should_close == crate::ffi::GLFW_TRUE
- }
-
- /// Retrieves the size of the window.
- ///
- /// # Errors
- /// Will return `Err` if a GLFW platform error occurs.
- pub fn size(&self) -> Result<Size, Error>
- {
- let mut width = 0;
- let mut height = 0;
-
- // SAFETY: The initialize function (called when the window is created) makes sure
- // the current thread is the main thread
- unsafe { crate::ffi::glfwGetWindowSize(self.handle, &mut width, &mut height) };
-
- get_glfw_error()?;
-
- #[allow(clippy::cast_sign_loss)]
- Ok(Size {
- width: width as u32,
- height: height as u32,
- })
- }
-
- /// Sets the callback which is called when the user attempts to close the window.
- pub fn set_close_callback(&self, callback: impl Fn() + 'static)
- {
- CLOSE_CALLBACK.with_borrow_mut(|cb| {
- *cb = Some(Box::new(callback));
- });
-
- // SAFETY: The initialize function (called when the window is created) makes sure
- // the current thread is the main thread
- unsafe {
- crate::ffi::glfwSetWindowCloseCallback(self.handle, Some(close_callback));
- }
- }
-
- pub fn set_framebuffer_size_callback(&self, callback: impl Fn(Size) + 'static)
- {
- FRAMEBUFFER_SIZE_CB.with_borrow_mut(|framebuffer_size_cb| {
- *framebuffer_size_cb = Some(Box::new(callback));
- });
-
- // SAFETY: The initialize function (called when the window is created) makes sure
- // the current thread is the main thread
- unsafe {
- crate::ffi::glfwSetFramebufferSizeCallback(
- self.handle,
- Some(framebuffer_size_callback),
- );
- }
- }
-
- pub fn set_key_callback(
- &self,
- callback: impl Fn(Key, i32, KeyState, KeyModifiers) + 'static,
- )
- {
- KEY_CALLBACK.with_borrow_mut(|key_callback| {
- *key_callback = Some(Box::new(callback));
- });
-
- // SAFETY: The initialize function (called when the window is created) makes sure
- // the current thread is the main thread
- unsafe {
- crate::ffi::glfwSetKeyCallback(self.handle, Some(key_callback));
- }
- }
-
- /// Sets the cursor position callback.
- ///
- /// The callback is provided with the position, in screen coordinates, relative to the
- /// upper-left corner of the content area of the window.
- pub fn set_cursor_pos_callback(&self, callback: impl Fn(CursorPosition) + 'static)
- {
- CURSOR_POS_CALLBACK.with_borrow_mut(|cursor_pos_callback| {
- *cursor_pos_callback = Some(Box::new(callback));
- });
-
- // SAFETY: The initialize function (called when the window is created) makes sure
- // the current thread is the main thread
- unsafe {
- crate::ffi::glfwSetCursorPosCallback(self.handle, Some(cursor_pos_callback));
- }
- }
-
- /// Sets the mouse button callback.
- pub fn set_mouse_button_callback(
- &self,
- callback: impl Fn(MouseButton, MouseButtonState, KeyModifiers) + 'static,
- )
- {
- MOUSE_BUTTON_CALLBACK.with_borrow_mut(|mouse_button_callback| {
- *mouse_button_callback = Some(Box::new(callback));
- });
-
- // SAFETY: The initialize function (called when the window is created) makes sure
- // the current thread is the main thread
- unsafe {
- crate::ffi::glfwSetMouseButtonCallback(
- self.handle,
- Some(mouse_button_callback),
- );
- }
- }
-
- /// Sets the callback called when the window gains or loses input focus.
- pub fn set_focus_callback(&self, callback: impl Fn(bool) + 'static)
- {
- FOCUS_CALLBACK.with_borrow_mut(|focus_cb| {
- *focus_cb = Some(Box::new(callback));
- });
-
- // SAFETY: The initialize function (called when the window is created) makes sure
- // the current thread is the main thread
- unsafe {
- crate::ffi::glfwSetWindowFocusCallback(self.handle, Some(focus_callback));
- }
- }
-
- /// Returns the last reported state of a keyboard key.
- ///
- /// # Errors
- /// Will return `Err` if a GLFW error occurs.
- pub fn get_key(&self, key: Key) -> Result<KeyState, Error>
- {
- // SAFETY: The initialize function (called when the window is created) makes sure
- // the current thread is the main thread
- let state = unsafe { crate::ffi::glfwGetKey(self.handle, key as i32) };
-
- get_glfw_error()?;
-
- Ok(match state {
- crate::ffi::GLFW_PRESS => KeyState::Pressed,
- crate::ffi::GLFW_RELEASE => KeyState::Released,
- _ => {
- // SAFETY: glfwGetKey can only return GLFW_PRESS or GLFW_RELEASE
- unsafe {
- unreachable_unchecked();
- }
- }
- })
- }
-
- /// Returns the last reported state of a mouse button.
- ///
- /// # Errors
- /// Will return `Err` if a GLFW error occurs.
- pub fn get_mouse_button(
- &self,
- mouse_button: MouseButton,
- ) -> Result<MouseButtonState, Error>
- {
- // SAFETY: The initialize function (called when the window is created) makes sure
- // the current thread is the main thread
- let state =
- unsafe { crate::ffi::glfwGetMouseButton(self.handle, mouse_button as i32) };
-
- get_glfw_error()?;
-
- Ok(match state {
- crate::ffi::GLFW_PRESS => MouseButtonState::Pressed,
- crate::ffi::GLFW_RELEASE => MouseButtonState::Released,
- _ => {
- // SAFETY: glfwGetMouseButton can only return GLFW_PRESS or GLFW_RELEASE
- unsafe {
- unreachable_unchecked();
- }
- }
- })
- }
-
- /// Returns the position of the cursor, in screen coordinates, relative to the
- /// upper-left corner of the content area of the window.
- ///
- /// # Errors
- /// Will return `Err` if a GLFW error occurs.
- pub fn get_cursor_position(&self) -> Result<CursorPosition, Error>
- {
- let mut x_pos = 0.0;
- let mut y_pos = 0.0;
-
- // SAFETY: The initialize function (called when the window is created) makes sure
- // the current thread is the main thread
- unsafe {
- crate::ffi::glfwGetCursorPos(self.handle, &mut x_pos, &mut y_pos);
- }
-
- get_glfw_error()?;
-
- Ok(CursorPosition { x: x_pos, y: y_pos })
- }
-
- /// Sets a input mode option.
- ///
- /// # Errors
- /// Will return `Err` if a GLFW error occurs.
- pub fn set_input_mode(
- &self,
- input_mode: InputMode,
- enabled: bool,
- ) -> Result<(), Error>
- {
- // SAFETY: The initialize function (called when the window is created) makes sure
- // the current thread is the main thread
- unsafe {
- crate::ffi::glfwSetInputMode(
- self.handle,
- input_mode as i32,
- i32::from(enabled),
- );
- }
-
- get_glfw_error()?;
-
- Ok(())
- }
-
- /// Sets the cursor mode.
- ///
- /// # Errors
- /// Will return `Err` if a GLFW error occurs.
- pub fn set_cursor_mode(&self, cursor_mode: CursorMode) -> Result<(), Error>
- {
- // SAFETY: The initialize function (called when the window is created) makes sure
- // the current thread is the main thread
- unsafe {
- crate::ffi::glfwSetInputMode(
- self.handle,
- crate::ffi::GLFW_CURSOR,
- cursor_mode as i32,
- );
- }
-
- get_glfw_error()?;
-
- Ok(())
- }
-
- /// Returns whether or not raw mouse motion is supported.
- ///
- /// # Errors
- /// Will return `Err` if a GLFW error occurs.
- pub fn is_raw_mouse_motion_supported(&self) -> Result<bool, Error>
- {
- // SAFETY: The initialize function (called when the window is created) makes sure
- // the current thread is the main thread
- let supported = unsafe { crate::ffi::glfwRawMouseMotionSupported() };
-
- get_glfw_error()?;
-
- Ok(supported == crate::ffi::GLFW_TRUE)
- }
-}
-
-/// [`Window`] builder.
-#[derive(Debug, Clone, Default)]
-pub struct Builder
-{
- hints: Vec<(Hint, HintValue)>,
-}
-
-impl Builder
-{
- #[must_use]
- pub fn new() -> Self
- {
- Self { hints: Vec::new() }
- }
-
- /// Adds a window creation hint to set.
- #[must_use]
- pub fn hint(mut self, hint: Hint, value: HintValue) -> Self
- {
- self.hints.push((hint, value));
-
- self
- }
-
- /// Sets the window hints to set.
- pub fn hints(mut self, hints: impl IntoIterator<Item = (Hint, HintValue)>)
- {
- self.hints = hints.into_iter().collect();
- }
-
- /// Creates a new window.
- ///
- /// # Errors
- /// Will return `Err` if
- /// - The title contains an internal nul byte
- /// - A GLFW error occurs
- pub fn create(&self, size: &Size, title: &str) -> Result<Window, Error>
- {
- let c_title =
- CString::new(title).map_err(|_| Error::InternalNulByteInWindowTitle)?;
-
- let init = initialize()?;
-
- for (hint, value) in &self.hints {
- // SAFETY: The initialize function makes sure the current thread is the main
- // thread
- //
- // Error is not checked for after since the two possible errors
- // (GLFW_NOT_INITIALIZED and GLFW_INVALID_ENUM) cannot occur.
- unsafe {
- crate::ffi::glfwWindowHint(
- *hint as i32,
- match value {
- HintValue::Number(num) => *num,
- HintValue::Bool(boolean) => {
- if *boolean {
- crate::ffi::GLFW_TRUE
- } else {
- crate::ffi::GLFW_FALSE
- }
- }
- },
- );
- }
- }
-
- // SAFETY: The initialize function makes sure the current thread is the main
- // thread
- let handle = unsafe {
- #[allow(clippy::cast_possible_wrap)]
- crate::ffi::glfwCreateWindow(
- size.width as i32,
- size.height as i32,
- c_title.as_ptr(),
- null_mut(),
- null_mut(),
- )
- };
-
- get_glfw_error()?;
-
- Ok(Window { _init: init, handle })
- }
-}
-
-/// Window creation hint
-#[derive(Debug, Clone, Copy)]
-#[repr(i32)]
-#[non_exhaustive]
-pub enum Hint
-{
- /// Specifies whether the OpenGL context should be created in debug mode, which may
- /// provide additional error and diagnostic reporting functionality.
- ///
- /// Valid values are [`HintValue::Bool`].
- OpenGLDebugContext = crate::ffi::GLFW_OPENGL_DEBUG_CONTEXT,
-
- /// Specifies the desired number of samples to use for multisampling. Zero disables
- /// multisampling.
- ///
- /// Valid values are: [`HintValue::Number`].
- Samples = crate::ffi::GLFW_SAMPLES,
-
- /// Specifies whether the framebuffer should be double buffered. You nearly always
- /// want to use double buffering. This is a hard constraint.
- ///
- /// Valid values are [`HintValue::Bool`].
- DoubleBuffer = crate::ffi::GLFW_DOUBLEBUFFER,
-}
-
-/// Window creation hint value.
-#[derive(Debug, Clone)]
-#[non_exhaustive]
-pub enum HintValue
-{
- Number(i32),
- Bool(bool),
-}
-
-/// Window size.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct Size
-{
- pub width: u32,
- pub height: u32,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-#[repr(i32)]
-pub enum InputMode
-{
- /// When sticky keys mode is enabled, the pollable state of a key will remain
- /// [`KeyState::Pressed`] until the state of that key is polled with
- /// [`Window::get_key`]. Once it has been polled, if a key release event had been
- /// processed in the meantime, the state will reset to [`KeyState::Released`],
- /// otherwise it will remain [`KeyState::Pressed`].
- StickyKeys = crate::ffi::GLFW_STICKY_KEYS,
-
- /// When sticky mouse buttons mode is enabled, the pollable state of a mouse button
- /// will remain [`MouseButtonState::Pressed`] until the state of that button is
- /// polled with [`Window::get_mouse_button`]. Once it has been polled, if a mouse
- /// button release event had been processed in the meantime, the state will reset
- /// to [`MouseButtonState::Released`], otherwise it will remain
- /// [`MouseButton::Pressed`].
- StickyMouseButtons = crate::ffi::GLFW_STICKY_MOUSE_BUTTONS,
-
- LockKeyMods = crate::ffi::GLFW_LOCK_KEY_MODS,
-
- /// 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 = crate::ffi::GLFW_RAW_MOUSE_MOTION,
-}
-
-#[derive(Debug, Clone, Copy)]
-#[repr(i32)]
-pub enum CursorMode
-{
- /// Hides and grabs the cursor, providing virtual and unlimited cursor movement. This
- /// is useful for implementing for example 3D camera controls.
- Disabled = crate::ffi::GLFW_CURSOR_DISABLED,
-
- /// Makes the cursor invisible when it is over the content area of the window but
- /// does not restrict the cursor from leaving.
- Hidden = crate::ffi::GLFW_CURSOR_HIDDEN,
-
- /// Makes the cursor visible and behaving normally.
- Normal = crate::ffi::GLFW_CURSOR_NORMAL,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, FromRepr, VariantArr)]
-#[variant_arr(name = KEYS)]
-#[repr(i32)]
-pub enum Key
-{
- // Unknown = crate::ffi::GLFW_KEY_UNKNOWN,
- Space = crate::ffi::GLFW_KEY_SPACE,
- Apostrophe = crate::ffi::GLFW_KEY_APOSTROPHE,
- Comma = crate::ffi::GLFW_KEY_COMMA,
- Minus = crate::ffi::GLFW_KEY_MINUS,
- Period = crate::ffi::GLFW_KEY_PERIOD,
- Slash = crate::ffi::GLFW_KEY_SLASH,
- Digit0 = crate::ffi::GLFW_KEY_0,
- Digit1 = crate::ffi::GLFW_KEY_1,
- Digit2 = crate::ffi::GLFW_KEY_2,
- Digit3 = crate::ffi::GLFW_KEY_3,
- Digit4 = crate::ffi::GLFW_KEY_4,
- Digit5 = crate::ffi::GLFW_KEY_5,
- Digit6 = crate::ffi::GLFW_KEY_6,
- Digit7 = crate::ffi::GLFW_KEY_7,
- Digit8 = crate::ffi::GLFW_KEY_8,
- Digit9 = crate::ffi::GLFW_KEY_9,
- Semicolon = crate::ffi::GLFW_KEY_SEMICOLON,
- Equal = crate::ffi::GLFW_KEY_EQUAL,
- A = crate::ffi::GLFW_KEY_A,
- B = crate::ffi::GLFW_KEY_B,
- C = crate::ffi::GLFW_KEY_C,
- D = crate::ffi::GLFW_KEY_D,
- E = crate::ffi::GLFW_KEY_E,
- F = crate::ffi::GLFW_KEY_F,
- G = crate::ffi::GLFW_KEY_G,
- H = crate::ffi::GLFW_KEY_H,
- I = crate::ffi::GLFW_KEY_I,
- J = crate::ffi::GLFW_KEY_J,
- K = crate::ffi::GLFW_KEY_K,
- L = crate::ffi::GLFW_KEY_L,
- M = crate::ffi::GLFW_KEY_M,
- N = crate::ffi::GLFW_KEY_N,
- O = crate::ffi::GLFW_KEY_O,
- P = crate::ffi::GLFW_KEY_P,
- Q = crate::ffi::GLFW_KEY_Q,
- R = crate::ffi::GLFW_KEY_R,
- S = crate::ffi::GLFW_KEY_S,
- T = crate::ffi::GLFW_KEY_T,
- U = crate::ffi::GLFW_KEY_U,
- V = crate::ffi::GLFW_KEY_V,
- W = crate::ffi::GLFW_KEY_W,
- X = crate::ffi::GLFW_KEY_X,
- Y = crate::ffi::GLFW_KEY_Y,
- Z = crate::ffi::GLFW_KEY_Z,
- LeftBracket = crate::ffi::GLFW_KEY_LEFT_BRACKET,
- Backslash = crate::ffi::GLFW_KEY_BACKSLASH,
- RightBracket = crate::ffi::GLFW_KEY_RIGHT_BRACKET,
- GraveAccent = crate::ffi::GLFW_KEY_GRAVE_ACCENT,
- World1 = crate::ffi::GLFW_KEY_WORLD_1,
- World2 = crate::ffi::GLFW_KEY_WORLD_2,
- Escape = crate::ffi::GLFW_KEY_ESCAPE,
- Enter = crate::ffi::GLFW_KEY_ENTER,
- Tab = crate::ffi::GLFW_KEY_TAB,
- Backspace = crate::ffi::GLFW_KEY_BACKSPACE,
- Insert = crate::ffi::GLFW_KEY_INSERT,
- Delete = crate::ffi::GLFW_KEY_DELETE,
- Right = crate::ffi::GLFW_KEY_RIGHT,
- Left = crate::ffi::GLFW_KEY_LEFT,
- Down = crate::ffi::GLFW_KEY_DOWN,
- Up = crate::ffi::GLFW_KEY_UP,
- PageUp = crate::ffi::GLFW_KEY_PAGE_UP,
- PageDown = crate::ffi::GLFW_KEY_PAGE_DOWN,
- Home = crate::ffi::GLFW_KEY_HOME,
- End = crate::ffi::GLFW_KEY_END,
- CapsLock = crate::ffi::GLFW_KEY_CAPS_LOCK,
- ScrollLock = crate::ffi::GLFW_KEY_SCROLL_LOCK,
- NumLock = crate::ffi::GLFW_KEY_NUM_LOCK,
- PrintScreen = crate::ffi::GLFW_KEY_PRINT_SCREEN,
- Pause = crate::ffi::GLFW_KEY_PAUSE,
- F1 = crate::ffi::GLFW_KEY_F1,
- F2 = crate::ffi::GLFW_KEY_F2,
- F3 = crate::ffi::GLFW_KEY_F3,
- F4 = crate::ffi::GLFW_KEY_F4,
- F5 = crate::ffi::GLFW_KEY_F5,
- F6 = crate::ffi::GLFW_KEY_F6,
- F7 = crate::ffi::GLFW_KEY_F7,
- F8 = crate::ffi::GLFW_KEY_F8,
- F9 = crate::ffi::GLFW_KEY_F9,
- F10 = crate::ffi::GLFW_KEY_F10,
- F11 = crate::ffi::GLFW_KEY_F11,
- F12 = crate::ffi::GLFW_KEY_F12,
- F13 = crate::ffi::GLFW_KEY_F13,
- F14 = crate::ffi::GLFW_KEY_F14,
- F15 = crate::ffi::GLFW_KEY_F15,
- F16 = crate::ffi::GLFW_KEY_F16,
- F17 = crate::ffi::GLFW_KEY_F17,
- F18 = crate::ffi::GLFW_KEY_F18,
- F19 = crate::ffi::GLFW_KEY_F19,
- F20 = crate::ffi::GLFW_KEY_F20,
- F21 = crate::ffi::GLFW_KEY_F21,
- F22 = crate::ffi::GLFW_KEY_F22,
- F23 = crate::ffi::GLFW_KEY_F23,
- F24 = crate::ffi::GLFW_KEY_F24,
- F25 = crate::ffi::GLFW_KEY_F25,
- Kp0 = crate::ffi::GLFW_KEY_KP_0,
- Kp1 = crate::ffi::GLFW_KEY_KP_1,
- Kp2 = crate::ffi::GLFW_KEY_KP_2,
- Kp3 = crate::ffi::GLFW_KEY_KP_3,
- Kp4 = crate::ffi::GLFW_KEY_KP_4,
- Kp5 = crate::ffi::GLFW_KEY_KP_5,
- Kp6 = crate::ffi::GLFW_KEY_KP_6,
- Kp7 = crate::ffi::GLFW_KEY_KP_7,
- Kp8 = crate::ffi::GLFW_KEY_KP_8,
- Kp9 = crate::ffi::GLFW_KEY_KP_9,
- KpDecimal = crate::ffi::GLFW_KEY_KP_DECIMAL,
- KpDivide = crate::ffi::GLFW_KEY_KP_DIVIDE,
- KpMultiply = crate::ffi::GLFW_KEY_KP_MULTIPLY,
- KpSubtract = crate::ffi::GLFW_KEY_KP_SUBTRACT,
- KpAdd = crate::ffi::GLFW_KEY_KP_ADD,
- KpEnter = crate::ffi::GLFW_KEY_KP_ENTER,
- KpEqual = crate::ffi::GLFW_KEY_KP_EQUAL,
- LeftShift = crate::ffi::GLFW_KEY_LEFT_SHIFT,
- LeftControl = crate::ffi::GLFW_KEY_LEFT_CONTROL,
- LeftAlt = crate::ffi::GLFW_KEY_LEFT_ALT,
- LeftSuper = crate::ffi::GLFW_KEY_LEFT_SUPER,
- RightShift = crate::ffi::GLFW_KEY_RIGHT_SHIFT,
- RightControl = crate::ffi::GLFW_KEY_RIGHT_CONTROL,
- RightAlt = crate::ffi::GLFW_KEY_RIGHT_ALT,
- RightSuper = crate::ffi::GLFW_KEY_RIGHT_SUPER,
- Menu = crate::ffi::GLFW_KEY_MENU,
-}
-
-#[derive(Debug, Clone, Copy, FromRepr)]
-#[repr(i32)]
-pub enum KeyState
-{
- Pressed = crate::ffi::GLFW_PRESS,
- Released = crate::ffi::GLFW_RELEASE,
- Repeat = crate::ffi::GLFW_REPEAT,
-}
-
-bitflags! {
- #[derive(Debug, Clone, Copy)]
- pub struct KeyModifiers: i32 {
- const SHIFT = crate::ffi::GLFW_MOD_SHIFT;
- const CONTROL = crate::ffi::GLFW_MOD_CONTROL;
- const ALT = crate::ffi::GLFW_MOD_ALT;
- const SUPER = crate::ffi::GLFW_MOD_SUPER;
- const CAPS_LOCK = crate::ffi::GLFW_MOD_CAPS_LOCK;
- const NUM_LOCK = crate::ffi::GLFW_MOD_NUM_LOCK;
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, FromRepr)]
-#[repr(i32)]
-pub enum MouseButton
-{
- One = crate::ffi::GLFW_MOUSE_BUTTON_1,
- Two = crate::ffi::GLFW_MOUSE_BUTTON_2,
- Three = crate::ffi::GLFW_MOUSE_BUTTON_3,
- Four = crate::ffi::GLFW_MOUSE_BUTTON_4,
- Five = crate::ffi::GLFW_MOUSE_BUTTON_5,
- Six = crate::ffi::GLFW_MOUSE_BUTTON_6,
- Seven = crate::ffi::GLFW_MOUSE_BUTTON_7,
- Eight = crate::ffi::GLFW_MOUSE_BUTTON_8,
-}
-
-impl MouseButton
-{
- pub const LEFT: Self = Self::One;
- pub const MIDDLE: Self = Self::Three;
- pub const RIGHT: Self = Self::Two;
-}
-
-#[derive(Debug, Clone, Copy, FromRepr)]
-#[repr(i32)]
-pub enum MouseButtonState
-{
- Pressed = crate::ffi::GLFW_PRESS,
- Released = crate::ffi::GLFW_RELEASE,
-}
-
-#[derive(Debug, Clone)]
-pub struct CursorPosition
-{
- pub x: f64,
- pub y: f64,
-}
-
-type CloseCallback = Box<dyn Fn()>;
-type FramebufferSizeCb = Box<dyn Fn(Size)>;
-type KeyCallback = Box<dyn Fn(Key, i32, KeyState, KeyModifiers)>;
-type CursorPositionCallback = Box<dyn Fn(CursorPosition)>;
-type MouseButtonCallback = Box<dyn Fn(MouseButton, MouseButtonState, KeyModifiers)>;
-type FocusCallback = Box<dyn Fn(bool)>;
-
-thread_local! {
-static CLOSE_CALLBACK: RefCell<Option<CloseCallback>> = RefCell::new(None);
-static FRAMEBUFFER_SIZE_CB: RefCell<Option<FramebufferSizeCb>> = RefCell::new(None);
-static KEY_CALLBACK: RefCell<Option<KeyCallback>> = RefCell::new(None);
-static CURSOR_POS_CALLBACK: RefCell<Option<CursorPositionCallback>> = RefCell::new(None);
-static MOUSE_BUTTON_CALLBACK: RefCell<Option<MouseButtonCallback>> = RefCell::new(None);
-static FOCUS_CALLBACK: RefCell<Option<FocusCallback>> = RefCell::new(None);
-}
-
-extern "C-unwind" fn close_callback(_window: *mut crate::ffi::GLFWwindow)
-{
- CLOSE_CALLBACK
- .try_with(|close_cb| {
- if let Some(cb) = close_cb.borrow().as_deref() {
- cb();
- }
- })
- .ok();
-}
-
-extern "C-unwind" fn framebuffer_size_callback(
- _window: *mut crate::ffi::GLFWwindow,
- c_width: c_int,
- c_height: c_int,
-)
-{
- // Width and height can't possibly have their sign bit set
- let width = u32::from_le_bytes(c_width.to_le_bytes());
- let height = u32::from_le_bytes(c_height.to_le_bytes());
-
- FRAMEBUFFER_SIZE_CB
- .try_with(|framebuffer_size_cb| {
- if let Some(cb) = framebuffer_size_cb.borrow().as_deref() {
- cb(Size { width, height });
- }
- })
- .ok();
-}
-
-extern "C-unwind" fn key_callback(
- _window: *mut crate::ffi::GLFWwindow,
- key_raw: c_int,
- scancode: c_int,
- action_raw: c_int,
- mods: c_int,
-)
-{
- let Some(key) = Key::from_repr(key_raw) else {
- write!(stdout(), "Unknown key {key_raw}").ok();
- return;
- };
-
- let Some(key_state) = KeyState::from_repr(action_raw) else {
- write!(stdout(), "Unknown key state {action_raw}").ok();
- return;
- };
-
- let Some(key_modifiers) = KeyModifiers::from_bits(mods) else {
- write!(
- stdout(),
- "Key modifiers {action_raw} contain one or more unknown bit(s)"
- )
- .ok();
-
- return;
- };
-
- KEY_CALLBACK
- .try_with(|key_callback| {
- if let Some(cb) = key_callback.borrow().as_deref() {
- cb(key, scancode, key_state, key_modifiers);
- }
- })
- .ok();
-}
-
-extern "C-unwind" fn cursor_pos_callback(
- _window: *mut crate::ffi::GLFWwindow,
- x_pos: c_double,
- y_pos: c_double,
-)
-{
- CURSOR_POS_CALLBACK
- .try_with(|cursor_pos_callback| {
- if let Some(cb) = cursor_pos_callback.borrow().as_deref() {
- cb(CursorPosition { x: x_pos, y: y_pos });
- }
- })
- .ok();
-}
-
-extern "C-unwind" fn mouse_button_callback(
- _window: *mut crate::ffi::GLFWwindow,
- mouse_button_raw: c_int,
- mouse_action: c_int,
- modifier_keys: c_int,
-)
-{
- let Some(mouse_button) = MouseButton::from_repr(mouse_button_raw) else {
- write!(stdout(), "Unknown mouse button {mouse_button_raw}").ok();
- return;
- };
-
- let Some(mouse_button_state) = MouseButtonState::from_repr(mouse_action) else {
- write!(stdout(), "Unknown mouse action {mouse_action}").ok();
- return;
- };
-
- let Some(key_modifiers) = KeyModifiers::from_bits(modifier_keys) else {
- write!(
- stdout(),
- "Key modifiers {modifier_keys:#b} contain one or more unknown bit(s)"
- )
- .ok();
-
- return;
- };
-
- MOUSE_BUTTON_CALLBACK
- .try_with(|mouse_button_callback| {
- if let Some(cb) = mouse_button_callback.borrow().as_deref() {
- cb(mouse_button, mouse_button_state, key_modifiers);
- }
- })
- .ok();
-}
-
-extern "C-unwind" fn focus_callback(_window: *mut crate::ffi::GLFWwindow, focused: c_int)
-{
- let is_focused = if focused == crate::ffi::GLFW_TRUE {
- true
- } else if focused == crate::ffi::GLFW_FALSE {
- false
- } else {
- write!(stdout(), "Invalid focus state {focused}").ok();
- return;
- };
-
- FOCUS_CALLBACK
- .try_with(|focus_callback| {
- if let Some(cb) = focus_callback.borrow().as_deref() {
- cb(is_focused);
- }
- })
- .ok();
-}
diff --git a/opengl-bindings/Cargo.toml b/opengl-bindings/Cargo.toml
new file mode 100644
index 0000000..8251642
--- /dev/null
+++ b/opengl-bindings/Cargo.toml
@@ -0,0 +1,65 @@
+[package]
+name = "opengl-bindings"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+glutin = "0.32.3"
+thiserror = "1.0.49"
+safer-ffi = "0.1.13"
+bitflags = "2.4.0"
+util-macros = { path = "../util-macros" }
+
+[build-dependencies]
+gl_generator = "=0.14.0"
+toml = "0.8.12"
+anyhow = "1.0.100"
+
+[package.metadata.build]
+gl_commands = [
+ "CreateBuffers",
+ "NamedBufferData",
+ "NamedBufferSubData",
+ "CreateVertexArrays",
+ "DrawArrays",
+ "DrawElements",
+ "VertexArrayElementBuffer",
+ "VertexArrayVertexBuffer",
+ "EnableVertexArrayAttrib",
+ "VertexArrayAttribFormat",
+ "VertexArrayAttribBinding",
+ "BindVertexArray",
+ "TextureStorage2D",
+ "TextureSubImage2D",
+ "DeleteTextures",
+ "GenerateTextureMipmap",
+ "TextureParameteri",
+ "CreateTextures",
+ "BindTextureUnit",
+ "DeleteShader",
+ "CreateShader",
+ "ShaderSource",
+ "CompileShader",
+ "GetShaderiv",
+ "GetShaderInfoLog",
+ "LinkProgram",
+ "GetProgramiv",
+ "CreateProgram",
+ "AttachShader",
+ "UseProgram",
+ "GetUniformLocation",
+ "ProgramUniform1f",
+ "ProgramUniform1i",
+ "ProgramUniform3f",
+ "ProgramUniformMatrix4fv",
+ "GetProgramInfoLog",
+ "DeleteProgram",
+ "Viewport",
+ "Clear",
+ "PolygonMode",
+ "Enable",
+ "Disable",
+ "GetIntegerv",
+ "DebugMessageCallback",
+ "DebugMessageControl"
+]
diff --git a/opengl-bindings/build.rs b/opengl-bindings/build.rs
new file mode 100644
index 0000000..060472c
--- /dev/null
+++ b/opengl-bindings/build.rs
@@ -0,0 +1,107 @@
+use std::collections::HashSet;
+use std::env;
+use std::fs::File;
+use std::path::{Path, PathBuf};
+
+use anyhow::anyhow;
+use gl_generator::{Api, Fallbacks, Profile, Registry, StructGenerator};
+
+fn main() -> Result<(), anyhow::Error>
+{
+ println!("cargo::rerun-if-changed=build.rs");
+ println!("cargo::rerun-if-changed=Cargo.toml");
+
+ let dest = env::var("OUT_DIR")?;
+
+ let mut file = File::create(Path::new(&dest).join("bindings.rs"))?;
+
+ let mut registry = Registry::new(Api::Gl, (4, 6), Profile::Core, Fallbacks::All, []);
+
+ let mut build_metadata = get_build_metadata()?;
+
+ filter_gl_commands(&mut registry, &mut build_metadata)?;
+
+ registry.write_bindings(StructGenerator, &mut file)?;
+
+ Ok(())
+}
+
+fn filter_gl_commands(
+ registry: &mut Registry,
+ build_metadata: &mut BuildMetadata,
+) -> Result<(), anyhow::Error>
+{
+ registry
+ .cmds
+ .retain(|command| build_metadata.gl_commands.remove(&command.proto.ident));
+
+ if !build_metadata.gl_commands.is_empty() {
+ return Err(anyhow!(
+ "Invalid GL commands: [{}]",
+ build_metadata
+ .gl_commands
+ .iter()
+ .cloned()
+ .collect::<Vec<_>>()
+ .join(", ")
+ ));
+ }
+
+ Ok(())
+}
+
+fn get_build_metadata() -> Result<BuildMetadata, anyhow::Error>
+{
+ let manifest_path = PathBuf::from(std::env::var("CARGO_MANIFEST_PATH")?);
+
+ let manifest = std::fs::read_to_string(manifest_path)?.parse::<toml::Table>()?;
+
+ let package = match manifest
+ .get("package")
+ .ok_or_else(|| anyhow!("Manifest does not have a package table"))?
+ {
+ toml::Value::Table(package) => Ok(package),
+ _ => Err(anyhow!("Manifest package must be a table")),
+ }?;
+
+ let metadata = match package
+ .get("metadata")
+ .ok_or_else(|| anyhow!("Manifest does not have a package.metadata table"))?
+ {
+ toml::Value::Table(metadata) => Ok(metadata),
+ _ => Err(anyhow!("Manifest package.metadata must be a table")),
+ }?;
+
+ let build_metadata = match metadata
+ .get("build")
+ .ok_or_else(|| anyhow!("Manifest does not have a package.metadata.build table"))?
+ {
+ toml::Value::Table(build_metadata) => Ok(build_metadata),
+ _ => Err(anyhow!("Manifest package.metadata.build must be a table")),
+ }?;
+
+ let gl_command_values = match build_metadata.get("gl_commands").ok_or_else(|| {
+ anyhow!("Manifest does not have a package.metadata.build.gl_commands array")
+ })? {
+ toml::Value::Array(gl_commands) => Ok(gl_commands),
+ _ => Err(anyhow!(
+ "Manifest package.metadata.build.gl_commands must be a array"
+ )),
+ }?;
+
+ let gl_commands = gl_command_values
+ .iter()
+ .map(|gl_command_val| match gl_command_val {
+ toml::Value::String(gl_command) => Ok(gl_command.clone()),
+ _ => Err(anyhow!("GL command must be a string")),
+ })
+ .collect::<Result<HashSet<_>, _>>()?;
+
+ Ok(BuildMetadata { gl_commands })
+}
+
+#[derive(Debug)]
+struct BuildMetadata
+{
+ gl_commands: HashSet<String>,
+}
diff --git a/opengl-bindings/src/buffer.rs b/opengl-bindings/src/buffer.rs
new file mode 100644
index 0000000..c64ec8d
--- /dev/null
+++ b/opengl-bindings/src/buffer.rs
@@ -0,0 +1,167 @@
+use std::marker::PhantomData;
+use std::mem::size_of_val;
+use std::ptr::null;
+
+use safer_ffi::layout::ReprC;
+
+use crate::CurrentContextWithFns;
+
+#[derive(Debug)]
+pub struct Buffer<Item: ReprC>
+{
+ buf: crate::sys::types::GLuint,
+ _pd: PhantomData<Item>,
+}
+
+impl<Item: ReprC> Buffer<Item>
+{
+ #[must_use]
+ pub fn new(current_context: &CurrentContextWithFns<'_>) -> Self
+ {
+ let mut buffer = crate::sys::types::GLuint::default();
+
+ unsafe {
+ current_context.fns().CreateBuffers(1, &raw mut buffer);
+ };
+
+ Self { buf: buffer, _pd: PhantomData }
+ }
+
+ /// Stores items in this buffer.
+ ///
+ /// # Errors
+ /// Returns `Err` if the total size (in bytes) is too large.
+ pub fn store(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ items: &[Item],
+ usage: Usage,
+ ) -> Result<(), Error>
+ {
+ let total_size = size_of_val(items);
+
+ let total_size: crate::sys::types::GLsizeiptr =
+ total_size
+ .try_into()
+ .map_err(|_| Error::TotalItemsSizeTooLarge {
+ total_size,
+ max_total_size: crate::sys::types::GLsizeiptr::MAX as usize,
+ })?;
+
+ unsafe {
+ current_context.fns().NamedBufferData(
+ self.buf,
+ total_size,
+ items.as_ptr().cast(),
+ usage.into_gl(),
+ );
+ }
+
+ Ok(())
+ }
+
+ /// Maps the values in the `values` slice into `Item`s which is stored into this
+ /// buffer.
+ ///
+ /// # Errors
+ /// Returns `Err` if the total size (in bytes) is too large.
+ pub fn store_mapped<Value>(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ values: &[Value],
+ usage: Usage,
+ mut map_func: impl FnMut(&Value) -> Item,
+ ) -> Result<(), Error>
+ {
+ let item_size: crate::sys::types::GLsizeiptr = const {
+ assert!(size_of::<Item>() <= crate::sys::types::GLsizeiptr::MAX as usize);
+
+ size_of::<Item>().cast_signed()
+ };
+
+ let total_size = size_of::<Item>() * values.len();
+
+ let total_size: crate::sys::types::GLsizeiptr =
+ total_size
+ .try_into()
+ .map_err(|_| Error::TotalItemsSizeTooLarge {
+ total_size,
+ max_total_size: crate::sys::types::GLsizeiptr::MAX as usize,
+ })?;
+
+ unsafe {
+ current_context.fns().NamedBufferData(
+ self.buf,
+ total_size,
+ null(),
+ usage.into_gl(),
+ );
+ }
+
+ for (index, value) in values.iter().enumerate() {
+ let item = map_func(value);
+
+ let offset = index * size_of::<Item>();
+
+ let Ok(offset_casted) = crate::sys::types::GLintptr::try_from(offset) else {
+ unreachable!(); // Reason: The total size can be casted to a GLintptr
+ // (done above) so offsets should be castable as well
+ };
+
+ unsafe {
+ current_context.fns().NamedBufferSubData(
+ self.buf,
+ offset_casted,
+ item_size,
+ (&raw const item).cast(),
+ );
+ }
+ }
+
+ Ok(())
+ }
+
+ pub(crate) fn object(&self) -> crate::sys::types::GLuint
+ {
+ self.buf
+ }
+}
+
+/// Buffer usage.
+#[derive(Debug)]
+pub enum Usage
+{
+ /// The buffer data is set only once and used by the GPU at most a few times.
+ Stream,
+
+ /// The buffer data is set only once and used many times.
+ Static,
+
+ /// The buffer data is changed a lot and used many times.
+ Dynamic,
+}
+
+impl Usage
+{
+ fn into_gl(self) -> crate::sys::types::GLenum
+ {
+ match self {
+ Self::Stream => crate::sys::STREAM_DRAW,
+ Self::Static => crate::sys::STATIC_DRAW,
+ Self::Dynamic => crate::sys::DYNAMIC_DRAW,
+ }
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error
+{
+ #[error(
+ "Total size of items ({total_size}) is too large. Must be < {max_total_size}"
+ )]
+ TotalItemsSizeTooLarge
+ {
+ total_size: usize,
+ max_total_size: usize,
+ },
+}
diff --git a/opengl-bindings/src/data_types.rs b/opengl-bindings/src/data_types.rs
new file mode 100644
index 0000000..7ead0ab
--- /dev/null
+++ b/opengl-bindings/src/data_types.rs
@@ -0,0 +1,37 @@
+use safer_ffi::derive_ReprC;
+use safer_ffi::layout::ReprC;
+
+#[derive(Debug, Clone)]
+#[derive_ReprC]
+#[repr(C)]
+pub struct Matrix<Value: ReprC, const ROWS: usize, const COLUMNS: usize>
+{
+ /// Items must be layed out this way for it to work with OpenGL shaders.
+ pub items: [[Value; ROWS]; COLUMNS],
+}
+
+#[derive(Debug, Clone)]
+#[derive_ReprC]
+#[repr(C)]
+pub struct Vec3<Value: ReprC>
+{
+ pub x: Value,
+ pub y: Value,
+ pub z: Value,
+}
+
+#[derive(Debug, Clone)]
+#[derive_ReprC]
+#[repr(C)]
+pub struct Vec2<Value>
+{
+ pub x: Value,
+ pub y: Value,
+}
+
+#[derive(Debug, Clone)]
+pub struct Dimens<Value>
+{
+ pub width: Value,
+ pub height: Value,
+}
diff --git a/opengl-bindings/src/debug.rs b/opengl-bindings/src/debug.rs
new file mode 100644
index 0000000..a9369a4
--- /dev/null
+++ b/opengl-bindings/src/debug.rs
@@ -0,0 +1,161 @@
+use std::ffi::c_void;
+use std::io::{stderr, Write};
+use std::mem::transmute;
+use std::panic::catch_unwind;
+
+use util_macros::FromRepr;
+
+use crate::CurrentContextWithFns;
+
+pub fn set_debug_message_callback(
+ current_context: &CurrentContextWithFns<'_>,
+ cb: MessageCallback,
+)
+{
+ unsafe {
+ current_context
+ .fns()
+ .DebugMessageCallback(Some(debug_message_cb), cb as *mut c_void);
+ }
+}
+
+/// Sets debug message parameters.
+///
+/// # Errors
+/// Returns `Err` if `ids` contains too many ids.
+pub fn set_debug_message_control(
+ current_context: &CurrentContextWithFns<'_>,
+ source: Option<MessageSource>,
+ ty: Option<MessageType>,
+ severity: Option<MessageSeverity>,
+ ids: &[u32],
+ ids_action: MessageIdsAction,
+) -> Result<(), SetDebugMessageControlError>
+{
+ let ids_len: crate::sys::types::GLsizei =
+ ids.len()
+ .try_into()
+ .map_err(|_| SetDebugMessageControlError::TooManyIds {
+ id_cnt: ids.len(),
+ max_id_cnt: crate::sys::types::GLsizei::MAX as usize,
+ })?;
+
+ unsafe {
+ current_context.fns().DebugMessageControl(
+ source.map_or(crate::sys::DONT_CARE, |source| source as u32),
+ ty.map_or(crate::sys::DONT_CARE, |ty| ty as u32),
+ severity.map_or(crate::sys::DONT_CARE, |severity| severity as u32),
+ ids_len,
+ ids.as_ptr(),
+ ids_action as u8,
+ );
+ }
+
+ Ok(())
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum SetDebugMessageControlError
+{
+ #[error("Too many ids provided ({id_cnt}). Must be < {max_id_cnt}")]
+ TooManyIds
+ {
+ id_cnt: usize, max_id_cnt: usize
+ },
+}
+
+pub type MessageCallback = fn(
+ source: MessageSource,
+ ty: MessageType,
+ id: u32,
+ severity: MessageSeverity,
+ message: &str,
+);
+
+#[derive(Debug, Clone, Copy)]
+#[repr(u8)] // GLboolean = u8
+pub enum MessageIdsAction
+{
+ Enable = crate::sys::TRUE,
+ Disable = crate::sys::FALSE,
+}
+
+#[derive(Debug, Clone, Copy, FromRepr)]
+#[repr(u32)] // GLenum = u32
+pub enum MessageSource
+{
+ Api = crate::sys::DEBUG_SOURCE_API,
+ WindowSystem = crate::sys::DEBUG_SOURCE_WINDOW_SYSTEM,
+ ShaderCompiler = crate::sys::DEBUG_SOURCE_SHADER_COMPILER,
+ ThirdParty = crate::sys::DEBUG_SOURCE_THIRD_PARTY,
+ Application = crate::sys::DEBUG_SOURCE_APPLICATION,
+ Other = crate::sys::DEBUG_SOURCE_OTHER,
+}
+
+#[derive(Debug, Clone, Copy, FromRepr)]
+#[repr(u32)] // GLenum = u32
+pub enum MessageType
+{
+ DeprecatedBehavior = crate::sys::DEBUG_TYPE_DEPRECATED_BEHAVIOR,
+ Error = crate::sys::DEBUG_TYPE_ERROR,
+ Marker = crate::sys::DEBUG_TYPE_MARKER,
+ Other = crate::sys::DEBUG_TYPE_OTHER,
+ Performance = crate::sys::DEBUG_TYPE_PERFORMANCE,
+ PopGroup = crate::sys::DEBUG_TYPE_POP_GROUP,
+ PushGroup = crate::sys::DEBUG_TYPE_PUSH_GROUP,
+ Portability = crate::sys::DEBUG_TYPE_PORTABILITY,
+ UndefinedBehavior = crate::sys::DEBUG_TYPE_UNDEFINED_BEHAVIOR,
+}
+
+#[derive(Debug, Clone, Copy, FromRepr)]
+#[repr(u32)] // GLenum = u32
+pub enum MessageSeverity
+{
+ High = crate::sys::DEBUG_SEVERITY_HIGH,
+ Medium = crate::sys::DEBUG_SEVERITY_MEDIUM,
+ Low = crate::sys::DEBUG_SEVERITY_LOW,
+ Notification = crate::sys::DEBUG_SEVERITY_NOTIFICATION,
+}
+
+extern "system" fn debug_message_cb(
+ source: crate::sys::types::GLenum,
+ ty: crate::sys::types::GLenum,
+ id: crate::sys::types::GLuint,
+ severity: crate::sys::types::GLenum,
+ message_length: crate::sys::types::GLsizei,
+ message: *const crate::sys::types::GLchar,
+ user_cb: *mut c_void,
+)
+{
+ let user_cb = unsafe { transmute::<*mut c_void, MessageCallback>(user_cb) };
+
+ let Ok(msg_length) = usize::try_from(message_length) else {
+ return;
+ };
+
+ // Unwinds are catched because unwinding from Rust code into foreign code is UB.
+ let res = catch_unwind(|| {
+ let msg_source = MessageSource::from_repr(source).unwrap();
+ let msg_type = MessageType::from_repr(ty).unwrap();
+ let msg_severity = MessageSeverity::from_repr(severity).unwrap();
+
+ // SAFETY: The received message should be a valid ASCII string
+ let message = unsafe {
+ std::str::from_utf8_unchecked(std::slice::from_raw_parts(
+ message.cast(),
+ msg_length,
+ ))
+ };
+
+ user_cb(msg_source, msg_type, id, msg_severity, message);
+ });
+
+ if res.is_err() {
+ // eprintln is not used since it can panic and unwinds are unwanted because
+ // unwinding from Rust code into foreign code is UB.
+ stderr()
+ .write_all(b"ERROR: Panic in debug message callback")
+ .ok();
+ println!();
+ }
+}
diff --git a/opengl-bindings/src/lib.rs b/opengl-bindings/src/lib.rs
new file mode 100644
index 0000000..7fd9933
--- /dev/null
+++ b/opengl-bindings/src/lib.rs
@@ -0,0 +1,119 @@
+#![deny(clippy::all, clippy::pedantic)]
+use std::ffi::CString;
+use std::process::abort;
+
+use glutin::context::{NotCurrentContext, PossiblyCurrentContext};
+use glutin::display::GetGlDisplay;
+use glutin::prelude::{GlDisplay, NotCurrentGlContext, PossiblyCurrentGlContext};
+use glutin::surface::{Surface, SurfaceTypeTrait};
+
+pub mod buffer;
+pub mod data_types;
+pub mod debug;
+pub mod misc;
+pub mod shader;
+pub mod texture;
+pub mod vertex_array;
+
+pub struct ContextWithFns
+{
+ context: PossiblyCurrentContext,
+ fns: Box<sys::Gl>,
+}
+
+impl ContextWithFns
+{
+ /// Returns a new `ContextWithFns`.
+ ///
+ /// # Errors
+ /// Returns `Err` if making this context current fails.
+ pub fn new<SurfaceType: SurfaceTypeTrait>(
+ context: NotCurrentContext,
+ surface: &Surface<SurfaceType>,
+ ) -> Result<Self, Error>
+ {
+ let context = context
+ .make_current(surface)
+ .map_err(Error::MakeContextCurrentFailed)?;
+
+ let display = context.display();
+
+ let gl = sys::Gl::load_with(|symbol| {
+ let Ok(symbol) = CString::new(symbol) else {
+ eprintln!("GL symbol contains nul byte");
+ abort();
+ };
+
+ display.get_proc_address(&symbol)
+ });
+
+ Ok(Self { context, fns: Box::new(gl) })
+ }
+
+ /// Attempts to make this context current.
+ ///
+ /// # Errors
+ /// Returns `Err` if making this context current fails.
+ pub fn make_current<SurfaceType: SurfaceTypeTrait>(
+ &self,
+ surface: &Surface<SurfaceType>,
+ ) -> Result<CurrentContextWithFns<'_>, Error>
+ {
+ if !self.context.is_current() {
+ self.context
+ .make_current(surface)
+ .map_err(Error::MakeContextCurrentFailed)?;
+ }
+
+ Ok(CurrentContextWithFns { ctx: self })
+ }
+
+ #[must_use]
+ pub fn context(&self) -> &PossiblyCurrentContext
+ {
+ &self.context
+ }
+
+ #[must_use]
+ pub fn context_mut(&mut self) -> &mut PossiblyCurrentContext
+ {
+ &mut self.context
+ }
+}
+
+pub struct CurrentContextWithFns<'ctx>
+{
+ ctx: &'ctx ContextWithFns,
+}
+
+impl CurrentContextWithFns<'_>
+{
+ #[inline]
+ pub(crate) fn fns(&self) -> &sys::Gl
+ {
+ &self.ctx.fns
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error
+{
+ #[error("Failed to make context current")]
+ MakeContextCurrentFailed(#[source] glutin::error::Error),
+}
+
+mod sys
+{
+ #![allow(
+ clippy::missing_safety_doc,
+ clippy::missing_transmute_annotations,
+ clippy::too_many_arguments,
+ clippy::unused_unit,
+ clippy::upper_case_acronyms,
+ clippy::doc_markdown,
+ clippy::unreadable_literal,
+ unsafe_op_in_unsafe_fn
+ )]
+
+ include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
+}
diff --git a/opengl-bindings/src/misc.rs b/opengl-bindings/src/misc.rs
new file mode 100644
index 0000000..bb54c1a
--- /dev/null
+++ b/opengl-bindings/src/misc.rs
@@ -0,0 +1,190 @@
+use bitflags::bitflags;
+
+use crate::data_types::{Dimens, Vec2};
+use crate::CurrentContextWithFns;
+
+/// Sets the viewport.
+///
+/// The `u32` values in `position` and `size` must fit in `i32`s.
+///
+/// # Errors
+/// Returns `Err` if any value in `position` or `size` does not fit into a `i32`.
+pub fn set_viewport(
+ current_context: &CurrentContextWithFns<'_>,
+ position: &Vec2<u32>,
+ size: &Dimens<u32>,
+) -> Result<(), SetViewportError>
+{
+ let position = Vec2::<crate::sys::types::GLint> {
+ x: position.x.try_into().map_err(|_| {
+ SetViewportError::PositionXValueTooLarge {
+ value: position.x,
+ max_value: crate::sys::types::GLint::MAX as u32,
+ }
+ })?,
+ y: position.y.try_into().map_err(|_| {
+ SetViewportError::PositionYValueTooLarge {
+ value: position.y,
+ max_value: crate::sys::types::GLint::MAX as u32,
+ }
+ })?,
+ };
+
+ let size = Dimens::<crate::sys::types::GLsizei> {
+ width: size.width.try_into().map_err(|_| {
+ SetViewportError::SizeWidthValueTooLarge {
+ value: size.width,
+ max_value: crate::sys::types::GLsizei::MAX as u32,
+ }
+ })?,
+ height: size.height.try_into().map_err(|_| {
+ SetViewportError::SizeHeightValueTooLarge {
+ value: size.height,
+ max_value: crate::sys::types::GLsizei::MAX as u32,
+ }
+ })?,
+ };
+
+ unsafe {
+ current_context
+ .fns()
+ .Viewport(position.x, position.y, size.width, size.height);
+ }
+
+ Ok(())
+}
+
+pub fn clear_buffers(current_context: &CurrentContextWithFns<'_>, mask: BufferClearMask)
+{
+ unsafe {
+ current_context.fns().Clear(mask.bits());
+ }
+}
+
+pub fn set_polygon_mode(
+ current_context: &CurrentContextWithFns<'_>,
+ face: impl Into<PolygonModeFace>,
+ mode: impl Into<PolygonMode>,
+)
+{
+ unsafe {
+ current_context
+ .fns()
+ .PolygonMode(face.into() as u32, mode.into() as u32);
+ }
+}
+
+pub fn enable(current_context: &CurrentContextWithFns<'_>, capacity: Capability)
+{
+ unsafe {
+ current_context.fns().Enable(capacity as u32);
+ }
+}
+
+pub fn disable(current_context: &CurrentContextWithFns<'_>, capability: Capability)
+{
+ unsafe {
+ current_context.fns().Disable(capability as u32);
+ }
+}
+
+pub fn set_enabled(
+ current_context: &CurrentContextWithFns<'_>,
+ capability: Capability,
+ enabled: bool,
+)
+{
+ if enabled {
+ enable(current_context, capability);
+ } else {
+ disable(current_context, capability);
+ }
+}
+
+#[must_use]
+pub fn get_context_flags(current_context: &CurrentContextWithFns<'_>) -> ContextFlags
+{
+ let mut context_flags = crate::sys::types::GLint::default();
+
+ unsafe {
+ current_context
+ .fns()
+ .GetIntegerv(crate::sys::CONTEXT_FLAGS, &raw mut context_flags);
+ }
+
+ ContextFlags::from_bits_truncate(context_flags.cast_unsigned())
+}
+
+bitflags! {
+ #[derive(Debug, Clone, Copy)]
+ pub struct BufferClearMask: u32 {
+ const COLOR = crate::sys::COLOR_BUFFER_BIT;
+ const DEPTH = crate::sys::DEPTH_BUFFER_BIT;
+ const STENCIL = crate::sys::STENCIL_BUFFER_BIT;
+ }
+}
+
+#[derive(Debug)]
+#[repr(u32)]
+pub enum Capability
+{
+ DepthTest = crate::sys::DEPTH_TEST,
+ MultiSample = crate::sys::MULTISAMPLE,
+ DebugOutput = crate::sys::DEBUG_OUTPUT,
+ DebugOutputSynchronous = crate::sys::DEBUG_OUTPUT_SYNCHRONOUS,
+}
+
+#[derive(Debug)]
+#[repr(u32)]
+pub enum PolygonMode
+{
+ Point = crate::sys::POINT,
+ Line = crate::sys::LINE,
+ Fill = crate::sys::FILL,
+}
+
+#[derive(Debug)]
+#[repr(u32)]
+pub enum PolygonModeFace
+{
+ Front = crate::sys::FRONT,
+ Back = crate::sys::BACK,
+ FrontAndBack = crate::sys::FRONT_AND_BACK,
+}
+
+bitflags! {
+#[derive(Debug, Clone, Copy)]
+pub struct ContextFlags: u32 {
+ const FORWARD_COMPATIBLE = crate::sys::CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT;
+ const DEBUG = crate::sys::CONTEXT_FLAG_DEBUG_BIT;
+ const ROBUST_ACCESS = crate::sys::CONTEXT_FLAG_ROBUST_ACCESS_BIT;
+}
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum SetViewportError
+{
+ #[error("Position X value ({value}) is too large. Must be < {max_value}")]
+ PositionXValueTooLarge
+ {
+ value: u32, max_value: u32
+ },
+
+ #[error("Position Y value ({value}) is too large. Must be < {max_value}")]
+ PositionYValueTooLarge
+ {
+ value: u32, max_value: u32
+ },
+
+ #[error("Size width value ({value}) is too large. Must be < {max_value}")]
+ SizeWidthValueTooLarge
+ {
+ value: u32, max_value: u32
+ },
+
+ #[error("Size height value ({value}) is too large. Must be < {max_value}")]
+ SizeHeightValueTooLarge
+ {
+ value: u32, max_value: u32
+ },
+}
diff --git a/opengl-bindings/src/shader.rs b/opengl-bindings/src/shader.rs
new file mode 100644
index 0000000..5ed66a2
--- /dev/null
+++ b/opengl-bindings/src/shader.rs
@@ -0,0 +1,366 @@
+use std::ffi::CStr;
+use std::ptr::null_mut;
+
+use safer_ffi::layout::ReprC;
+
+use crate::data_types::{Matrix, Vec3};
+use crate::CurrentContextWithFns;
+
+#[derive(Debug)]
+pub struct Shader
+{
+ shader: crate::sys::types::GLuint,
+}
+
+impl Shader
+{
+ #[must_use]
+ pub fn new(current_context: &CurrentContextWithFns<'_>, kind: Kind) -> Self
+ {
+ let shader = unsafe {
+ current_context
+ .fns()
+ .CreateShader(kind as crate::sys::types::GLenum)
+ };
+
+ Self { shader }
+ }
+
+ /// Sets the source code of this shader.
+ ///
+ /// # Errors
+ /// Returns `Err` if `source` is not ASCII.
+ pub fn set_source(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ source: &str,
+ ) -> Result<(), Error>
+ {
+ if !source.is_ascii() {
+ return Err(Error::SourceNotAscii);
+ }
+
+ let length: crate::sys::types::GLint =
+ source.len().try_into().map_err(|_| Error::SourceTooLarge {
+ length: source.len(),
+ max_length: crate::sys::types::GLint::MAX as usize,
+ })?;
+
+ unsafe {
+ current_context.fns().ShaderSource(
+ self.shader,
+ 1,
+ &source.as_ptr().cast(),
+ &raw const length,
+ );
+ }
+
+ Ok(())
+ }
+
+ /// Compiles this shader.
+ ///
+ /// # Errors
+ /// Returns `Err` if compiling fails.
+ pub fn compile(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ ) -> Result<(), Error>
+ {
+ unsafe {
+ current_context.fns().CompileShader(self.shader);
+ }
+
+ let mut compile_success = crate::sys::types::GLint::default();
+
+ unsafe {
+ current_context.fns().GetShaderiv(
+ self.shader,
+ crate::sys::COMPILE_STATUS,
+ &raw mut compile_success,
+ );
+ }
+
+ if compile_success == 0 {
+ let info_log = self.get_info_log(current_context);
+
+ return Err(Error::CompileFailed { log: info_log });
+ }
+
+ Ok(())
+ }
+
+ pub fn delete(self, current_context: &CurrentContextWithFns<'_>)
+ {
+ unsafe {
+ current_context.fns().DeleteShader(self.shader);
+ }
+ }
+
+ fn get_info_log(&self, current_context: &CurrentContextWithFns<'_>) -> String
+ {
+ const BUF_SIZE: crate::sys::types::GLsizei = 512;
+
+ let mut buf = vec![crate::sys::types::GLchar::default(); BUF_SIZE as usize];
+
+ unsafe {
+ current_context.fns().GetShaderInfoLog(
+ self.shader,
+ BUF_SIZE,
+ 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()) }
+ }
+}
+
+/// Shader kind.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+#[repr(u32)]
+pub enum Kind
+{
+ Vertex = crate::sys::VERTEX_SHADER,
+ Fragment = crate::sys::FRAGMENT_SHADER,
+}
+
+/// Shader program
+#[derive(Debug, PartialEq, Eq, Hash)]
+pub struct Program
+{
+ program: crate::sys::types::GLuint,
+}
+
+impl Program
+{
+ #[must_use]
+ pub fn new(current_context: &CurrentContextWithFns<'_>) -> Self
+ {
+ let program = unsafe { current_context.fns().CreateProgram() };
+
+ Self { program }
+ }
+
+ pub fn attach(&self, current_context: &CurrentContextWithFns<'_>, shader: &Shader)
+ {
+ unsafe {
+ current_context
+ .fns()
+ .AttachShader(self.program, shader.shader);
+ }
+ }
+
+ /// Links this program.
+ ///
+ /// # Errors
+ /// Returns `Err` if linking fails.
+ pub fn link(&self, current_context: &CurrentContextWithFns<'_>) -> Result<(), Error>
+ {
+ unsafe {
+ current_context.fns().LinkProgram(self.program);
+ }
+
+ let mut link_success = crate::sys::types::GLint::default();
+
+ unsafe {
+ current_context.fns().GetProgramiv(
+ self.program,
+ crate::sys::LINK_STATUS,
+ &raw mut link_success,
+ );
+ }
+
+ if link_success == 0 {
+ let info_log = self.get_info_log(current_context);
+
+ return Err(Error::LinkFailed { log: info_log });
+ }
+
+ Ok(())
+ }
+
+ pub fn activate(&self, current_context: &CurrentContextWithFns<'_>)
+ {
+ unsafe {
+ current_context.fns().UseProgram(self.program);
+ }
+ }
+
+ pub fn set_uniform(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ name: &CStr,
+ var: &impl UniformVariable,
+ )
+ {
+ let location = UniformLocation(unsafe {
+ current_context
+ .fns()
+ .GetUniformLocation(self.program, name.as_ptr().cast())
+ });
+
+ var.set(current_context, self, location);
+ }
+
+ pub fn delete(self, current_context: &CurrentContextWithFns<'_>)
+ {
+ unsafe {
+ current_context.fns().DeleteProgram(self.program);
+ }
+ }
+
+ fn get_info_log(&self, current_context: &CurrentContextWithFns<'_>) -> String
+ {
+ const BUF_SIZE: crate::sys::types::GLsizei = 512;
+
+ let mut buf = vec![crate::sys::types::GLchar::default(); BUF_SIZE as usize];
+
+ unsafe {
+ current_context.fns().GetProgramInfoLog(
+ self.program,
+ BUF_SIZE,
+ 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 trait UniformVariable: ReprC + sealed::Sealed
+{
+ fn set(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ program: &Program,
+ uniform_location: UniformLocation,
+ );
+}
+
+impl UniformVariable for f32
+{
+ fn set(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ program: &Program,
+ uniform_location: UniformLocation,
+ )
+ {
+ unsafe {
+ current_context.fns().ProgramUniform1f(
+ program.program,
+ uniform_location.0,
+ *self,
+ );
+ }
+ }
+}
+
+impl sealed::Sealed for f32 {}
+
+impl UniformVariable for i32
+{
+ fn set(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ program: &Program,
+ uniform_location: UniformLocation,
+ )
+ {
+ unsafe {
+ current_context.fns().ProgramUniform1i(
+ program.program,
+ uniform_location.0,
+ *self,
+ );
+ }
+ }
+}
+
+impl sealed::Sealed for i32 {}
+
+impl UniformVariable for Vec3<f32>
+{
+ fn set(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ program: &Program,
+ uniform_location: UniformLocation,
+ )
+ {
+ unsafe {
+ current_context.fns().ProgramUniform3f(
+ program.program,
+ uniform_location.0,
+ self.x,
+ self.y,
+ self.z,
+ );
+ }
+ }
+}
+
+impl sealed::Sealed for Vec3<f32> {}
+
+impl UniformVariable for Matrix<f32, 4, 4>
+{
+ fn set(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ program: &Program,
+ uniform_location: UniformLocation,
+ )
+ {
+ unsafe {
+ current_context.fns().ProgramUniformMatrix4fv(
+ program.program,
+ uniform_location.0,
+ 1,
+ crate::sys::FALSE,
+ self.items.as_ptr().cast::<f32>(),
+ );
+ }
+ }
+}
+
+impl sealed::Sealed for Matrix<f32, 4, 4> {}
+
+#[derive(Debug)]
+pub struct UniformLocation(crate::sys::types::GLint);
+
+/// Shader error.
+#[derive(Debug, thiserror::Error)]
+pub enum Error
+{
+ #[error("All characters in source are not within the ASCII range")]
+ SourceNotAscii,
+
+ #[error("Source is too large. Length ({length}) must be < {max_length}")]
+ SourceTooLarge
+ {
+ length: usize, max_length: usize
+ },
+
+ #[error("Failed to compile shader")]
+ CompileFailed
+ {
+ log: String
+ },
+
+ #[error("Failed to link shader program")]
+ LinkFailed
+ {
+ log: String
+ },
+}
+
+mod sealed
+{
+ pub trait Sealed {}
+}
diff --git a/opengl-bindings/src/texture.rs b/opengl-bindings/src/texture.rs
new file mode 100644
index 0000000..1859beb
--- /dev/null
+++ b/opengl-bindings/src/texture.rs
@@ -0,0 +1,236 @@
+use crate::data_types::Dimens;
+use crate::CurrentContextWithFns;
+
+#[derive(Debug)]
+pub struct Texture
+{
+ texture: crate::sys::types::GLuint,
+}
+
+impl Texture
+{
+ #[must_use]
+ pub fn new(current_context: &CurrentContextWithFns<'_>) -> Self
+ {
+ let mut texture = crate::sys::types::GLuint::default();
+
+ unsafe {
+ current_context.fns().CreateTextures(
+ crate::sys::TEXTURE_2D,
+ 1,
+ &raw mut texture,
+ );
+ };
+
+ Self { texture }
+ }
+
+ pub fn bind_to_texture_unit(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ texture_unit: u32,
+ )
+ {
+ unsafe {
+ current_context
+ .fns()
+ .BindTextureUnit(texture_unit, self.texture);
+ }
+ }
+
+ /// Allocates the texture storage, stores pixel data & generates a mipmap.
+ ///
+ /// # Errors
+ /// Returns `Err` if any value in `size` does not fit into a `i32`.
+ pub fn generate(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ size: &Dimens<u32>,
+ data: &[u8],
+ pixel_data_format: PixelDataFormat,
+ ) -> Result<(), GenerateError>
+ {
+ let size = Dimens::<crate::sys::types::GLsizei> {
+ width: size.width.try_into().map_err(|_| {
+ GenerateError::SizeWidthValueTooLarge {
+ value: size.width,
+ max_value: crate::sys::types::GLsizei::MAX as u32,
+ }
+ })?,
+ height: size.height.try_into().map_err(|_| {
+ GenerateError::SizeHeightValueTooLarge {
+ value: size.height,
+ max_value: crate::sys::types::GLsizei::MAX as u32,
+ }
+ })?,
+ };
+
+ self.alloc_image(current_context, pixel_data_format, &size, data);
+
+ unsafe {
+ current_context.fns().GenerateTextureMipmap(self.texture);
+ }
+
+ Ok(())
+ }
+
+ pub fn set_wrap(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ wrapping: Wrapping,
+ )
+ {
+ unsafe {
+ current_context.fns().TextureParameteri(
+ self.texture,
+ crate::sys::TEXTURE_WRAP_S,
+ wrapping as i32,
+ );
+
+ current_context.fns().TextureParameteri(
+ self.texture,
+ crate::sys::TEXTURE_WRAP_T,
+ wrapping as i32,
+ );
+ }
+ }
+
+ pub fn set_magnifying_filter(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ filtering: Filtering,
+ )
+ {
+ unsafe {
+ current_context.fns().TextureParameteri(
+ self.texture,
+ crate::sys::TEXTURE_MAG_FILTER,
+ filtering as i32,
+ );
+ }
+ }
+
+ pub fn set_minifying_filter(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ filtering: Filtering,
+ )
+ {
+ unsafe {
+ current_context.fns().TextureParameteri(
+ self.texture,
+ crate::sys::TEXTURE_MIN_FILTER,
+ filtering as i32,
+ );
+ }
+ }
+
+ pub fn delete(self, current_context: &CurrentContextWithFns<'_>)
+ {
+ unsafe {
+ current_context
+ .fns()
+ .DeleteTextures(1, &raw const self.texture);
+ }
+ }
+
+ fn alloc_image(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ pixel_data_format: PixelDataFormat,
+ size: &Dimens<crate::sys::types::GLsizei>,
+ data: &[u8],
+ )
+ {
+ unsafe {
+ current_context.fns().TextureStorage2D(
+ self.texture,
+ 1,
+ pixel_data_format.to_sized_internal_format(),
+ size.width,
+ size.height,
+ );
+
+ current_context.fns().TextureSubImage2D(
+ self.texture,
+ 0,
+ 0,
+ 0,
+ size.width,
+ size.height,
+ pixel_data_format.to_format(),
+ crate::sys::UNSIGNED_BYTE,
+ data.as_ptr().cast(),
+ );
+ }
+ }
+}
+
+const fn try_cast_u32_to_i32(val: u32) -> i32
+{
+ assert!(val <= i32::MAX as u32);
+
+ val.cast_signed()
+}
+
+/// Texture wrapping.
+#[derive(Debug, Clone, Copy)]
+#[repr(i32)]
+pub enum Wrapping
+{
+ Repeat = const { try_cast_u32_to_i32(crate::sys::REPEAT) },
+ MirroredRepeat = const { try_cast_u32_to_i32(crate::sys::MIRRORED_REPEAT) },
+ ClampToEdge = const { try_cast_u32_to_i32(crate::sys::CLAMP_TO_EDGE) },
+ ClampToBorder = const { try_cast_u32_to_i32(crate::sys::CLAMP_TO_BORDER) },
+}
+
+#[derive(Debug, Clone, Copy)]
+#[repr(i32)]
+pub enum Filtering
+{
+ Nearest = const { try_cast_u32_to_i32(crate::sys::NEAREST) },
+ Linear = const { try_cast_u32_to_i32(crate::sys::LINEAR) },
+}
+
+/// Texture pixel data format.
+#[derive(Debug, Clone, Copy)]
+pub enum PixelDataFormat
+{
+ Rgb8,
+ Rgba8,
+}
+
+impl PixelDataFormat
+{
+ fn to_sized_internal_format(self) -> crate::sys::types::GLenum
+ {
+ match self {
+ Self::Rgb8 => crate::sys::RGB8,
+ Self::Rgba8 => crate::sys::RGBA8,
+ }
+ }
+
+ fn to_format(self) -> crate::sys::types::GLenum
+ {
+ match self {
+ Self::Rgb8 => crate::sys::RGB,
+ Self::Rgba8 => crate::sys::RGBA,
+ }
+ }
+}
+
+/// Error generating texture.
+#[derive(Debug, thiserror::Error)]
+pub enum GenerateError
+{
+ #[error("Size width value ({value}) is too large. Must be < {max_value}")]
+ SizeWidthValueTooLarge
+ {
+ value: u32, max_value: u32
+ },
+ #[error("Size height value ({value}) is too large. Must be < {max_value}")]
+ SizeHeightValueTooLarge
+ {
+ value: u32, max_value: u32
+ },
+}
diff --git a/opengl-bindings/src/vertex_array.rs b/opengl-bindings/src/vertex_array.rs
new file mode 100644
index 0000000..9942fe7
--- /dev/null
+++ b/opengl-bindings/src/vertex_array.rs
@@ -0,0 +1,260 @@
+use std::ffi::{c_int, c_void};
+use std::mem::size_of;
+
+use safer_ffi::layout::ReprC;
+
+use crate::buffer::Buffer;
+use crate::CurrentContextWithFns;
+
+#[derive(Debug)]
+pub struct VertexArray
+{
+ array: crate::sys::types::GLuint,
+}
+
+impl VertexArray
+{
+ #[must_use]
+ pub fn new(current_context: &CurrentContextWithFns<'_>) -> Self
+ {
+ let mut array = 0;
+
+ unsafe {
+ current_context.fns().CreateVertexArrays(1, &raw mut array);
+ }
+
+ Self { array }
+ }
+
+ /// Draws the currently bound vertex array.
+ ///
+ /// # Errors
+ /// Returns `Err` if:
+ /// - `start_index` is too large
+ /// - `cnt` is too large
+ pub fn draw_arrays(
+ current_context: &CurrentContextWithFns<'_>,
+ primitive_kind: PrimitiveKind,
+ start_index: u32,
+ cnt: u32,
+ ) -> Result<(), DrawError>
+ {
+ let start_index: crate::sys::types::GLint =
+ start_index
+ .try_into()
+ .map_err(|_| DrawError::StartIndexValueTooLarge {
+ value: start_index,
+ max_value: crate::sys::types::GLint::MAX as u32,
+ })?;
+
+ let cnt: crate::sys::types::GLsizei =
+ cnt.try_into().map_err(|_| DrawError::CountValueTooLarge {
+ value: cnt,
+ max_value: crate::sys::types::GLsizei::MAX as u32,
+ })?;
+
+ unsafe {
+ current_context
+ .fns()
+ .DrawArrays(primitive_kind.into_gl(), start_index, cnt);
+ }
+
+ Ok(())
+ }
+
+ /// Draws the currently bound vertex array.
+ ///
+ /// # Errors
+ /// Returns `Err` if `cnt` is too large.
+ pub fn draw_elements(
+ current_context: &CurrentContextWithFns<'_>,
+ primitive_kind: PrimitiveKind,
+ start_index: u32,
+ cnt: u32,
+ ) -> Result<(), DrawError>
+ {
+ let cnt: crate::sys::types::GLsizei =
+ cnt.try_into().map_err(|_| DrawError::CountValueTooLarge {
+ value: cnt,
+ max_value: crate::sys::types::GLsizei::MAX as u32,
+ })?;
+
+ unsafe {
+ current_context.fns().DrawElements(
+ primitive_kind.into_gl(),
+ cnt,
+ crate::sys::UNSIGNED_INT,
+ // TODO: Make this not sometimes UB. DrawElements expects a actual
+ // pointer to a memory location when no VBO is bound.
+ // See: https://stackoverflow.com/q/21706113
+ std::ptr::without_provenance::<c_void>(start_index as usize),
+ );
+ }
+
+ Ok(())
+ }
+
+ pub fn bind_element_buffer(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ element_buffer: &Buffer<u32>,
+ )
+ {
+ unsafe {
+ current_context
+ .fns()
+ .VertexArrayElementBuffer(self.array, element_buffer.object());
+ }
+ }
+
+ pub fn bind_vertex_buffer<VertexT: ReprC>(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ binding_index: u32,
+ vertex_buffer: &Buffer<VertexT>,
+ offset: isize,
+ )
+ {
+ let vertex_size = const { cast_usize_to_c_int(size_of::<VertexT>()) };
+
+ unsafe {
+ current_context.fns().VertexArrayVertexBuffer(
+ self.array,
+ binding_index,
+ vertex_buffer.object(),
+ offset,
+ vertex_size,
+ );
+ }
+ }
+
+ pub fn enable_attrib(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ attrib_index: u32,
+ )
+ {
+ unsafe {
+ current_context.fns().EnableVertexArrayAttrib(
+ self.array,
+ attrib_index as crate::sys::types::GLuint,
+ );
+ }
+ }
+
+ pub fn set_attrib_format(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ attrib_index: u32,
+ data_type: DataType,
+ normalized: bool,
+ offset: u32,
+ )
+ {
+ unsafe {
+ current_context.fns().VertexArrayAttribFormat(
+ self.array,
+ attrib_index,
+ data_type.size(),
+ data_type as u32,
+ if normalized {
+ crate::sys::TRUE
+ } else {
+ crate::sys::FALSE
+ },
+ offset,
+ );
+ }
+ }
+
+ /// Associate a vertex attribute and a vertex buffer binding.
+ pub fn set_attrib_vertex_buf_binding(
+ &self,
+ current_context: &CurrentContextWithFns<'_>,
+ attrib_index: u32,
+ vertex_buf_binding_index: u32,
+ )
+ {
+ unsafe {
+ current_context.fns().VertexArrayAttribBinding(
+ self.array,
+ attrib_index,
+ vertex_buf_binding_index,
+ );
+ }
+ }
+
+ pub fn bind(&self, current_context: &CurrentContextWithFns<'_>)
+ {
+ unsafe { current_context.fns().BindVertexArray(self.array) }
+ }
+}
+
+#[derive(Debug)]
+pub enum PrimitiveKind
+{
+ Triangles,
+}
+
+impl PrimitiveKind
+{
+ fn into_gl(self) -> crate::sys::types::GLenum
+ {
+ match self {
+ Self::Triangles => crate::sys::TRIANGLES,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+#[repr(u32)]
+pub enum DataType
+{
+ Float = crate::sys::FLOAT,
+}
+
+impl DataType
+{
+ fn size(self) -> crate::sys::types::GLint
+ {
+ match self {
+ Self::Float => const { cast_usize_to_c_int(size_of::<f32>()) },
+ }
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum DrawError
+{
+ #[error("Start index value {value} is too large. Must be < {max_value}")]
+ StartIndexValueTooLarge
+ {
+ value: u32, max_value: u32
+ },
+
+ #[error("Count value {value} is too large. Must be < {max_value}")]
+ CountValueTooLarge
+ {
+ value: u32, max_value: u32
+ },
+}
+
+const fn cast_usize_to_c_int(num: usize) -> c_int
+{
+ assert!(num <= c_int::MAX.cast_unsigned() as usize);
+
+ c_int::from_ne_bytes(shorten_byte_array(num.to_ne_bytes()))
+}
+
+const fn shorten_byte_array<const SRC_LEN: usize, const DST_LEN: usize>(
+ src: [u8; SRC_LEN],
+) -> [u8; DST_LEN]
+{
+ assert!(DST_LEN < SRC_LEN);
+
+ let mut ret = [0; DST_LEN];
+
+ ret.copy_from_slice(src.split_at(DST_LEN).0);
+
+ ret
+}
diff --git a/src/main.rs b/src/main.rs
index 3055e28..4ede773 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,32 +9,35 @@ 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::Dimens3;
+use engine::ecs::actions::Actions;
+use engine::ecs::event::component::Added;
+use engine::ecs::pair::Pair;
use engine::ecs::phase::START as START_PHASE;
use engine::ecs::sole::Single;
-use engine::file_format::wavefront::mtl::parse as parse_mtl;
-use engine::file_format::wavefront::obj::parse as parse_obj;
+use engine::ecs::system::observer::Observe;
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::transform::Position;
+use engine::renderer::GraphicsProperties;
+use engine::transform::WorldPosition;
use engine::vector::Vec3;
-use engine::window::{
- Builder as WindowBuilder,
- CursorMode,
- Extension as WindowExtension,
+use engine::windowing::window::{
+ CreationAttributes as WindowCreationAttributes,
+ CursorGrabMode as WindowCursorGrabMode,
Window,
};
use engine::Engine;
-use tracing::Level;
-use tracing_subscriber::FmtSubscriber;
-
-const WINDOW_SIZE: Dimens<u32> = Dimens { width: 1920, height: 1080 };
+use tracing::level_filters::LevelFilter;
+use tracing_subscriber::layer::SubscriberExt;
+use tracing_subscriber::util::SubscriberInitExt;
+use tracing_subscriber::EnvFilter;
const YELLOW: Color<f32> = Color {
red: 0.988235294118,
@@ -46,61 +49,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 }),
- ));
-
- 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 }),
- cube_mesh_create(
- CubeMeshCreationSpec::builder()
- .width(2.0)
- .height(2.0)
- .depth(2.0)
- .build(),
- |face_verts, _, _| face_verts,
- ),
- MaterialBuilder::new().ambient(YELLOW * 5.0).build(),
- 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,
@@ -109,15 +71,13 @@ fn main() -> Result<(), Box<dyn Error>>
engine.add_sole(GlobalLight::default())?;
- engine.register_system(*START_PHASE, prepare_window);
+ engine.register_system(*START_PHASE, init);
- engine.add_extension(OpenglRendererExtension::default());
+ engine.register_observer(configure_window_on_added);
- engine.add_extension(
- WindowExtension::new(WindowBuilder::default().multisampling_sample_count(8))
- .window_title("Game")
- .window_size(WINDOW_SIZE),
- );
+ engine.add_extension(engine::windowing::Extension::default());
+
+ engine.add_extension(OpenglRendererExtension::default());
engine.add_extension(FlyCameraExtension(FlyCameraOptions {
mouse_sensitivity: 0.2,
@@ -125,12 +85,62 @@ fn main() -> Result<(), Box<dyn Error>>
engine.add_extension(InputExtension::default());
+ engine.spawn((
+ WindowCreationAttributes::default().with_title("Game"),
+ GraphicsProperties::builder().debug(true).build(),
+ ));
+
engine.start();
Ok(())
}
-fn prepare_window(window: Single<Window>)
+fn configure_window_on_added(observe: Observe<Pair<Added, Window>>)
+{
+ for evt_match in &observe {
+ let mut window = evt_match.get_added_comp_mut();
+
+ window.cursor_visible = false;
+ window.cursor_grab_mode = WindowCursorGrabMode::Locked;
+
+ window.set_changed();
+ }
+}
+
+fn init(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 }),
+ ));
}