From 59e14b43e836b5b97cc999938b515fac3e1f60dc Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 2 Nov 2024 16:18:41 -0400 Subject: [PATCH 01/20] Set up a rust UI --- .gitignore | 9 +- Cargo.lock | 2339 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 16 + build.rs | 11 + src/main.rs | 325 ++++++ src/shrooms_vb_core.rs | 37 + 6 files changed, 2733 insertions(+), 4 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 build.rs create mode 100644 src/main.rs create mode 100644 src/shrooms_vb_core.rs diff --git a/.gitignore b/.gitignore index 432d6a2..d92e239 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -/shrooms-vb -/shrooms-vb.exe -.vscode -output \ No newline at end of file +/shrooms-vb +/shrooms-vb.exe +.vscode +output +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8f06876 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2339 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ab_glyph" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[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.6.0", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "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 = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bit-set" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytemuck" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" + +[[package]] +name = "bytes" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.6.0", + "log", + "polling", + "rustix", + "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", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9" +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 = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chlorine" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e10e7569f6ca78ef7664d7d651115172d4875c4410c050306bccde856a99a49" + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "com" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6" +dependencies = [ + "com_macros", +] + +[[package]] +name = "com_macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5" +dependencies = [ + "com_macros_support", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "com_macros_support" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[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 = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "cursor-icon" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" + +[[package]] +name = "d3d12" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdbd1f579714e3c809ebd822c81ef148b1ceaeb3d535352afc73fd0c4c6a0017" +dependencies = [ + "bitflags 2.6.0", + "libloading", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[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.87", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glow" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4e1951bbd9434a81aa496fe59ccc2235af3820d27b85f9314e279609211e2c" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.6.0", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "gpu-allocator" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd4240fc91d3433d5e5b0fc5b67672d771850dc19bbee03c1381e19322803d7" +dependencies = [ + "log", + "presser", + "thiserror", + "winapi", + "windows", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557" +dependencies = [ + "bitflags 2.6.0", + "gpu-descriptor-types", + "hashbrown 0.14.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "hassle-rs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" +dependencies = [ + "bitflags 2.6.0", + "com", + "libc", + "libloading", + "thiserror", + "widestring", + "winapi", +] + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "imgui" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8addafa5cecf0515812226e806913814e02ce38d10215778082af5174abe5669" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "imgui-sys", + "mint", + "parking_lot", +] + +[[package]] +name = "imgui-sys" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ead193f9f4b60398e8b8f4ab1483e2321640d87aeebdaa3e5f44c55633ccd804" +dependencies = [ + "cc", + "cfg-if", + "chlorine", + "mint", +] + +[[package]] +name = "imgui-wgpu" +version = "0.25.0" +source = "git+https://github.com/Yatekii/imgui-wgpu-rs?rev=2edd348#2edd348a0fc11e9e72f19060c34a6e45c760b116" +dependencies = [ + "bytemuck", + "imgui", + "log", + "smallvec", + "wgpu", +] + +[[package]] +name = "imgui-winit-support" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff7fcccfa9efab56c94274c0fec9939bb14149342b49e6a425883a5b7dda6a3f" +dependencies = [ + "imgui", + "winit", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", +] + +[[package]] +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 = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", + "redox_syscall 0.5.7", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.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 = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + +[[package]] +name = "metal" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +dependencies = [ + "bitflags 2.6.0", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "mint" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" + +[[package]] +name = "naga" +version = "22.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd5a652b6faf21496f2cfd88fc49989c8db0825d1f6746b1a71a6ede24a63ad" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.6.0", + "cfg_aliases 0.1.1", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "rustc-hash", + "spirv", + "termcolor", + "thiserror", + "unicode-xid", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.6.0", + "jni-sys", + "log", + "ndk-sys 0.6.0+11769913", + "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.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[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 = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[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-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.6.0", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "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", + "objc2-contacts", + "objc2-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.6.0", + "block2", + "dispatch", + "libc", + "objc2", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "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", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "orbclient" +version = "0.3.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" +dependencies = [ + "libredox", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" +dependencies = [ + "ttf-parser", +] + +[[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.7", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "polling" +version = "3.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" + +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "range-alloc" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" + +[[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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "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 = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + +[[package]] +name = "serde" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shrooms-vb-native" +version = "0.1.0" +dependencies = [ + "anyhow", + "cc", + "imgui", + "imgui-wgpu", + "imgui-winit-support", + "pollster", + "wgpu", + "winit", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.6.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix", + "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 = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[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.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" + +[[package]] +name = "ttf-parser" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5902c5d130972a0000f60860bfbf46f7ca3db5391eddfedd1b8728bd9dc96c0e" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.87", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "wayland-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" +dependencies = [ + "bitflags 2.6.0", + "rustix", + "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.6.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b08bc3aafdb0035e7fe0fdf17ba0c09c268732707dca4ae098f60cb28c9e4c" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b31cab548ee68c7eb155517f2212049dc151f7cd7910c2b66abfd31c3ee12bd" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "782e12f6cd923c3c316130d56205ebab53f55d6666b7faddfad36cecaeeb4022" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +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 = "wgpu" +version = "22.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d1c4ba43f80542cf63a0a6ed3134629ae73e8ab51e4b765a67f3aa062eb433" +dependencies = [ + "arrayvec", + "cfg_aliases 0.1.1", + "document-features", + "js-sys", + "log", + "naga", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "22.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348c840d1051b8e86c3bcd31206080c5e71e5933dabd79be1ce732b0b2f089a" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.6.0", + "cfg_aliases 0.1.1", + "document-features", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "rustc-hash", + "smallvec", + "thiserror", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6bbf4b4de8b2a83c0401d9e5ae0080a2792055f25859a02bf9be97952bbed4f" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.6.0", + "block", + "cfg_aliases 0.1.1", + "core-graphics-types", + "d3d12", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "hassle-rs", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "parking_lot", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash", + "smallvec", + "thiserror", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "winapi", +] + +[[package]] +name = "wgpu-types" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9d91f0e2c4b51434dfa6db77846f2793149d8e73f800fa2e41f52b8eac3c5d" +dependencies = [ + "bitflags 2.6.0", + "js-sys", + "web-sys", +] + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "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]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "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" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be9e76a1f1077e04a411f0b989cbd3c93339e1771cb41e71ac4aee95bfd2c67" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.6.0", + "block2", + "bytemuck", + "calloop", + "cfg_aliases 0.2.1", + "concurrent-queue", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix", + "sctk-adwaita", + "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.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[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", + "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.6.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.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c764a56 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "shrooms-vb-native" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +imgui = "0.12" +imgui-wgpu = { git = "https://github.com/Yatekii/imgui-wgpu-rs", rev = "2edd348" } +imgui-winit-support = "0.13" +pollster = "0.4" +wgpu = "22.1" +winit = "0.30" + +[build-dependencies] +cc = "1" \ No newline at end of file diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..ed0f5b1 --- /dev/null +++ b/build.rs @@ -0,0 +1,11 @@ +use std::path::Path; + +fn main() { + cc::Build::new() + .include(Path::new("shrooms-vb-core/core")) + .opt_level(3) + .flag_if_supported("-flto") + .flag_if_supported("-fno-strict-aliasing") + .file(Path::new("shrooms-vb-core/core/vb.c")) + .compile("vb"); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b9de0c2 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,325 @@ +use imgui::*; +use imgui_wgpu::{Renderer, RendererConfig}; +use imgui_winit_support::WinitPlatform; +use pollster::block_on; +use std::{sync::Arc, time::Instant}; +use winit::{ + application::ApplicationHandler, + dpi::LogicalSize, + event::{Event, WindowEvent}, + event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, + keyboard::{Key, NamedKey}, + window::Window, +}; + +struct ImguiState { + context: imgui::Context, + platform: WinitPlatform, + renderer: Renderer, + clear_color: wgpu::Color, + last_frame: Instant, + last_cursor: Option, +} + +struct AppWindow { + device: wgpu::Device, + queue: wgpu::Queue, + window: Arc, + surface_desc: wgpu::SurfaceConfiguration, + surface: wgpu::Surface<'static>, + hidpi_factor: f64, + imgui: Option, +} + +#[derive(Default)] +struct App { + window: Option, +} + +impl AppWindow { + fn setup_gpu(event_loop: &ActiveEventLoop) -> Self { + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::PRIMARY, + ..Default::default() + }); + + let window = { + let size = LogicalSize::new(384, 244); + + let attributes = Window::default_attributes() + .with_inner_size(size) + .with_title("Shrooms VB"); + Arc::new(event_loop.create_window(attributes).unwrap()) + }; + + let size = window.inner_size(); + let hidpi_factor = window.scale_factor(); + let surface = instance.create_surface(window.clone()).unwrap(); + + let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: Some(&surface), + force_fallback_adapter: false, + })) + .unwrap(); + + let (device, queue) = + block_on(adapter.request_device(&wgpu::DeviceDescriptor::default(), None)).unwrap(); + + // Set up swap chain + let surface_desc = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Fifo, + desired_maximum_frame_latency: 2, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![wgpu::TextureFormat::Bgra8Unorm], + }; + + surface.configure(&device, &surface_desc); + + let imgui = None; + Self { + device, + queue, + window, + surface_desc, + surface, + hidpi_factor, + imgui, + } + } + + fn setup_imgui(&mut self) { + let mut context = imgui::Context::create(); + let mut platform = imgui_winit_support::WinitPlatform::new(&mut context); + platform.attach_window( + context.io_mut(), + &self.window, + imgui_winit_support::HiDpiMode::Default, + ); + context.set_ini_filename(None); + + let font_size = (13.0 * self.hidpi_factor) as f32; + context.io_mut().font_global_scale = (1.0 / self.hidpi_factor) as f32; + + context.fonts().add_font(&[FontSource::DefaultFontData { + config: Some(imgui::FontConfig { + oversample_h: 1, + pixel_snap_h: true, + size_pixels: font_size, + ..Default::default() + }), + }]); + + // + // Set up dear imgui wgpu renderer + // + let clear_color = wgpu::Color::BLACK; + + let renderer_config = RendererConfig { + texture_format: self.surface_desc.format, + ..Default::default() + }; + + let renderer = Renderer::new(&mut context, &self.device, &self.queue, renderer_config); + let last_frame = Instant::now(); + let last_cursor = None; + + self.imgui = Some(ImguiState { + context, + platform, + renderer, + clear_color, + last_frame, + last_cursor, + }) + } + + fn new(event_loop: &ActiveEventLoop) -> Self { + let mut window = Self::setup_gpu(event_loop); + window.setup_imgui(); + window + } +} + +impl ApplicationHandler for App { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + self.window = Some(AppWindow::new(event_loop)); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: winit::window::WindowId, + event: WindowEvent, + ) { + let window = self.window.as_mut().unwrap(); + let imgui = window.imgui.as_mut().unwrap(); + let mut quit = false; + + match &event { + WindowEvent::Resized(size) => { + window.surface_desc = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Fifo, + desired_maximum_frame_latency: 2, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![wgpu::TextureFormat::Bgra8Unorm], + }; + + window + .surface + .configure(&window.device, &window.surface_desc); + } + WindowEvent::CloseRequested => event_loop.exit(), + WindowEvent::KeyboardInput { event, .. } => { + if let Key::Named(NamedKey::Escape) = event.logical_key { + if event.state.is_pressed() { + quit = true; + } + } + } + WindowEvent::RedrawRequested => { + let now = Instant::now(); + imgui + .context + .io_mut() + .update_delta_time(now - imgui.last_frame); + imgui.last_frame = now; + + let frame = match window.surface.get_current_texture() { + Ok(frame) => frame, + Err(e) => { + eprintln!("dropped frame: {e:?}"); + return; + } + }; + imgui + .platform + .prepare_frame(imgui.context.io_mut(), &window.window) + .expect("Failed to prepare frame"); + let ui = imgui.context.frame(); + ui.main_menu_bar(|| { + ui.menu("ROM", || { + if ui.menu_item("Open ROM") { + println!("clicked"); + } + if ui.menu_item("Quit") { + event_loop.exit(); + } + }); + ui.menu("Emulation", || { + if ui.menu_item("Pause") { + println!("clicked"); + } + if ui.menu_item("Reset") { + println!("clicked"); + } + }); + }); + + let mut encoder: wgpu::CommandEncoder = window + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + + if imgui.last_cursor != ui.mouse_cursor() { + imgui.last_cursor = ui.mouse_cursor(); + imgui.platform.prepare_render(ui, &window.window); + } + + let view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(imgui.clear_color), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + imgui + .renderer + .render( + imgui.context.render(), + &window.queue, + &window.device, + &mut rpass, + ) + .expect("Rendering failed"); + + drop(rpass); + + window.queue.submit(Some(encoder.finish())); + + frame.present(); + } + _ => (), + } + + imgui.platform.handle_event::<()>( + imgui.context.io_mut(), + &window.window, + &Event::WindowEvent { window_id, event }, + ); + + if quit { + self.window.take(); + } + } + + fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: ()) { + let window = self.window.as_mut().unwrap(); + let imgui = window.imgui.as_mut().unwrap(); + imgui.platform.handle_event::<()>( + imgui.context.io_mut(), + &window.window, + &Event::UserEvent(event), + ); + } + + fn device_event( + &mut self, + _event_loop: &ActiveEventLoop, + device_id: winit::event::DeviceId, + event: winit::event::DeviceEvent, + ) { + let window = self.window.as_mut().unwrap(); + let imgui = window.imgui.as_mut().unwrap(); + imgui.platform.handle_event::<()>( + imgui.context.io_mut(), + &window.window, + &Event::DeviceEvent { device_id, event }, + ); + } + + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + let window = self.window.as_mut().unwrap(); + let imgui = window.imgui.as_mut().unwrap(); + window.window.request_redraw(); + imgui.platform.handle_event::<()>( + imgui.context.io_mut(), + &window.window, + &Event::AboutToWait, + ); + } +} + +fn main() { + let event_loop = EventLoop::new().unwrap(); + event_loop.set_control_flow(ControlFlow::Poll); + event_loop.run_app(&mut App::default()).unwrap(); +} diff --git a/src/shrooms_vb_core.rs b/src/shrooms_vb_core.rs new file mode 100644 index 0000000..2df7589 --- /dev/null +++ b/src/shrooms_vb_core.rs @@ -0,0 +1,37 @@ + +#[repr(C)] +struct VB { _data: [u8; 0] } + +#[link(name = "vb")] +extern "C" { + #[link_name = "vbInit"] + fn vb_init(sim: *mut VB) -> *mut VB; + + #[link_name = "vbSizeOf"] + fn vb_size_of() -> usize; + + #[link_name = "vbGetKeys"] + fn vb_get_keys(sim: *mut VB) -> u16; +} + +pub struct CoreVB { + sim: *mut VB, + _data: Box<[u8]>, +} + +impl CoreVB { + pub fn new() -> Self { + let size = unsafe { vb_size_of() }; + let mut data = vec![0; size].into_boxed_slice(); + let sim = data.as_mut_ptr() as *mut VB; + unsafe { vb_init(sim) }; + CoreVB { + sim, + _data: data + } + } + + pub fn keys(&self) -> u16 { + unsafe { vb_get_keys(self.sim) } + } +} \ No newline at end of file From 40c45617483ab573992634fb0d3fdd8ba91ccfc4 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 3 Nov 2024 11:32:53 -0500 Subject: [PATCH 02/20] Beginnings of rust emulation --- Cargo.lock | 120 +++++++++++ Cargo.toml | 6 +- build.rs | 3 +- src/anaglyph.wgsl | 49 +++++ src/app.rs | 450 +++++++++++++++++++++++++++++++++++++++++ src/emulator.rs | 93 +++++++++ src/main.rs | 341 +++---------------------------- src/renderer.rs | 58 ++++++ src/shrooms_vb_core.rs | 148 ++++++++++++-- 9 files changed, 936 insertions(+), 332 deletions(-) create mode 100644 src/anaglyph.wgsl create mode 100644 src/app.rs create mode 100644 src/emulator.rs create mode 100644 src/renderer.rs diff --git a/Cargo.lock b/Cargo.lock index 8f06876..023dbe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,6 +73,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.92" @@ -245,6 +294,46 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e10e7569f6ca78ef7664d7d651115172d4875c4410c050306bccde856a99a49" +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -255,6 +344,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "com" version = "0.6.0" @@ -583,6 +678,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.4.0" @@ -652,6 +753,12 @@ dependencies = [ "hashbrown 0.15.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "jni" version = "0.21.1" @@ -1383,6 +1490,7 @@ version = "0.1.0" dependencies = [ "anyhow", "cc", + "clap", "imgui", "imgui-wgpu", "imgui-winit-support", @@ -1470,6 +1578,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "1.0.109" @@ -1609,6 +1723,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index c764a56..2b1203f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] anyhow = "1" +clap = { version = "4", features = ["derive"] } imgui = "0.12" imgui-wgpu = { git = "https://github.com/Yatekii/imgui-wgpu-rs", rev = "2edd348" } imgui-winit-support = "0.13" @@ -13,4 +14,7 @@ wgpu = "22.1" winit = "0.30" [build-dependencies] -cc = "1" \ No newline at end of file +cc = "1" + +[profile.release] +lto = "thin" \ No newline at end of file diff --git a/build.rs b/build.rs index ed0f5b1..17bd565 100644 --- a/build.rs +++ b/build.rs @@ -1,9 +1,10 @@ use std::path::Path; fn main() { + println!("cargo::rerun-if-changed=shrooms-vb-core"); cc::Build::new() .include(Path::new("shrooms-vb-core/core")) - .opt_level(3) + .opt_level(2) .flag_if_supported("-flto") .flag_if_supported("-fno-strict-aliasing") .file(Path::new("shrooms-vb-core/core/vb.c")) diff --git a/src/anaglyph.wgsl b/src/anaglyph.wgsl new file mode 100644 index 0000000..0015101 --- /dev/null +++ b/src/anaglyph.wgsl @@ -0,0 +1,49 @@ +// Vertex shader + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) tex_coords: vec2, +}; + +@vertex +fn vs_main( + @builtin(vertex_index) in_vertex_index: u32, +) -> VertexOutput { + var out: VertexOutput; + var x: f32; + var y: f32; + switch in_vertex_index { + case 0u, 3u: { + x = -1.0; + y = 1.0; + } + case 1u: { + x = -1.0; + y = -1.0; + } + case 2u, 4u: { + x = 1.0; + y = -1.0; + } + default: { + x = 1.0; + y = 1.0; + } + } + out.clip_position = vec4(x, y, 0.0, 1.0); + out.tex_coords = vec2((x + 1.0) / 2.0, (1.0 - y) / 2.0); + return out; +} + +// Fragment shader +@group(0) @binding(0) +var u_textures: binding_array>; +@group(0) @binding(1) +var u_sampler: sampler; + + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return textureSample(u_textures[0], u_sampler, in.tex_coords); + // return vec4(0.3, 0.2, 0.1, 1.0); +} diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..7919c6b --- /dev/null +++ b/src/app.rs @@ -0,0 +1,450 @@ +use imgui::*; +use imgui_wgpu::{Renderer, RendererConfig}; +use imgui_winit_support::WinitPlatform; +use pollster::block_on; +use std::{num::NonZero, sync::Arc, time::Instant}; +use winit::{ + application::ApplicationHandler, + dpi::LogicalSize, + event::{ElementState, Event, WindowEvent}, + event_loop::ActiveEventLoop, + keyboard::Key, + platform::windows::{CornerPreference, WindowAttributesExtWindows}, + window::Window, +}; + +use crate::{ + emulator::{EmulatorClient, EmulatorCommand}, + renderer::GameRenderer, +}; + +struct ImguiState { + context: imgui::Context, + platform: WinitPlatform, + renderer: Renderer, + clear_color: wgpu::Color, + last_frame: Instant, + last_cursor: Option, +} + +struct AppWindow { + device: wgpu::Device, + queue: Arc, + window: Arc, + surface_desc: wgpu::SurfaceConfiguration, + surface: wgpu::Surface<'static>, + hidpi_factor: f64, + pipeline: wgpu::RenderPipeline, + bind_group: wgpu::BindGroup, + imgui: Option, +} + +impl AppWindow { + fn setup_gpu(event_loop: &ActiveEventLoop, client: &EmulatorClient) -> Self { + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::PRIMARY, + ..Default::default() + }); + + let window = { + let size = LogicalSize::new(384, 244); + + let attributes = Window::default_attributes() + .with_inner_size(size) + .with_title("Shrooms VB") + .with_corner_preference(CornerPreference::DoNotRound); + Arc::new(event_loop.create_window(attributes).unwrap()) + }; + + let size = window.inner_size(); + let hidpi_factor = window.scale_factor(); + let surface = instance.create_surface(window.clone()).unwrap(); + + let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: Some(&surface), + force_fallback_adapter: false, + })) + .unwrap(); + + let (device, queue) = block_on(adapter.request_device( + &wgpu::DeviceDescriptor { + required_features: wgpu::Features::TEXTURE_BINDING_ARRAY, + ..wgpu::DeviceDescriptor::default() + }, + None, + )) + .unwrap(); + let queue = Arc::new(queue); + let eyes = [ + Arc::new(GameRenderer::create_texture(&device, "left eye")), + Arc::new(GameRenderer::create_texture(&device, "right eye")), + ]; + client.send_command(EmulatorCommand::SetRenderer(GameRenderer { + queue: queue.clone(), + eyes: eyes.clone(), + })); + let eyes = [ + eyes[0].create_view(&wgpu::TextureViewDescriptor::default()), + eyes[1].create_view(&wgpu::TextureViewDescriptor::default()), + ]; + let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); + let texture_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("texture bind group layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: NonZero::new(2), + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("bind group"), + layout: &texture_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureViewArray(&[&eyes[0], &eyes[1]]), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + ], + }); + + // Set up swap chain + let surface_desc = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Fifo, + desired_maximum_frame_latency: 2, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![wgpu::TextureFormat::Bgra8Unorm], + }; + + surface.configure(&device, &surface_desc); + + let shader = device.create_shader_module(wgpu::include_wgsl!("anaglyph.wgsl")); + let render_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("render pipeline layout"), + bind_group_layouts: &[&texture_bind_group_layout], + push_constant_ranges: &[], + }); + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("render pipeline"), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Bgra8UnormSrgb, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: Some(wgpu::Face::Back), + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + cache: None, + }); + + let imgui = None; + Self { + device, + queue, + window, + surface_desc, + surface, + hidpi_factor, + pipeline: render_pipeline, + bind_group, + imgui, + } + } + + fn setup_imgui(&mut self) { + let mut context = imgui::Context::create(); + let mut platform = imgui_winit_support::WinitPlatform::new(&mut context); + platform.attach_window( + context.io_mut(), + &self.window, + imgui_winit_support::HiDpiMode::Default, + ); + context.set_ini_filename(None); + + let font_size = (13.0 * self.hidpi_factor) as f32; + context.io_mut().font_global_scale = (1.0 / self.hidpi_factor) as f32; + + context.fonts().add_font(&[FontSource::DefaultFontData { + config: Some(imgui::FontConfig { + oversample_h: 1, + pixel_snap_h: true, + size_pixels: font_size, + ..Default::default() + }), + }]); + + // + // Set up dear imgui wgpu renderer + // + let clear_color = wgpu::Color::BLACK; + + let renderer_config = RendererConfig { + texture_format: self.surface_desc.format, + ..Default::default() + }; + + let renderer = Renderer::new(&mut context, &self.device, &self.queue, renderer_config); + + let last_frame = Instant::now(); + let last_cursor = None; + + self.imgui = Some(ImguiState { + context, + platform, + renderer, + clear_color, + last_frame, + last_cursor, + }) + } + + fn new(event_loop: &ActiveEventLoop, client: &EmulatorClient) -> Self { + let mut window = Self::setup_gpu(event_loop, client); + window.setup_imgui(); + window + } +} + +pub struct App { + window: Option, + client: EmulatorClient, +} + +impl App { + pub fn new(client: EmulatorClient) -> Self { + Self { + window: None, + client, + } + } +} + +impl ApplicationHandler for App { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + self.window = Some(AppWindow::new(event_loop, &self.client)); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: winit::window::WindowId, + event: WindowEvent, + ) { + let window = self.window.as_mut().unwrap(); + let imgui = window.imgui.as_mut().unwrap(); + + match &event { + WindowEvent::Resized(size) => { + window.surface_desc = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Fifo, + desired_maximum_frame_latency: 2, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![wgpu::TextureFormat::Bgra8Unorm], + }; + + window + .surface + .configure(&window.device, &window.surface_desc); + } + WindowEvent::CloseRequested => event_loop.exit(), + WindowEvent::KeyboardInput { event, .. } => { + if let Key::Character("s") = event.logical_key.as_ref() { + match event.state { + ElementState::Pressed => { + self.client.send_command(EmulatorCommand::PressStart) + } + ElementState::Released => { + self.client.send_command(EmulatorCommand::ReleaseStart) + } + } + } + } + WindowEvent::RedrawRequested => { + let now = Instant::now(); + imgui + .context + .io_mut() + .update_delta_time(now - imgui.last_frame); + imgui.last_frame = now; + + let frame = match window.surface.get_current_texture() { + Ok(frame) => frame, + Err(e) => { + eprintln!("dropped frame: {e:?}"); + return; + } + }; + imgui + .platform + .prepare_frame(imgui.context.io_mut(), &window.window) + .expect("Failed to prepare frame"); + let ui = imgui.context.frame(); + let mut height = 0.0; + ui.main_menu_bar(|| { + height = ui.window_size()[1]; + ui.menu("ROM", || { + if ui.menu_item("Open ROM") { + println!("clicked"); + } + if ui.menu_item("Quit") { + event_loop.exit(); + } + }); + ui.menu("Emulation", || { + if ui.menu_item("Pause") { + println!("clicked"); + } + if ui.menu_item("Reset") { + println!("clicked"); + } + }); + }); + + let mut encoder: wgpu::CommandEncoder = window + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + + if imgui.last_cursor != ui.mouse_cursor() { + imgui.last_cursor = ui.mouse_cursor(); + imgui.platform.prepare_render(ui, &window.window); + } + + let view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(imgui.clear_color), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + rpass.set_pipeline(&window.pipeline); + let hidpi = window.hidpi_factor as f32; + rpass.set_viewport(0.0, height * hidpi, 384.0 * hidpi, 224.0 * hidpi, 0.0, 1.0); + rpass.set_bind_group(0, &window.bind_group, &[]); + rpass.draw(0..6, 0..1); + + rpass.set_viewport(0.0, 0.0, 384.0 * hidpi, 224.0 * hidpi, 0.0, 1.0); + imgui + .renderer + .render( + imgui.context.render(), + &window.queue, + &window.device, + &mut rpass, + ) + .expect("Rendering failed"); + + drop(rpass); + + window.queue.submit(Some(encoder.finish())); + + frame.present(); + } + _ => (), + } + + imgui.platform.handle_event::<()>( + imgui.context.io_mut(), + &window.window, + &Event::WindowEvent { window_id, event }, + ); + } + + fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: ()) { + let window = self.window.as_mut().unwrap(); + let imgui = window.imgui.as_mut().unwrap(); + imgui.platform.handle_event::<()>( + imgui.context.io_mut(), + &window.window, + &Event::UserEvent(event), + ); + } + + fn device_event( + &mut self, + _event_loop: &ActiveEventLoop, + device_id: winit::event::DeviceId, + event: winit::event::DeviceEvent, + ) { + let window = self.window.as_mut().unwrap(); + let imgui = window.imgui.as_mut().unwrap(); + imgui.platform.handle_event::<()>( + imgui.context.io_mut(), + &window.window, + &Event::DeviceEvent { device_id, event }, + ); + } + + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + let window = self.window.as_mut().unwrap(); + let imgui = window.imgui.as_mut().unwrap(); + window.window.request_redraw(); + imgui.platform.handle_event::<()>( + imgui.context.io_mut(), + &window.window, + &Event::AboutToWait, + ); + } +} diff --git a/src/emulator.rs b/src/emulator.rs new file mode 100644 index 0000000..5f1e201 --- /dev/null +++ b/src/emulator.rs @@ -0,0 +1,93 @@ +use std::{ + fs, + path::Path, + sync::mpsc::{self, TryRecvError}, +}; + +use anyhow::Result; + +use crate::{renderer::GameRenderer, shrooms_vb_core::CoreVB}; + +pub struct Emulator { + sim: CoreVB, + commands: mpsc::Receiver, + renderer: Option, +} + +impl Emulator { + pub fn new() -> (Self, EmulatorClient) { + let (sink, source) = mpsc::channel(); + let emu = Emulator { + sim: CoreVB::new(), + commands: source, + renderer: None, + }; + let queue = EmulatorClient { queue: sink }; + (emu, queue) + } + + pub fn load_rom(&mut self, path: &Path) -> Result<()> { + let bytes = fs::read(path)?; + self.sim.load_rom(bytes)?; + Ok(()) + } + + pub fn run(&mut self) { + let mut eye_contents = [vec![0u8; 384 * 224], vec![0u8; 384 * 224]]; + loop { + self.sim.emulate_frame(); + if let Some(renderer) = &mut self.renderer { + if self.sim.read_pixels(&mut eye_contents) { + renderer.render(&eye_contents); + } + } + loop { + match self.commands.try_recv() { + Ok(command) => self.handle_command(command), + Err(TryRecvError::Empty) => { + break; + } + Err(TryRecvError::Disconnected) => { + return; + } + } + } + } + } + + fn handle_command(&mut self, command: EmulatorCommand) { + match command { + EmulatorCommand::SetRenderer(renderer) => { + self.renderer = Some(renderer); + } + EmulatorCommand::PressStart => { + self.sim.set_keys(0x1003); + } + EmulatorCommand::ReleaseStart => { + self.sim.set_keys(0x0003); + } + } + } +} + +#[derive(Debug)] +pub enum EmulatorCommand { + SetRenderer(GameRenderer), + PressStart, + ReleaseStart, +} + +pub struct EmulatorClient { + queue: mpsc::Sender, +} + +impl EmulatorClient { + pub fn send_command(&self, command: EmulatorCommand) { + if let Err(err) = self.queue.send(command) { + eprintln!( + "could not send command {:?} as emulator is shut down", + err.0 + ); + } + } +} diff --git a/src/main.rs b/src/main.rs index b9de0c2..8f625d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,325 +1,32 @@ -use imgui::*; -use imgui_wgpu::{Renderer, RendererConfig}; -use imgui_winit_support::WinitPlatform; -use pollster::block_on; -use std::{sync::Arc, time::Instant}; -use winit::{ - application::ApplicationHandler, - dpi::LogicalSize, - event::{Event, WindowEvent}, - event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, - keyboard::{Key, NamedKey}, - window::Window, -}; +use std::{path::PathBuf, thread}; -struct ImguiState { - context: imgui::Context, - platform: WinitPlatform, - renderer: Renderer, - clear_color: wgpu::Color, - last_frame: Instant, - last_cursor: Option, +use anyhow::Result; +use app::App; +use clap::Parser; +use emulator::Emulator; +use winit::event_loop::{ControlFlow, EventLoop}; + +mod app; +mod emulator; +mod renderer; +mod shrooms_vb_core; + +#[derive(Parser)] +struct Args { + rom: PathBuf, } -struct AppWindow { - device: wgpu::Device, - queue: wgpu::Queue, - window: Arc, - surface_desc: wgpu::SurfaceConfiguration, - surface: wgpu::Surface<'static>, - hidpi_factor: f64, - imgui: Option, -} +fn main() -> Result<()> { + let args = Args::parse(); -#[derive(Default)] -struct App { - window: Option, -} + let (mut emulator, client) = Emulator::new(); + emulator.load_rom(&args.rom)?; + thread::spawn(move || { + emulator.run(); + }); -impl AppWindow { - fn setup_gpu(event_loop: &ActiveEventLoop) -> Self { - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::PRIMARY, - ..Default::default() - }); - - let window = { - let size = LogicalSize::new(384, 244); - - let attributes = Window::default_attributes() - .with_inner_size(size) - .with_title("Shrooms VB"); - Arc::new(event_loop.create_window(attributes).unwrap()) - }; - - let size = window.inner_size(); - let hidpi_factor = window.scale_factor(); - let surface = instance.create_surface(window.clone()).unwrap(); - - let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::HighPerformance, - compatible_surface: Some(&surface), - force_fallback_adapter: false, - })) - .unwrap(); - - let (device, queue) = - block_on(adapter.request_device(&wgpu::DeviceDescriptor::default(), None)).unwrap(); - - // Set up swap chain - let surface_desc = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: wgpu::TextureFormat::Bgra8UnormSrgb, - width: size.width, - height: size.height, - present_mode: wgpu::PresentMode::Fifo, - desired_maximum_frame_latency: 2, - alpha_mode: wgpu::CompositeAlphaMode::Auto, - view_formats: vec![wgpu::TextureFormat::Bgra8Unorm], - }; - - surface.configure(&device, &surface_desc); - - let imgui = None; - Self { - device, - queue, - window, - surface_desc, - surface, - hidpi_factor, - imgui, - } - } - - fn setup_imgui(&mut self) { - let mut context = imgui::Context::create(); - let mut platform = imgui_winit_support::WinitPlatform::new(&mut context); - platform.attach_window( - context.io_mut(), - &self.window, - imgui_winit_support::HiDpiMode::Default, - ); - context.set_ini_filename(None); - - let font_size = (13.0 * self.hidpi_factor) as f32; - context.io_mut().font_global_scale = (1.0 / self.hidpi_factor) as f32; - - context.fonts().add_font(&[FontSource::DefaultFontData { - config: Some(imgui::FontConfig { - oversample_h: 1, - pixel_snap_h: true, - size_pixels: font_size, - ..Default::default() - }), - }]); - - // - // Set up dear imgui wgpu renderer - // - let clear_color = wgpu::Color::BLACK; - - let renderer_config = RendererConfig { - texture_format: self.surface_desc.format, - ..Default::default() - }; - - let renderer = Renderer::new(&mut context, &self.device, &self.queue, renderer_config); - let last_frame = Instant::now(); - let last_cursor = None; - - self.imgui = Some(ImguiState { - context, - platform, - renderer, - clear_color, - last_frame, - last_cursor, - }) - } - - fn new(event_loop: &ActiveEventLoop) -> Self { - let mut window = Self::setup_gpu(event_loop); - window.setup_imgui(); - window - } -} - -impl ApplicationHandler for App { - fn resumed(&mut self, event_loop: &ActiveEventLoop) { - self.window = Some(AppWindow::new(event_loop)); - } - - fn window_event( - &mut self, - event_loop: &ActiveEventLoop, - window_id: winit::window::WindowId, - event: WindowEvent, - ) { - let window = self.window.as_mut().unwrap(); - let imgui = window.imgui.as_mut().unwrap(); - let mut quit = false; - - match &event { - WindowEvent::Resized(size) => { - window.surface_desc = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: wgpu::TextureFormat::Bgra8UnormSrgb, - width: size.width, - height: size.height, - present_mode: wgpu::PresentMode::Fifo, - desired_maximum_frame_latency: 2, - alpha_mode: wgpu::CompositeAlphaMode::Auto, - view_formats: vec![wgpu::TextureFormat::Bgra8Unorm], - }; - - window - .surface - .configure(&window.device, &window.surface_desc); - } - WindowEvent::CloseRequested => event_loop.exit(), - WindowEvent::KeyboardInput { event, .. } => { - if let Key::Named(NamedKey::Escape) = event.logical_key { - if event.state.is_pressed() { - quit = true; - } - } - } - WindowEvent::RedrawRequested => { - let now = Instant::now(); - imgui - .context - .io_mut() - .update_delta_time(now - imgui.last_frame); - imgui.last_frame = now; - - let frame = match window.surface.get_current_texture() { - Ok(frame) => frame, - Err(e) => { - eprintln!("dropped frame: {e:?}"); - return; - } - }; - imgui - .platform - .prepare_frame(imgui.context.io_mut(), &window.window) - .expect("Failed to prepare frame"); - let ui = imgui.context.frame(); - ui.main_menu_bar(|| { - ui.menu("ROM", || { - if ui.menu_item("Open ROM") { - println!("clicked"); - } - if ui.menu_item("Quit") { - event_loop.exit(); - } - }); - ui.menu("Emulation", || { - if ui.menu_item("Pause") { - println!("clicked"); - } - if ui.menu_item("Reset") { - println!("clicked"); - } - }); - }); - - let mut encoder: wgpu::CommandEncoder = window - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - - if imgui.last_cursor != ui.mouse_cursor() { - imgui.last_cursor = ui.mouse_cursor(); - imgui.platform.prepare_render(ui, &window.window); - } - - let view = frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: None, - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(imgui.clear_color), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - imgui - .renderer - .render( - imgui.context.render(), - &window.queue, - &window.device, - &mut rpass, - ) - .expect("Rendering failed"); - - drop(rpass); - - window.queue.submit(Some(encoder.finish())); - - frame.present(); - } - _ => (), - } - - imgui.platform.handle_event::<()>( - imgui.context.io_mut(), - &window.window, - &Event::WindowEvent { window_id, event }, - ); - - if quit { - self.window.take(); - } - } - - fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: ()) { - let window = self.window.as_mut().unwrap(); - let imgui = window.imgui.as_mut().unwrap(); - imgui.platform.handle_event::<()>( - imgui.context.io_mut(), - &window.window, - &Event::UserEvent(event), - ); - } - - fn device_event( - &mut self, - _event_loop: &ActiveEventLoop, - device_id: winit::event::DeviceId, - event: winit::event::DeviceEvent, - ) { - let window = self.window.as_mut().unwrap(); - let imgui = window.imgui.as_mut().unwrap(); - imgui.platform.handle_event::<()>( - imgui.context.io_mut(), - &window.window, - &Event::DeviceEvent { device_id, event }, - ); - } - - fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { - let window = self.window.as_mut().unwrap(); - let imgui = window.imgui.as_mut().unwrap(); - window.window.request_redraw(); - imgui.platform.handle_event::<()>( - imgui.context.io_mut(), - &window.window, - &Event::AboutToWait, - ); - } -} - -fn main() { let event_loop = EventLoop::new().unwrap(); event_loop.set_control_flow(ControlFlow::Poll); - event_loop.run_app(&mut App::default()).unwrap(); + event_loop.run_app(&mut App::new(client))?; + Ok(()) } diff --git a/src/renderer.rs b/src/renderer.rs new file mode 100644 index 0000000..06dbe91 --- /dev/null +++ b/src/renderer.rs @@ -0,0 +1,58 @@ +use std::sync::Arc; + +use wgpu::{ + Extent3d, ImageCopyTexture, ImageDataLayout, Origin3d, Queue, Texture, TextureDescriptor, + TextureFormat, TextureUsages, +}; + +#[derive(Debug)] +pub struct GameRenderer { + pub queue: Arc, + pub eyes: [Arc; 2], +} + +impl GameRenderer { + pub fn render(&self, buffers: &[Vec; 2]) { + for (texture, buffer) in self.eyes.iter().zip(buffers) { + self.update_texture(texture, buffer); + } + } + fn update_texture(&self, texture: &Texture, buffer: &[u8]) { + let texture = ImageCopyTexture { + texture, + mip_level: 0, + origin: Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }; + let size = Extent3d { + width: 384, + height: 224, + depth_or_array_layers: 1, + }; + let data_layout = ImageDataLayout { + offset: 0, + bytes_per_row: Some(384), + rows_per_image: Some(224), + }; + self.queue.write_texture(texture, buffer, data_layout, size); + } + pub fn create_texture(device: &wgpu::Device, name: &str) -> Texture { + let desc = TextureDescriptor { + label: Some(name), + size: Extent3d { + width: 384, + height: 224, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: TextureFormat::R8Unorm, + usage: TextureUsages::COPY_SRC + | TextureUsages::COPY_DST + | TextureUsages::TEXTURE_BINDING, + view_formats: &[TextureFormat::R8Unorm], + }; + device.create_texture(&desc) + } +} diff --git a/src/shrooms_vb_core.rs b/src/shrooms_vb_core.rs index 2df7589..fa085ed 100644 --- a/src/shrooms_vb_core.rs +++ b/src/shrooms_vb_core.rs @@ -1,37 +1,159 @@ +use std::{ffi::c_void, ptr}; + +use anyhow::{anyhow, Result}; #[repr(C)] -struct VB { _data: [u8; 0] } +struct VB { + _data: [u8; 0], +} + +#[allow(non_camel_case_types)] +type c_int = i32; +type OnFrame = extern "C" fn(sim: *mut VB) -> c_int; #[link(name = "vb")] extern "C" { + #[link_name = "vbEmulate"] + fn vb_emulate(sim: *mut VB, cycles: *mut u32) -> c_int; + #[link_name = "vbGetCartROM"] + fn vb_get_cart_rom(sim: *mut VB, size: *mut u32) -> *mut c_void; + #[link_name = "vbGetPixels"] + fn vb_get_pixels( + sim: *mut VB, + left: *mut c_void, + left_stride_x: c_int, + left_stride_y: c_int, + right: *mut c_void, + right_stride_x: c_int, + right_stride_y: c_int, + ); + #[link_name = "vbGetUserData"] + fn vb_get_user_data(sim: *mut VB) -> *mut c_void; #[link_name = "vbInit"] fn vb_init(sim: *mut VB) -> *mut VB; - + #[link_name = "vbReset"] + fn vb_reset(sim: *mut VB); + #[link_name = "vbSetCartROM"] + fn vb_set_cart_rom(sim: *mut VB, rom: *mut c_void, size: u32) -> c_int; + #[link_name = "vbSetKeys"] + fn vb_set_keys(sim: *mut VB, keys: u16) -> u16; + #[link_name = "vbSetFrameCallback"] + fn vb_set_frame_callback(sim: *mut VB, on_frame: OnFrame); + #[link_name = "vbSetUserData"] + fn vb_set_user_data(sim: *mut VB, tag: *mut c_void); #[link_name = "vbSizeOf"] fn vb_size_of() -> usize; +} - #[link_name = "vbGetKeys"] - fn vb_get_keys(sim: *mut VB) -> u16; +extern "C" fn on_frame(sim: *mut VB) -> i32 { + // SAFETY: the *mut VB owns its userdata. + // There is no way for the userdata to be null or otherwise invalid. + let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() }; + data.frame_seen = true; + 1 +} + +struct VBState { + frame_seen: bool, } pub struct CoreVB { sim: *mut VB, - _data: Box<[u8]>, } +// SAFETY: the memory pointed to by sim is valid +unsafe impl Send for CoreVB {} + impl CoreVB { pub fn new() -> Self { + // init the VB instance itself let size = unsafe { vb_size_of() }; - let mut data = vec![0; size].into_boxed_slice(); - let sim = data.as_mut_ptr() as *mut VB; + // allocate a vec of u64 so that this memory is 8-byte aligned + let memory = vec![0u64; size.div_ceil(4)]; + let sim: *mut VB = Box::into_raw(memory.into_boxed_slice()).cast(); unsafe { vb_init(sim) }; - CoreVB { - sim, - _data: data + unsafe { vb_reset(sim) }; + + // set up userdata + let state = VBState { frame_seen: false }; + unsafe { vb_set_user_data(sim, Box::into_raw(Box::new(state)).cast()) }; + unsafe { vb_set_frame_callback(sim, on_frame) }; + + CoreVB { sim } + } + + pub fn load_rom(&mut self, rom: Vec) -> Result<()> { + self.unload_rom(); + + let size = rom.len() as u32; + let rom = Box::into_raw(rom.into_boxed_slice()).cast(); + let status = unsafe { vb_set_cart_rom(self.sim, rom, size) }; + if status == 0 { + Ok(()) + } else { + let _: Vec = + unsafe { Vec::from_raw_parts(rom.cast(), size as usize, size as usize) }; + Err(anyhow!("Invalid ROM size of {} bytes", size)) } } - pub fn keys(&self) -> u16 { - unsafe { vb_get_keys(self.sim) } + fn unload_rom(&mut self) -> Option> { + let mut size = 0; + let rom = unsafe { vb_get_cart_rom(self.sim, &mut size) }; + if rom.is_null() { + return None; + } + unsafe { vb_set_cart_rom(self.sim, ptr::null_mut(), 0) }; + let vec = unsafe { Vec::from_raw_parts(rom.cast(), size as usize, size as usize) }; + Some(vec) } -} \ No newline at end of file + + pub fn emulate_frame(&mut self) { + let mut cycles = 20_000_000; + unsafe { vb_emulate(self.sim, &mut cycles) }; + } + + pub fn read_pixels(&mut self, buffers: &mut [Vec; 2]) -> bool { + // SAFETY: the *mut VB owns its userdata. + // There is no way for the userdata to be null or otherwise invalid. + let data: &mut VBState = unsafe { &mut *vb_get_user_data(self.sim).cast() }; + if !data.frame_seen { + return false; + } + data.frame_seen = false; + + assert_eq!(buffers[0].len(), 384 * 224); + assert_eq!(buffers[1].len(), 384 * 224); + unsafe { + vb_get_pixels( + self.sim, + buffers[0].as_mut_ptr().cast(), + 1, + 384, + buffers[1].as_mut_ptr().cast(), + 1, + 384, + ); + }; + true + } + + pub fn set_keys(&mut self, keys: u16) { + unsafe { vb_set_keys(self.sim, keys) }; + } +} + +impl Drop for CoreVB { + fn drop(&mut self) { + // SAFETY: the *mut VB owns its userdata. + // There is no way for the userdata to be null or otherwise invalid. + let ptr: *mut VBState = unsafe { vb_get_user_data(self.sim).cast() }; + // SAFETY: we made this pointer ourselves, we can for sure free it + unsafe { drop(Box::from_raw(ptr)) }; + + let len = unsafe { vb_size_of() }.div_ceil(4); + // SAFETY: the sim's memory originally came from a Vec + let bytes: Vec = unsafe { Vec::from_raw_parts(self.sim.cast(), len, len) }; + drop(bytes); + } +} From 8fdff927eb43d1328a51d59f38b34f53df39ba6a Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 3 Nov 2024 12:44:38 -0500 Subject: [PATCH 03/20] Fix non-windows compilation --- src/app.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index 7919c6b..ef43c97 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,13 +3,14 @@ use imgui_wgpu::{Renderer, RendererConfig}; use imgui_winit_support::WinitPlatform; use pollster::block_on; use std::{num::NonZero, sync::Arc, time::Instant}; +#[cfg(target_os = "windows")] +use winit::platform::windows::{CornerPreference, WindowAttributesExtWindows as _}; use winit::{ application::ApplicationHandler, dpi::LogicalSize, event::{ElementState, Event, WindowEvent}, event_loop::ActiveEventLoop, keyboard::Key, - platform::windows::{CornerPreference, WindowAttributesExtWindows}, window::Window, }; @@ -51,8 +52,9 @@ impl AppWindow { let attributes = Window::default_attributes() .with_inner_size(size) - .with_title("Shrooms VB") - .with_corner_preference(CornerPreference::DoNotRound); + .with_title("Shrooms VB"); + #[cfg(target_os = "windows")] + let attributes = attributes.with_corner_preference(CornerPreference::DoNotRound); Arc::new(event_loop.create_window(attributes).unwrap()) }; From b025f7260467b3ffffff400f17c1caf2a23197cf Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 3 Nov 2024 13:25:20 -0500 Subject: [PATCH 04/20] Use one texture with two channels for video --- src/anaglyph.wgsl | 4 ++-- src/app.rs | 26 +++++++------------------- src/emulator.rs | 2 +- src/renderer.rs | 17 ++++++----------- src/shrooms_vb_core.rs | 18 +++++++++--------- 5 files changed, 25 insertions(+), 42 deletions(-) diff --git a/src/anaglyph.wgsl b/src/anaglyph.wgsl index 0015101..796e7b1 100644 --- a/src/anaglyph.wgsl +++ b/src/anaglyph.wgsl @@ -37,13 +37,13 @@ fn vs_main( // Fragment shader @group(0) @binding(0) -var u_textures: binding_array>; +var u_texture: texture_2d; @group(0) @binding(1) var u_sampler: sampler; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return textureSample(u_textures[0], u_sampler, in.tex_coords); + return textureSample(u_texture, u_sampler, in.tex_coords); // return vec4(0.3, 0.2, 0.1, 1.0); } diff --git a/src/app.rs b/src/app.rs index ef43c97..c196d8f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,7 +2,7 @@ use imgui::*; use imgui_wgpu::{Renderer, RendererConfig}; use imgui_winit_support::WinitPlatform; use pollster::block_on; -use std::{num::NonZero, sync::Arc, time::Instant}; +use std::{sync::Arc, time::Instant}; #[cfg(target_os = "windows")] use winit::platform::windows::{CornerPreference, WindowAttributesExtWindows as _}; use winit::{ @@ -69,27 +69,15 @@ impl AppWindow { })) .unwrap(); - let (device, queue) = block_on(adapter.request_device( - &wgpu::DeviceDescriptor { - required_features: wgpu::Features::TEXTURE_BINDING_ARRAY, - ..wgpu::DeviceDescriptor::default() - }, - None, - )) - .unwrap(); + let (device, queue) = + block_on(adapter.request_device(&wgpu::DeviceDescriptor::default(), None)).unwrap(); let queue = Arc::new(queue); - let eyes = [ - Arc::new(GameRenderer::create_texture(&device, "left eye")), - Arc::new(GameRenderer::create_texture(&device, "right eye")), - ]; + let eyes = Arc::new(GameRenderer::create_texture(&device, "eye")); client.send_command(EmulatorCommand::SetRenderer(GameRenderer { queue: queue.clone(), eyes: eyes.clone(), })); - let eyes = [ - eyes[0].create_view(&wgpu::TextureViewDescriptor::default()), - eyes[1].create_view(&wgpu::TextureViewDescriptor::default()), - ]; + let eyes = eyes.create_view(&wgpu::TextureViewDescriptor::default()); let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); let texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { @@ -103,7 +91,7 @@ impl AppWindow { view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, - count: NonZero::new(2), + count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, @@ -119,7 +107,7 @@ impl AppWindow { entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::TextureViewArray(&[&eyes[0], &eyes[1]]), + resource: wgpu::BindingResource::TextureView(&eyes), }, wgpu::BindGroupEntry { binding: 1, diff --git a/src/emulator.rs b/src/emulator.rs index 5f1e201..18100fd 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -33,7 +33,7 @@ impl Emulator { } pub fn run(&mut self) { - let mut eye_contents = [vec![0u8; 384 * 224], vec![0u8; 384 * 224]]; + let mut eye_contents = vec![0u8; 384 * 224 * 2]; loop { self.sim.emulate_frame(); if let Some(renderer) = &mut self.renderer { diff --git a/src/renderer.rs b/src/renderer.rs index 06dbe91..a97e9e7 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -8,18 +8,13 @@ use wgpu::{ #[derive(Debug)] pub struct GameRenderer { pub queue: Arc, - pub eyes: [Arc; 2], + pub eyes: Arc, } impl GameRenderer { - pub fn render(&self, buffers: &[Vec; 2]) { - for (texture, buffer) in self.eyes.iter().zip(buffers) { - self.update_texture(texture, buffer); - } - } - fn update_texture(&self, texture: &Texture, buffer: &[u8]) { + pub fn render(&self, buffer: &[u8]) { let texture = ImageCopyTexture { - texture, + texture: &self.eyes, mip_level: 0, origin: Origin3d::ZERO, aspect: wgpu::TextureAspect::All, @@ -31,7 +26,7 @@ impl GameRenderer { }; let data_layout = ImageDataLayout { offset: 0, - bytes_per_row: Some(384), + bytes_per_row: Some(384 * 2), rows_per_image: Some(224), }; self.queue.write_texture(texture, buffer, data_layout, size); @@ -47,11 +42,11 @@ impl GameRenderer { mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: TextureFormat::R8Unorm, + format: TextureFormat::Rg8Unorm, usage: TextureUsages::COPY_SRC | TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING, - view_formats: &[TextureFormat::R8Unorm], + view_formats: &[TextureFormat::Rg8Unorm], }; device.create_texture(&desc) } diff --git a/src/shrooms_vb_core.rs b/src/shrooms_vb_core.rs index fa085ed..a28f386 100644 --- a/src/shrooms_vb_core.rs +++ b/src/shrooms_vb_core.rs @@ -113,7 +113,7 @@ impl CoreVB { unsafe { vb_emulate(self.sim, &mut cycles) }; } - pub fn read_pixels(&mut self, buffers: &mut [Vec; 2]) -> bool { + pub fn read_pixels(&mut self, buffers: &mut [u8]) -> bool { // SAFETY: the *mut VB owns its userdata. // There is no way for the userdata to be null or otherwise invalid. let data: &mut VBState = unsafe { &mut *vb_get_user_data(self.sim).cast() }; @@ -122,17 +122,17 @@ impl CoreVB { } data.frame_seen = false; - assert_eq!(buffers[0].len(), 384 * 224); - assert_eq!(buffers[1].len(), 384 * 224); + // the buffer must be big enough for our data + assert!(buffers.len() >= 384 * 224 * 2); unsafe { vb_get_pixels( self.sim, - buffers[0].as_mut_ptr().cast(), - 1, - 384, - buffers[1].as_mut_ptr().cast(), - 1, - 384, + buffers.as_mut_ptr().cast(), + 2, + 384 * 2, + buffers.as_mut_ptr().offset(1).cast(), + 2, + 384 * 2, ); }; true From a0e39796bf352f184114653a5283ba129febcacf Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 3 Nov 2024 13:49:10 -0500 Subject: [PATCH 05/20] Actually do anaglyph --- Cargo.lock | 15 +++++++++++++++ Cargo.toml | 1 + src/anaglyph.wgsl | 10 ++++++++-- src/app.rs | 31 +++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 023dbe0..7a98704 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,6 +220,20 @@ name = "bytemuck" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] [[package]] name = "bytes" @@ -1489,6 +1503,7 @@ name = "shrooms-vb-native" version = "0.1.0" dependencies = [ "anyhow", + "bytemuck", "cc", "clap", "imgui", diff --git a/Cargo.toml b/Cargo.toml index 2b1203f..8820305 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] anyhow = "1" +bytemuck = { version = "1", features = ["derive"] } clap = { version = "4", features = ["derive"] } imgui = "0.12" imgui-wgpu = { git = "https://github.com/Yatekii/imgui-wgpu-rs", rev = "2edd348" } diff --git a/src/anaglyph.wgsl b/src/anaglyph.wgsl index 796e7b1..0483665 100644 --- a/src/anaglyph.wgsl +++ b/src/anaglyph.wgsl @@ -41,9 +41,15 @@ var u_texture: texture_2d; @group(0) @binding(1) var u_sampler: sampler; +struct Colors { + left: vec4, + right: vec4, +}; +@group(0) @binding(2) +var colors: Colors; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return textureSample(u_texture, u_sampler, in.tex_coords); - // return vec4(0.3, 0.2, 0.1, 1.0); + let brt = textureSample(u_texture, u_sampler, in.tex_coords); + return colors.left * brt[0] + colors.right * brt[1]; } diff --git a/src/app.rs b/src/app.rs index c196d8f..ae029ef 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,6 +3,7 @@ use imgui_wgpu::{Renderer, RendererConfig}; use imgui_winit_support::WinitPlatform; use pollster::block_on; use std::{sync::Arc, time::Instant}; +use wgpu::util::DeviceExt as _; #[cfg(target_os = "windows")] use winit::platform::windows::{CornerPreference, WindowAttributesExtWindows as _}; use winit::{ @@ -79,6 +80,15 @@ impl AppWindow { })); let eyes = eyes.create_view(&wgpu::TextureViewDescriptor::default()); let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); + let colors = Colors { + left: [1.0, 0.0, 0.0, 1.0], + right: [0.0, 0.7734375, 0.9375, 1.0], + }; + let color_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("colors"), + contents: bytemuck::bytes_of(&colors), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); let texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("texture bind group layout"), @@ -99,6 +109,16 @@ impl AppWindow { ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, ], }); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { @@ -113,6 +133,10 @@ impl AppWindow { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler), }, + wgpu::BindGroupEntry { + binding: 2, + resource: color_buf.as_entire_binding(), + }, ], }); @@ -438,3 +462,10 @@ impl ApplicationHandler for App { ); } } + +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +struct Colors { + left: [f32; 4], + right: [f32; 4], +} From 8bba7b9e1b1e60971be1fb9cf7cca4d68de3c3c4 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 3 Nov 2024 14:02:25 -0500 Subject: [PATCH 06/20] Turn off vulkan because it's slow on windows --- src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index ae029ef..134a2b4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -44,7 +44,7 @@ struct AppWindow { impl AppWindow { fn setup_gpu(event_loop: &ActiveEventLoop, client: &EmulatorClient) -> Self { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::PRIMARY, + backends: wgpu::Backends::PRIMARY.difference(wgpu::Backends::VULKAN), ..Default::default() }); From 70373647fbddf55bb70d85c402c7b82776cb9b1a Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 4 Nov 2024 09:59:58 -0500 Subject: [PATCH 07/20] Add audio, fix timing --- Cargo.lock | 378 ++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 6 + src/audio.rs | 101 +++++++++++ src/emulator.rs | 55 +++++- src/main.rs | 8 +- src/shrooms_vb_core.rs | 66 ++++++- 6 files changed, 597 insertions(+), 17 deletions(-) create mode 100644 src/audio.rs diff --git a/Cargo.lock b/Cargo.lock index 7a98704..2edeed5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,12 +31,43 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.6.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "android-activity" version = "0.6.0" @@ -51,7 +82,7 @@ dependencies = [ "jni-sys", "libc", "log", - "ndk", + "ndk 0.9.0", "ndk-context", "ndk-sys 0.6.0+11769913", "num_enum", @@ -167,6 +198,24 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.87", +] + [[package]] name = "bit-set" version = "0.6.0" @@ -284,6 +333,15 @@ 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" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -308,6 +366,17 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e10e7569f6ca78ef7664d7d651115172d4875c4410c050306bccde856a99a49" +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.20" @@ -454,6 +523,49 @@ dependencies = [ "libc", ] +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce857aa0b77d77287acc1ac3e37a05a8c95a2af3647d23b15f263bdaeb7562b" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk 0.8.0", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -477,6 +589,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + [[package]] name = "dispatch" version = "0.2.0" @@ -513,6 +631,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "equivalent" version = "1.0.1" @@ -588,6 +712,12 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "glow" version = "0.13.1" @@ -638,7 +768,7 @@ dependencies = [ "presser", "thiserror", "winapi", - "windows", + "windows 0.52.0", ] [[package]] @@ -773,6 +903,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "jni" version = "0.21.1" @@ -885,6 +1024,15 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -924,6 +1072,12 @@ dependencies = [ "paste", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mint" version = "0.5.9" @@ -951,6 +1105,20 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.6.0", + "jni-sys", + "log", + "ndk-sys 0.5.0+25.2.9519653", + "num_enum", + "thiserror", +] + [[package]] name = "ndk" version = "0.9.0" @@ -990,6 +1158,54 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_enum" version = "0.7.3" @@ -1223,6 +1439,29 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni", + "ndk 0.8.0", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -1341,6 +1580,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -1395,6 +1643,15 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "realfft" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390252372b7f2aac8360fc5e72eba10136b166d6faeed97e6d0c8324eb99b2b1" +dependencies = [ + "rustfft", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1413,18 +1670,80 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "renderdoc-sys" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "rtrb" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f94e84c073f3b85d4012b44722fa8842b9986d741590d4f2636ad0a5b14143" + +[[package]] +name = "rubato" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd96992d7e24b3d7f35fdfe02af037a356ac90d41b466945cf3333525a86eea" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "realfft", +] + [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustfft" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43806561bc506d0c5d160643ad742e3161049ac01027b5e6d7524091fd401d86" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", + "version_check", +] + [[package]] name = "rustix" version = "0.38.38" @@ -1506,10 +1825,16 @@ dependencies = [ "bytemuck", "cc", "clap", + "cpal", "imgui", "imgui-wgpu", "imgui-winit-support", + "itertools", + "num-derive", + "num-traits", "pollster", + "rtrb", + "rubato", "wgpu", "winit", ] @@ -1587,6 +1912,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + [[package]] name = "strict-num" version = "0.1.1" @@ -1708,6 +2039,16 @@ version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + [[package]] name = "ttf-parser" version = "0.25.0" @@ -2111,7 +2452,17 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ - "windows-core", + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", "windows-targets 0.52.6", ] @@ -2124,6 +2475,25 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -2351,7 +2721,7 @@ dependencies = [ "js-sys", "libc", "memmap2", - "ndk", + "ndk 0.9.0", "objc2", "objc2-app-kit", "objc2-foundation", diff --git a/Cargo.toml b/Cargo.toml index 8820305..51aceec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,10 +7,16 @@ edition = "2021" anyhow = "1" bytemuck = { version = "1", features = ["derive"] } clap = { version = "4", features = ["derive"] } +cpal = "0.15" imgui = "0.12" imgui-wgpu = { git = "https://github.com/Yatekii/imgui-wgpu-rs", rev = "2edd348" } imgui-winit-support = "0.13" +itertools = "0.13" +num-derive = "0.4" +num-traits = "0.2" pollster = "0.4" +rtrb = "0.3" +rubato = "0.16" wgpu = "22.1" winit = "0.30" diff --git a/src/audio.rs b/src/audio.rs new file mode 100644 index 0000000..2486830 --- /dev/null +++ b/src/audio.rs @@ -0,0 +1,101 @@ +use anyhow::{bail, Result}; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use itertools::Itertools; +use rubato::{FftFixedInOut, Resampler}; + +pub struct Audio { + #[allow(unused)] + stream: cpal::Stream, + sampler: FftFixedInOut, + input_buffer: Vec>, + output_buffer: Vec>, + sample_sink: rtrb::Producer, +} + +impl Audio { + pub fn init() -> Result { + let host = cpal::default_host(); + let Some(device) = host.default_output_device() else { + bail!("No output device available"); + }; + let Some(config) = device + .supported_output_configs()? + .find(|c| c.channels() == 2 && c.sample_format().is_float()) + else { + bail!("No suitable output config available"); + }; + let mut config = config.with_max_sample_rate().config(); + let sampler = FftFixedInOut::new(41700, config.sample_rate.0 as usize, 834, 2)?; + config.buffer_size = cpal::BufferSize::Fixed(sampler.output_frames_max() as u32); + + let input_buffer = sampler.input_buffer_allocate(true); + let output_buffer = sampler.output_buffer_allocate(true); + let (sample_sink, mut sample_source) = + rtrb::RingBuffer::new(sampler.output_frames_max() * 4); + + let stream = device.build_output_stream( + &config, + move |data: &mut [f32], _| { + let requested = data.len(); + let chunk = match sample_source.read_chunk(data.len()) { + Ok(c) => c, + Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => { + sample_source.read_chunk(n).unwrap() + } + }; + let len = chunk.len(); + let (first, second) = chunk.as_slices(); + data[0..first.len()].copy_from_slice(first); + data[first.len()..len].copy_from_slice(second); + for rest in &mut data[len..requested] { + *rest = 0.0; + } + chunk.commit_all(); + }, + move |err| eprintln!("stream error: {err}"), + None, + )?; + stream.play()?; + Ok(Self { + stream, + sampler, + input_buffer, + output_buffer, + sample_sink, + }) + } + + pub fn update(&mut self, samples: &[f32]) { + for sample in samples.chunks_exact(2) { + for (channel, value) in self.input_buffer.iter_mut().zip(sample) { + channel.push(*value); + } + if self.input_buffer[0].len() >= self.sampler.input_frames_next() { + let (_, output_samples) = self + .sampler + .process_into_buffer(&self.input_buffer, &mut self.output_buffer, None) + .unwrap(); + + let chunk = match self.sample_sink.write_chunk_uninit(output_samples * 2) { + Ok(c) => c, + Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => { + self.sample_sink.write_chunk_uninit(n).unwrap() + } + }; + let interleaved = self.output_buffer[0] + .iter() + .interleave(self.output_buffer[1].iter()) + .cloned(); + chunk.fill_from_iter(interleaved); + + for channel in &mut self.input_buffer { + channel.clear(); + } + } + } + + while self.sample_sink.slots() < self.sampler.output_frames_max() * 2 { + std::hint::spin_loop(); + } + } +} diff --git a/src/emulator.rs b/src/emulator.rs index 18100fd..e1a4836 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -1,29 +1,60 @@ use std::{ fs, - path::Path, + path::{Path, PathBuf}, sync::mpsc::{self, TryRecvError}, }; use anyhow::Result; -use crate::{renderer::GameRenderer, shrooms_vb_core::CoreVB}; +use crate::{audio::Audio, renderer::GameRenderer, shrooms_vb_core::CoreVB}; + +pub struct EmulatorBuilder { + rom: Option, + commands: mpsc::Receiver, +} + +impl EmulatorBuilder { + pub fn new() -> (Self, EmulatorClient) { + let (queue, commands) = mpsc::channel(); + let builder = Self { + rom: None, + commands, + }; + let client = EmulatorClient { queue }; + (builder, client) + } + + pub fn with_rom(self, path: &Path) -> Self { + Self { + rom: Some(path.into()), + ..self + } + } + + pub fn build(self) -> Result { + let mut emulator = Emulator::new(self.commands)?; + if let Some(path) = self.rom { + emulator.load_rom(&path)?; + } + Ok(emulator) + } +} pub struct Emulator { sim: CoreVB, + audio: Audio, commands: mpsc::Receiver, renderer: Option, } impl Emulator { - pub fn new() -> (Self, EmulatorClient) { - let (sink, source) = mpsc::channel(); - let emu = Emulator { + fn new(commands: mpsc::Receiver) -> Result { + Ok(Self { sim: CoreVB::new(), - commands: source, + audio: Audio::init()?, + commands, renderer: None, - }; - let queue = EmulatorClient { queue: sink }; - (emu, queue) + }) } pub fn load_rom(&mut self, path: &Path) -> Result<()> { @@ -34,6 +65,7 @@ impl Emulator { pub fn run(&mut self) { let mut eye_contents = vec![0u8; 384 * 224 * 2]; + let mut audio_samples = vec![]; loop { self.sim.emulate_frame(); if let Some(renderer) = &mut self.renderer { @@ -41,6 +73,11 @@ impl Emulator { renderer.render(&eye_contents); } } + self.sim.read_samples(&mut audio_samples); + if !audio_samples.is_empty() { + self.audio.update(&audio_samples); + audio_samples.clear(); + } loop { match self.commands.try_recv() { Ok(command) => self.handle_command(command), diff --git a/src/main.rs b/src/main.rs index 8f625d1..b066de0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,11 @@ use std::{path::PathBuf, thread}; use anyhow::Result; use app::App; use clap::Parser; -use emulator::Emulator; +use emulator::EmulatorBuilder; use winit::event_loop::{ControlFlow, EventLoop}; mod app; +mod audio; mod emulator; mod renderer; mod shrooms_vb_core; @@ -19,9 +20,10 @@ struct Args { fn main() -> Result<()> { let args = Args::parse(); - let (mut emulator, client) = Emulator::new(); - emulator.load_rom(&args.rom)?; + let (builder, client) = EmulatorBuilder::new(); + let builder = builder.with_rom(&args.rom); thread::spawn(move || { + let mut emulator = builder.build().unwrap(); emulator.run(); }); diff --git a/src/shrooms_vb_core.rs b/src/shrooms_vb_core.rs index a28f386..90acc21 100644 --- a/src/shrooms_vb_core.rs +++ b/src/shrooms_vb_core.rs @@ -1,6 +1,7 @@ -use std::{ffi::c_void, ptr}; +use std::{ffi::c_void, ptr, slice}; use anyhow::{anyhow, Result}; +use num_derive::{FromPrimitive, ToPrimitive}; #[repr(C)] struct VB { @@ -9,6 +10,20 @@ struct VB { #[allow(non_camel_case_types)] type c_int = i32; +#[allow(non_camel_case_types)] +type c_uint = u32; + +#[repr(u32)] +#[derive(FromPrimitive, ToPrimitive)] +enum VBDataType { + S8 = 0, + U8 = 1, + S16 = 2, + U16 = 3, + S32 = 4, + F32 = 5, +} + type OnFrame = extern "C" fn(sim: *mut VB) -> c_int; #[link(name = "vb")] @@ -27,6 +42,13 @@ extern "C" { right_stride_x: c_int, right_stride_y: c_int, ); + #[link_name = "vbGetSamples"] + fn vb_get_samples( + sim: *mut VB, + typ_: *mut VBDataType, + capacity: *mut c_uint, + position: *mut c_uint, + ) -> *mut c_void; #[link_name = "vbGetUserData"] fn vb_get_user_data(sim: *mut VB) -> *mut c_void; #[link_name = "vbInit"] @@ -39,6 +61,13 @@ extern "C" { fn vb_set_keys(sim: *mut VB, keys: u16) -> u16; #[link_name = "vbSetFrameCallback"] fn vb_set_frame_callback(sim: *mut VB, on_frame: OnFrame); + #[link_name = "vbSetSamples"] + fn vb_set_samples( + sim: *mut VB, + samples: *mut c_void, + typ_: VBDataType, + capacity: c_uint, + ) -> c_int; #[link_name = "vbSetUserData"] fn vb_set_user_data(sim: *mut VB, tag: *mut c_void); #[link_name = "vbSizeOf"] @@ -53,6 +82,9 @@ extern "C" fn on_frame(sim: *mut VB) -> i32 { 1 } +const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4; +const AUDIO_CAPACITY_FLOATS: usize = AUDIO_CAPACITY_SAMPLES * 2; + struct VBState { frame_seen: bool, } @@ -79,6 +111,11 @@ impl CoreVB { unsafe { vb_set_user_data(sim, Box::into_raw(Box::new(state)).cast()) }; unsafe { vb_set_frame_callback(sim, on_frame) }; + // set up audio buffer + let audio_buffer = vec![0.0f32; AUDIO_CAPACITY_FLOATS]; + let samples: *mut c_void = Box::into_raw(audio_buffer.into_boxed_slice()).cast(); + unsafe { vb_set_samples(sim, samples, VBDataType::F32, AUDIO_CAPACITY_SAMPLES as u32) }; + CoreVB { sim } } @@ -138,6 +175,25 @@ impl CoreVB { true } + pub fn read_samples(&mut self, samples: &mut Vec) { + let mut position = 0; + let ptr = + unsafe { vb_get_samples(self.sim, ptr::null_mut(), ptr::null_mut(), &mut position) }; + // SAFETY: position is an offset in a buffer of (f32, f32). so, position * 2 is an offset in a buffer of f32. + let read_samples: &mut [f32] = + unsafe { slice::from_raw_parts_mut(ptr.cast(), position as usize * 2) }; + samples.extend_from_slice(read_samples); + + unsafe { + vb_set_samples( + self.sim, + ptr, + VBDataType::F32, + AUDIO_CAPACITY_SAMPLES as u32, + ) + }; + } + pub fn set_keys(&mut self, keys: u16) { unsafe { vb_set_keys(self.sim, keys) }; } @@ -145,6 +201,14 @@ impl CoreVB { impl Drop for CoreVB { fn drop(&mut self) { + let ptr = + unsafe { vb_get_samples(self.sim, ptr::null_mut(), ptr::null_mut(), ptr::null_mut()) }; + // SAFETY: the audio buffer originally came from a Vec + let floats: Vec = unsafe { + Vec::from_raw_parts(ptr.cast(), AUDIO_CAPACITY_FLOATS, AUDIO_CAPACITY_FLOATS) + }; + drop(floats); + // SAFETY: the *mut VB owns its userdata. // There is no way for the userdata to be null or otherwise invalid. let ptr: *mut VBState = unsafe { vb_get_user_data(self.sim).cast() }; From 7c9e9c7fa44f1a5c10b39ad00c2cf060a3e8beff Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 4 Nov 2024 10:00:12 -0500 Subject: [PATCH 08/20] Turn off LTO for now, makes builds quite slow --- Cargo.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 51aceec..6a848fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,3 @@ winit = "0.30" [build-dependencies] cc = "1" - -[profile.release] -lto = "thin" \ No newline at end of file From 756835f90e5c778fb132f7220a3602fa5be3f261 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 4 Nov 2024 21:53:59 -0500 Subject: [PATCH 09/20] Turn vulkan back on because my linux partition needs it --- src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index 134a2b4..ae029ef 100644 --- a/src/app.rs +++ b/src/app.rs @@ -44,7 +44,7 @@ struct AppWindow { impl AppWindow { fn setup_gpu(event_loop: &ActiveEventLoop, client: &EmulatorClient) -> Self { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::PRIMARY.difference(wgpu::Backends::VULKAN), + backends: wgpu::Backends::PRIMARY, ..Default::default() }); From 593475960d5459ba91b4bf54aa6573fcc39ab3c1 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 4 Nov 2024 22:18:57 -0500 Subject: [PATCH 10/20] Make menu items functional --- Cargo.lock | 182 +++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + src/app.rs | 18 +++- src/emulator.rs | 53 ++++++++++-- src/main.rs | 18 ++-- src/shrooms_vb_core.rs | 4 + 6 files changed, 255 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2edeed5..3944f49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -177,6 +177,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + [[package]] name = "ash" version = "0.38.0+1.3.281" @@ -207,7 +213,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "proc-macro2", "quote", "regex", @@ -417,6 +423,36 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -595,6 +631,27 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -840,6 +897,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "imgui" version = "0.12.0" @@ -903,6 +969,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -1105,6 +1180,29 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "native-dialog" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e7038885d2aeab236bd60da9e159a5967b47cde3292da3b15ff1bec27c039f" +dependencies = [ + "ascii", + "block", + "cocoa", + "core-foundation", + "dirs-next", + "objc", + "objc-foundation", + "objc_id", + "once_cell", + "raw-window-handle 0.5.2", + "thiserror", + "versions", + "wfd", + "which", + "winapi", +] + [[package]] name = "ndk" version = "0.8.0" @@ -1130,7 +1228,7 @@ dependencies = [ "log", "ndk-sys 0.6.0+11769913", "num_enum", - "raw-window-handle", + "raw-window-handle 0.6.2", "thiserror", ] @@ -1236,6 +1334,17 @@ dependencies = [ "malloc_buf", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -1439,6 +1548,15 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "oboe" version = "0.6.1" @@ -1637,6 +1755,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -1670,6 +1794,17 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.11.1" @@ -1829,7 +1964,8 @@ dependencies = [ "imgui", "imgui-wgpu", "imgui-winit-support", - "itertools", + "itertools 0.13.0", + "native-dialog", "num-derive", "num-traits", "pollster", @@ -2091,6 +2227,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "versions" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c73a36bc44e3039f51fbee93e39f41225f6b17b380eb70cc2aab942df06b34dd" +dependencies = [ + "itertools 0.11.0", + "nom", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -2303,6 +2449,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "wfd" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e713040b67aae5bf1a0ae3e1ebba8cc29ab2b90da9aa1bff6e09031a8a41d7a8" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "wgpu" version = "22.1.0" @@ -2317,7 +2473,7 @@ dependencies = [ "naga", "parking_lot", "profiling", - "raw-window-handle", + "raw-window-handle 0.6.2", "smallvec", "static_assertions", "wasm-bindgen", @@ -2345,7 +2501,7 @@ dependencies = [ "once_cell", "parking_lot", "profiling", - "raw-window-handle", + "raw-window-handle 0.6.2", "rustc-hash", "smallvec", "thiserror", @@ -2387,7 +2543,7 @@ dependencies = [ "parking_lot", "profiling", "range-alloc", - "raw-window-handle", + "raw-window-handle 0.6.2", "renderdoc-sys", "rustc-hash", "smallvec", @@ -2409,6 +2565,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "widestring" version = "1.1.0" @@ -2729,7 +2897,7 @@ dependencies = [ "orbclient", "percent-encoding", "pin-project", - "raw-window-handle", + "raw-window-handle 0.6.2", "redox_syscall 0.4.1", "rustix", "sctk-adwaita", diff --git a/Cargo.toml b/Cargo.toml index 6a848fd..e2c6317 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ imgui = "0.12" imgui-wgpu = { git = "https://github.com/Yatekii/imgui-wgpu-rs", rev = "2edd348" } imgui-winit-support = "0.13" itertools = "0.13" +native-dialog = "0.7" num-derive = "0.4" num-traits = "0.2" pollster = "0.4" diff --git a/src/app.rs b/src/app.rs index ae029ef..3403b3b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -350,18 +350,28 @@ impl ApplicationHandler for App { height = ui.window_size()[1]; ui.menu("ROM", || { if ui.menu_item("Open ROM") { - println!("clicked"); + let rom = native_dialog::FileDialog::new() + .add_filter("Virtual Boy ROMs", &["vb", "vbrom"]) + .show_open_single_file() + .unwrap(); + if let Some(path) = rom { + self.client.send_command(EmulatorCommand::LoadGame(path)); + } } if ui.menu_item("Quit") { event_loop.exit(); } }); ui.menu("Emulation", || { - if ui.menu_item("Pause") { - println!("clicked"); + if self.client.is_running() { + if ui.menu_item("Pause") { + self.client.send_command(EmulatorCommand::Pause); + } + } else if ui.menu_item("Resume") { + self.client.send_command(EmulatorCommand::Resume); } if ui.menu_item("Reset") { - println!("clicked"); + self.client.send_command(EmulatorCommand::Reset); } }); }); diff --git a/src/emulator.rs b/src/emulator.rs index e1a4836..e87a9b4 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -1,7 +1,11 @@ use std::{ fs, path::{Path, PathBuf}, - sync::mpsc::{self, TryRecvError}, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc::{self, TryRecvError}, + Arc, + }, }; use anyhow::Result; @@ -11,6 +15,7 @@ use crate::{audio::Audio, renderer::GameRenderer, shrooms_vb_core::CoreVB}; pub struct EmulatorBuilder { rom: Option, commands: mpsc::Receiver, + running: Arc, } impl EmulatorBuilder { @@ -19,8 +24,12 @@ impl EmulatorBuilder { let builder = Self { rom: None, commands, + running: Arc::new(AtomicBool::new(false)), + }; + let client = EmulatorClient { + queue, + running: builder.running.clone(), }; - let client = EmulatorClient { queue }; (builder, client) } @@ -32,7 +41,7 @@ impl EmulatorBuilder { } pub fn build(self) -> Result { - let mut emulator = Emulator::new(self.commands)?; + let mut emulator = Emulator::new(self.commands, self.running)?; if let Some(path) = self.rom { emulator.load_rom(&path)?; } @@ -45,21 +54,28 @@ pub struct Emulator { audio: Audio, commands: mpsc::Receiver, renderer: Option, + running: Arc, + has_game: bool, } impl Emulator { - fn new(commands: mpsc::Receiver) -> Result { + fn new(commands: mpsc::Receiver, running: Arc) -> Result { Ok(Self { sim: CoreVB::new(), audio: Audio::init()?, commands, renderer: None, + running, + has_game: false, }) } pub fn load_rom(&mut self, path: &Path) -> Result<()> { let bytes = fs::read(path)?; + self.sim.reset(); self.sim.load_rom(bytes)?; + self.has_game = true; + self.running.store(true, Ordering::Release); Ok(()) } @@ -67,7 +83,9 @@ impl Emulator { let mut eye_contents = vec![0u8; 384 * 224 * 2]; let mut audio_samples = vec![]; loop { - self.sim.emulate_frame(); + if self.running.load(Ordering::Acquire) { + self.sim.emulate_frame(); + } if let Some(renderer) = &mut self.renderer { if self.sim.read_pixels(&mut eye_contents) { renderer.render(&eye_contents); @@ -97,6 +115,23 @@ impl Emulator { EmulatorCommand::SetRenderer(renderer) => { self.renderer = Some(renderer); } + EmulatorCommand::LoadGame(path) => { + if let Err(error) = self.load_rom(&path) { + eprintln!("error loading rom: {}", error); + } + } + EmulatorCommand::Pause => { + self.running.store(false, Ordering::Release); + } + EmulatorCommand::Resume => { + if self.has_game { + self.running.store(true, Ordering::Relaxed); + } + } + EmulatorCommand::Reset => { + self.sim.reset(); + self.running.store(true, Ordering::Release); + } EmulatorCommand::PressStart => { self.sim.set_keys(0x1003); } @@ -110,15 +145,23 @@ impl Emulator { #[derive(Debug)] pub enum EmulatorCommand { SetRenderer(GameRenderer), + LoadGame(PathBuf), + Pause, + Resume, + Reset, PressStart, ReleaseStart, } pub struct EmulatorClient { queue: mpsc::Sender, + running: Arc, } impl EmulatorClient { + pub fn is_running(&self) -> bool { + self.running.load(Ordering::Acquire) + } pub fn send_command(&self, command: EmulatorCommand) { if let Err(err) = self.queue.send(command) { eprintln!( diff --git a/src/main.rs b/src/main.rs index b066de0..474bad5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::{path::PathBuf, thread}; +use std::{path::PathBuf, process, thread}; use anyhow::Result; use app::App; @@ -14,16 +14,24 @@ mod shrooms_vb_core; #[derive(Parser)] struct Args { - rom: PathBuf, + rom: Option, } fn main() -> Result<()> { let args = Args::parse(); - let (builder, client) = EmulatorBuilder::new(); - let builder = builder.with_rom(&args.rom); + let (mut builder, client) = EmulatorBuilder::new(); + if let Some(path) = args.rom { + builder = builder.with_rom(&path); + } thread::spawn(move || { - let mut emulator = builder.build().unwrap(); + let mut emulator = match builder.build() { + Ok(e) => e, + Err(err) => { + eprintln!("Error initializing emulator: {err}"); + process::exit(1); + } + }; emulator.run(); }); diff --git a/src/shrooms_vb_core.rs b/src/shrooms_vb_core.rs index 90acc21..19ce04b 100644 --- a/src/shrooms_vb_core.rs +++ b/src/shrooms_vb_core.rs @@ -119,6 +119,10 @@ impl CoreVB { CoreVB { sim } } + pub fn reset(&mut self) { + unsafe { vb_reset(self.sim) }; + } + pub fn load_rom(&mut self, rom: Vec) -> Result<()> { self.unload_rom(); From 75fa3be25cb574c85e1ebb14612799af44a25398 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Mon, 4 Nov 2024 23:39:00 -0500 Subject: [PATCH 11/20] Support resizing the window --- src/app.rs | 67 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/src/app.rs b/src/app.rs index 3403b3b..a2be00c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,7 +8,7 @@ use wgpu::util::DeviceExt as _; use winit::platform::windows::{CornerPreference, WindowAttributesExtWindows as _}; use winit::{ application::ApplicationHandler, - dpi::LogicalSize, + dpi::{LogicalSize, PhysicalSize}, event::{ElementState, Event, WindowEvent}, event_loop::ActiveEventLoop, keyboard::Key, @@ -297,17 +297,8 @@ impl ApplicationHandler for App { match &event { WindowEvent::Resized(size) => { - window.surface_desc = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: wgpu::TextureFormat::Bgra8UnormSrgb, - width: size.width, - height: size.height, - present_mode: wgpu::PresentMode::Fifo, - desired_maximum_frame_latency: 2, - alpha_mode: wgpu::CompositeAlphaMode::Auto, - view_formats: vec![wgpu::TextureFormat::Bgra8Unorm], - }; - + window.surface_desc.width = size.width; + window.surface_desc.height = size.height; window .surface .configure(&window.device, &window.surface_desc); @@ -345,9 +336,9 @@ impl ApplicationHandler for App { .prepare_frame(imgui.context.io_mut(), &window.window) .expect("Failed to prepare frame"); let ui = imgui.context.frame(); - let mut height = 0.0; + let mut menu_height = 0.0; ui.main_menu_bar(|| { - height = ui.window_size()[1]; + menu_height = ui.window_size()[1]; ui.menu("ROM", || { if ui.menu_item("Open ROM") { let rom = native_dialog::FileDialog::new() @@ -374,6 +365,27 @@ impl ApplicationHandler for App { self.client.send_command(EmulatorCommand::Reset); } }); + ui.menu("Video", || { + let current_dims = PhysicalSize::new( + window.surface_desc.width, + window.surface_desc.height, + ) + .to_logical(window.hidpi_factor); + for scale in 1..=4 { + let label = format!("x{scale}"); + let dims = LogicalSize::new(384 * scale, 224 * scale + 20); + let selected = dims == current_dims; + if ui.menu_item_config(label).selected(selected).build() { + if let Some(size) = window.window.request_inner_size(dims) { + window.surface_desc.width = size.width; + window.surface_desc.height = size.height; + window + .surface + .configure(&window.device, &window.surface_desc); + } + } + } + }); }); let mut encoder: wgpu::CommandEncoder = window @@ -403,13 +415,19 @@ impl ApplicationHandler for App { occlusion_query_set: None, }); + // Draw the game rpass.set_pipeline(&window.pipeline); - let hidpi = window.hidpi_factor as f32; - rpass.set_viewport(0.0, height * hidpi, 384.0 * hidpi, 224.0 * hidpi, 0.0, 1.0); + let window_width = window.surface_desc.width as f32; + let window_height = window.surface_desc.height as f32; + let menu_height = menu_height * window.hidpi_factor as f32; + let ((x, y), (width, height)) = + compute_game_bounds(window_width, window_height, menu_height); + rpass.set_viewport(x, y, width, height, 0.0, 1.0); rpass.set_bind_group(0, &window.bind_group, &[]); rpass.draw(0..6, 0..1); - rpass.set_viewport(0.0, 0.0, 384.0 * hidpi, 224.0 * hidpi, 0.0, 1.0); + // Draw the menu on top of the game + rpass.set_viewport(0.0, 0.0, window_width, window_height, 0.0, 1.0); imgui .renderer .render( @@ -473,6 +491,21 @@ impl ApplicationHandler for App { } } +fn compute_game_bounds( + window_width: f32, + window_height: f32, + menu_height: f32, +) -> ((f32, f32), (f32, f32)) { + let available_width = window_width; + let available_height = window_height - menu_height; + + let width = available_width.min(available_height * 384.0 / 224.0); + let height = available_height.min(available_width * 224.0 / 384.0); + let x = (available_width - width) / 2.0; + let y = menu_height + (available_height - height) / 2.0; + ((x, y), (width, height)) +} + #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] #[repr(C)] struct Colors { From 5c5d56cb123e96731ebc4ecce9b0b0810ad2380a Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Tue, 5 Nov 2024 00:07:48 -0500 Subject: [PATCH 12/20] Support full controller input --- Cargo.lock | 1 + Cargo.toml | 1 + src/app.rs | 20 ++++++------- src/controller.rs | 64 ++++++++++++++++++++++++++++++++++++++++++ src/emulator.rs | 16 +++++------ src/main.rs | 1 + src/shrooms_vb_core.rs | 27 ++++++++++++++++-- 7 files changed, 109 insertions(+), 21 deletions(-) create mode 100644 src/controller.rs diff --git a/Cargo.lock b/Cargo.lock index 3944f49..70ce33d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1957,6 +1957,7 @@ name = "shrooms-vb-native" version = "0.1.0" dependencies = [ "anyhow", + "bitflags 2.6.0", "bytemuck", "cc", "clap", diff --git a/Cargo.toml b/Cargo.toml index e2c6317..5ce4bf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] anyhow = "1" +bitflags = "2" bytemuck = { version = "1", features = ["derive"] } clap = { version = "4", features = ["derive"] } cpal = "0.15" diff --git a/src/app.rs b/src/app.rs index a2be00c..2906783 100644 --- a/src/app.rs +++ b/src/app.rs @@ -9,13 +9,13 @@ use winit::platform::windows::{CornerPreference, WindowAttributesExtWindows as _ use winit::{ application::ApplicationHandler, dpi::{LogicalSize, PhysicalSize}, - event::{ElementState, Event, WindowEvent}, + event::{Event, WindowEvent}, event_loop::ActiveEventLoop, - keyboard::Key, window::Window, }; use crate::{ + controller::ControllerState, emulator::{EmulatorClient, EmulatorCommand}, renderer::GameRenderer, }; @@ -270,13 +270,17 @@ impl AppWindow { pub struct App { window: Option, client: EmulatorClient, + controller: ControllerState, } impl App { pub fn new(client: EmulatorClient) -> Self { + let controller = ControllerState::new(); + client.send_command(EmulatorCommand::SetKeys(controller.pressed())); Self { window: None, client, + controller, } } } @@ -305,15 +309,9 @@ impl ApplicationHandler for App { } WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::KeyboardInput { event, .. } => { - if let Key::Character("s") = event.logical_key.as_ref() { - match event.state { - ElementState::Pressed => { - self.client.send_command(EmulatorCommand::PressStart) - } - ElementState::Released => { - self.client.send_command(EmulatorCommand::ReleaseStart) - } - } + if self.controller.key_event(event) { + self.client + .send_command(EmulatorCommand::SetKeys(self.controller.pressed())); } } WindowEvent::RedrawRequested => { diff --git a/src/controller.rs b/src/controller.rs new file mode 100644 index 0000000..1e8995e --- /dev/null +++ b/src/controller.rs @@ -0,0 +1,64 @@ +use winit::{ + event::{ElementState, KeyEvent}, + keyboard::{Key, NamedKey}, +}; + +use crate::shrooms_vb_core::VBKey; + +pub struct ControllerState { + pressed: VBKey, +} + +impl ControllerState { + pub fn new() -> Self { + Self { + pressed: VBKey::SGN, + } + } + + pub fn pressed(&self) -> VBKey { + self.pressed + } + + pub fn key_event(&mut self, event: &KeyEvent) -> bool { + let Some(input) = self.key_to_input(&event.logical_key) else { + return false; + }; + match event.state { + ElementState::Pressed => { + if self.pressed.contains(input) { + return false; + } + self.pressed.insert(input); + true + } + ElementState::Released => { + if !self.pressed.contains(input) { + return false; + } + self.pressed.remove(input); + true + } + } + } + + fn key_to_input(&self, key: &Key) -> Option { + match key.as_ref() { + Key::Character("a") => Some(VBKey::SEL), + Key::Character("s") => Some(VBKey::STA), + Key::Character("d") => Some(VBKey::B), + Key::Character("f") => Some(VBKey::A), + Key::Character("e") => Some(VBKey::LT), + Key::Character("r") => Some(VBKey::RT), + Key::Character("i") => Some(VBKey::RU), + Key::Character("j") => Some(VBKey::RL), + Key::Character("k") => Some(VBKey::RD), + Key::Character("l") => Some(VBKey::RR), + Key::Named(NamedKey::ArrowUp) => Some(VBKey::LU), + Key::Named(NamedKey::ArrowLeft) => Some(VBKey::LL), + Key::Named(NamedKey::ArrowDown) => Some(VBKey::LD), + Key::Named(NamedKey::ArrowRight) => Some(VBKey::LR), + _ => None, + } + } +} diff --git a/src/emulator.rs b/src/emulator.rs index e87a9b4..f93d310 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -10,7 +10,11 @@ use std::{ use anyhow::Result; -use crate::{audio::Audio, renderer::GameRenderer, shrooms_vb_core::CoreVB}; +use crate::{ + audio::Audio, + renderer::GameRenderer, + shrooms_vb_core::{CoreVB, VBKey}, +}; pub struct EmulatorBuilder { rom: Option, @@ -132,11 +136,8 @@ impl Emulator { self.sim.reset(); self.running.store(true, Ordering::Release); } - EmulatorCommand::PressStart => { - self.sim.set_keys(0x1003); - } - EmulatorCommand::ReleaseStart => { - self.sim.set_keys(0x0003); + EmulatorCommand::SetKeys(keys) => { + self.sim.set_keys(keys); } } } @@ -149,8 +150,7 @@ pub enum EmulatorCommand { Pause, Resume, Reset, - PressStart, - ReleaseStart, + SetKeys(VBKey), } pub struct EmulatorClient { diff --git a/src/main.rs b/src/main.rs index 474bad5..7a588ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ use winit::event_loop::{ControlFlow, EventLoop}; mod app; mod audio; +mod controller; mod emulator; mod renderer; mod shrooms_vb_core; diff --git a/src/shrooms_vb_core.rs b/src/shrooms_vb_core.rs index 19ce04b..8fb1789 100644 --- a/src/shrooms_vb_core.rs +++ b/src/shrooms_vb_core.rs @@ -1,6 +1,7 @@ use std::{ffi::c_void, ptr, slice}; use anyhow::{anyhow, Result}; +use bitflags::bitflags; use num_derive::{FromPrimitive, ToPrimitive}; #[repr(C)] @@ -24,6 +25,28 @@ enum VBDataType { F32 = 5, } +bitflags! { + #[derive(Clone, Copy, Debug)] + pub struct VBKey: u16 { + const PWR = 0x0001; + const SGN = 0x0002; + const A = 0x0004; + const B = 0x0008; + const RT = 0x0010; + const LT = 0x0020; + const RU = 0x0040; + const RR = 0x0080; + const LR = 0x0100; + const LL = 0x0200; + const LD = 0x0400; + const LU = 0x0800; + const STA = 0x1000; + const SEL = 0x2000; + const RL = 0x4000; + const RD = 0x8000; + } +} + type OnFrame = extern "C" fn(sim: *mut VB) -> c_int; #[link(name = "vb")] @@ -198,8 +221,8 @@ impl CoreVB { }; } - pub fn set_keys(&mut self, keys: u16) { - unsafe { vb_set_keys(self.sim, keys) }; + pub fn set_keys(&mut self, keys: VBKey) { + unsafe { vb_set_keys(self.sim, keys.bits()) }; } } From 498e6fbdccc40c7e082a97b9a8a208ad6e165530 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Wed, 6 Nov 2024 23:26:31 -0500 Subject: [PATCH 13/20] Apply styles --- assets/selawk.ttf | Bin 0 -> 44224 bytes src/app.rs | 9 +++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 assets/selawk.ttf diff --git a/assets/selawk.ttf b/assets/selawk.ttf new file mode 100644 index 0000000000000000000000000000000000000000..736bac3c210abb56f305c0c2307da8e002cd6c91 GIT binary patch literal 44224 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 Date: Wed, 6 Nov 2024 23:36:33 -0500 Subject: [PATCH 14/20] Disable emulation options when game is not running --- src/app.rs | 7 ++++--- src/emulator.rs | 23 +++++++++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/app.rs b/src/app.rs index 5aadce6..96d0a1d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -357,14 +357,15 @@ impl ApplicationHandler for App { } }); ui.menu("Emulation", || { + let has_game = self.client.has_game(); if self.client.is_running() { - if ui.menu_item("Pause") { + if ui.menu_item_config("Pause").enabled(has_game).build() { self.client.send_command(EmulatorCommand::Pause); } - } else if ui.menu_item("Resume") { + } else if ui.menu_item_config("Resume").enabled(has_game).build() { self.client.send_command(EmulatorCommand::Resume); } - if ui.menu_item("Reset") { + if ui.menu_item_config("Reset").enabled(has_game).build() { self.client.send_command(EmulatorCommand::Reset); } }); diff --git a/src/emulator.rs b/src/emulator.rs index f93d310..8682209 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -20,6 +20,7 @@ pub struct EmulatorBuilder { rom: Option, commands: mpsc::Receiver, running: Arc, + has_game: Arc, } impl EmulatorBuilder { @@ -29,10 +30,12 @@ impl EmulatorBuilder { rom: None, commands, running: Arc::new(AtomicBool::new(false)), + has_game: Arc::new(AtomicBool::new(false)), }; let client = EmulatorClient { queue, running: builder.running.clone(), + has_game: builder.has_game.clone(), }; (builder, client) } @@ -45,7 +48,7 @@ impl EmulatorBuilder { } pub fn build(self) -> Result { - let mut emulator = Emulator::new(self.commands, self.running)?; + let mut emulator = Emulator::new(self.commands, self.running, self.has_game)?; if let Some(path) = self.rom { emulator.load_rom(&path)?; } @@ -59,18 +62,22 @@ pub struct Emulator { commands: mpsc::Receiver, renderer: Option, running: Arc, - has_game: bool, + has_game: Arc, } impl Emulator { - fn new(commands: mpsc::Receiver, running: Arc) -> Result { + fn new( + commands: mpsc::Receiver, + running: Arc, + has_game: Arc, + ) -> Result { Ok(Self { sim: CoreVB::new(), audio: Audio::init()?, commands, renderer: None, running, - has_game: false, + has_game, }) } @@ -78,7 +85,7 @@ impl Emulator { let bytes = fs::read(path)?; self.sim.reset(); self.sim.load_rom(bytes)?; - self.has_game = true; + self.has_game.store(true, Ordering::Release); self.running.store(true, Ordering::Release); Ok(()) } @@ -128,7 +135,7 @@ impl Emulator { self.running.store(false, Ordering::Release); } EmulatorCommand::Resume => { - if self.has_game { + if self.has_game.load(Ordering::Acquire) { self.running.store(true, Ordering::Relaxed); } } @@ -156,12 +163,16 @@ pub enum EmulatorCommand { pub struct EmulatorClient { queue: mpsc::Sender, running: Arc, + has_game: Arc, } impl EmulatorClient { pub fn is_running(&self) -> bool { self.running.load(Ordering::Acquire) } + pub fn has_game(&self) -> bool { + self.has_game.load(Ordering::Acquire) + } pub fn send_command(&self, command: EmulatorCommand) { if let Err(err) = self.queue.send(command) { eprintln!( From 62b34ea760e0289fdc2368b3a518031e53dc3de4 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Wed, 6 Nov 2024 23:55:25 -0500 Subject: [PATCH 15/20] Remove c impl, update readme --- Cargo.lock | 2 +- Cargo.toml | 5 ++- README.md | 12 +++--- assets.h | 12 ------ assets/lefteye.bin | Bin 86016 -> 0 bytes assets/righteye.bin | Bin 86016 -> 0 bytes audio.c | 65 ------------------------------- audio.h | 18 --------- cli.c | 11 ------ cli.h | 10 ----- controller.c | 65 ------------------------------- controller.h | 16 -------- game.c | 71 ---------------------------------- game.h | 9 ----- graphics.c | 92 -------------------------------------------- graphics.h | 22 ----------- main.c | 85 ---------------------------------------- makefile | 48 ----------------------- 18 files changed, 11 insertions(+), 532 deletions(-) delete mode 100644 assets.h delete mode 100644 assets/lefteye.bin delete mode 100644 assets/righteye.bin delete mode 100644 audio.c delete mode 100644 audio.h delete mode 100644 cli.c delete mode 100644 cli.h delete mode 100644 controller.c delete mode 100644 controller.h delete mode 100644 game.c delete mode 100644 game.h delete mode 100644 graphics.c delete mode 100644 graphics.h delete mode 100644 main.c delete mode 100644 makefile diff --git a/Cargo.lock b/Cargo.lock index 70ce33d..8db4c14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1953,7 +1953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "shrooms-vb-native" +name = "shrooms-vb" version = "0.1.0" dependencies = [ "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 5ce4bf1..855efdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "shrooms-vb-native" +name = "shrooms-vb" version = "0.1.0" edition = "2021" @@ -24,3 +24,6 @@ winit = "0.30" [build-dependencies] cc = "1" + +[profile.release] +lto = true \ No newline at end of file diff --git a/README.md b/README.md index 35758eb..1f18a68 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # Shrooms VB (native) -An SDL-based implementation of shrooms-vb. +A native implementation of shrooms-vb. Written in Rust, using winit, wgpu, and Dear ImGui. Should run on any major OS. ## Setup Install the following dependencies: - - `gcc` (or MinGW on Windows) (or whatever, just set `CC`) - - `pkg-config` - - sdl2 + - `cargo` Run ```sh -make build -``` \ No newline at end of file +cargo build --release +``` + +The executable will be in `target/release/shrooms-vb[.exe]` \ No newline at end of file diff --git a/assets.h b/assets.h deleted file mode 100644 index 3d8d26d..0000000 --- a/assets.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef SHROOMS_VB_NATIVE_ASSETS_ -#define SHROOMS_VB_NATIVE_ASSETS_ - -#include - -extern const uint8_t _binary_assets_lefteye_bin_start; -const uint8_t *LEFT_EYE_DEFAULT = &_binary_assets_lefteye_bin_start; - -extern const uint8_t _binary_assets_righteye_bin_start; -const uint8_t *RIGHT_EYE_DEFAULT = &_binary_assets_righteye_bin_start; - -#endif diff --git a/assets/lefteye.bin b/assets/lefteye.bin deleted file mode 100644 index f17761d6e8a924d2865f4c6530a1bd13b08e3cec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86016 zcmeI*3v%Ns5I|9HGMm&UJ^i6kBjFiOl`vSj${T_BbHLb1rCuJ-=6Y~_R^Yj9vH)`1 z^m;x&+dI9r{u}-u-V*qK82r#(^0#B?kj>*@0X_H2Kl}$<_nI&7y%y+ep;v~qaRSMo z&p+dsU$)%(v;SX04a!h|+xYt7pYvys^6O^po3HXM82+!FM&;l?O7^{WlfRuq@71Ez zRRZGw)Z@qB26+Fy%JtoLT7Q22_O9Dy<#Yk|>#}BP2mu5TKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**zDHntjQ#o^2}b%G1;+0F?KhG#=Pwdq|9{b9G1SWi*#DQ4 z(%=^fu>Zeku^8&*0_^|GNonwl1la#yv{(%Fasl@L<)k#Ypuqko2iAx!NHlavi~XU! z#fCGK-Wp$Q!ixW5(m&)3QL43+fAueI zdd*rjn#k72;pDtq#*Y);x1`15W_0pfQx@ti@GNRFwWU32FR#CFWq)2&GPn8)$t7L4 zHgZes&ZLeL{!t&jMC+#@3)9b!WJp;L<$m(tO1;SNzEV|HFU^!rybCW_?#JOYnRk3R zP1u-arXJcoMQcV=Kw+X@jzv5-&}mVtm+Lf~$hd2Gns7KKvlJDEP65S< zp^-fer^&tH!)e0p+0fB)yO#EU{jHPE@w0!3x*>IUN+|e z{_+ZNo$M;{ZIc{*X=HzGzrJ7+>UlUR^N!KZgw-hwEzEGi7-1E6XmC6-$OI#lP zWb*WqtEs)dt%YB7>lt+X?Ss3Y@8RZQdD5WGa6?Jc%N9mLN%Xr0_$$YGujxS&XM-Wh zP125UukPn}Q!UXDwuZHuop-h_qdvyUX!Yezdlq^X`-iHLrzO zzN2P$ILAVFdhSlx)pVK`%ZNK%N|k6_QKuM5a!twcy_r%l%D0dAf5Lz68o%!!C$?|n z*ueHAx42|B60LLc`1#(Js>ORBhxg1o#&)VayP9fp1+)#6*{=jH2_~x<)}EK}ryQ^T znkXfs!wB0rvk2tH2m$39$cX z$GJe%t_)B2W)Jhx32KyI5}&*!&{`!3^b z{WttSys-Fx82r#(^0#B?kj>*@0X_H2Kl}$<_nI&7y%y+ep;v~qaRSMo&p+dsU$)%( zv;SX04a!h|+xYt7pYvys^6O^po3HXM82+!FM&;l?O7^{WlfRuq@71EzRRZGw)Z@qB a26+Fy%JtoLT7Q22_O9Dy<#Yk|>+uH_XWy*= diff --git a/assets/righteye.bin b/assets/righteye.bin deleted file mode 100644 index d323a3c6a96249d86985a120af240b94a58f18e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86016 zcmeI*U6R`>5I|vX(wo#Kefx(-jYMvo2Ly$Wk_ZGyz(*T-r0&cP4j9;6@CcM~ip7)op!b+!>i&GR! z>c>legQc|ev)&T-r6Q|7TWC#lQ>uziOZqcORSSSW`iGn$%55#>|N56_`knRGctp0o z4ky=bnZHi>3`xty$>`*#rYzM{;91sW>XXi*v%UVnmHoO)sXXc{B)4?k`j9QLJCnLj z_^m#Ah}OFx3)A~871GvIIp6$`a$i(h4 zWot##Kw;v(oQr&ZK<9~idpT~yiHcp*+l0e0nWd~SbQ&m56z5#z^8+lO3mVzmaGKf; zFK!cV_lAy^+qJa+>TjKNwx9Du)P~gEDc$_@##4~nXzly!n``;0=4hb(Qy5{}5b-fR#t>5nIK4IG%$}aRT{_+ZNo$M-k z`Stex(#Zb${Q81PsQ2Nd${n+v39C~WTAJY^q$4d2#Z&H)Po;9iy(KS~zB74wsnyb6 z-}ZzbI`s@X{`SG$_xEt~v|MS@R=A;*>0t{asTBHI6a1Cq9&5Uh#NJ>?a+0#^+pGKe z*;Gq5gzdq4o9;il0L=~Qfq4X-;pFQ5^1TTQSHz~7>B;eQ{*m+4&-Qjvm}`Txac)wQg=;j}|eSdFX?y5HnRIbSRsJUA9hI4JR)pc*eX>TyI^Q2RE#z-@2 zsfh+><4&Xcm{zkloVLYgSKSGpf|j#WeE~$Jr*@X@r+jPe+~?gXHELc9v3ys}?r^S+ zcDn9P*wu8Jmdl7ITq>1p9C1%ElHyvD^XFqq!)V`L?tg{<+%^8*KXz;n;|O58l2aTq z8_Cu=dH#NGTh->hkHcr>j=7U6&#so5TmfwpW%eV1OM%I1g+0$(_*KqVe=U@j(P5-{ zFk9#Q6RxGE<3vFgSFr$5u1wKfQh$VI^e{uumS@*Oxk>-}5ox*HKj}}nmYPzfGN!mP z?R!ZnE~3)-Z0{=;($PXHhGmOxW$iJyQJV2;e`*J$l7IB<7^}&vB1l@eheP zDgJZZqu1Z__fP1{oMRzK5tu3PevSPzb8hsFz~2PMM!)}{(BH;DJp!`?i2qsg^t*xp z@xQ_0pfp!RbULW1c?7x^7Olc0P(-VDlmpw0>u9;dHP*JfcRfw z6&S-T0pfp_JpHa9K>V+;3XEZv0P#Odo_<#lApTca1;#K-fcT#!ProY&5dSNz0%MpZ zK>W{=r{5I>i2oH*b$HJ34D%27C=si9-mLI4Q1m5TK@z8an#)>bYud$@0Wl0Pl$=~-fM}z7A6egr@Ku4eEyjb39Ucz|MU{5zXiU2_-Frl zn*U{Ae3dW3@PC{i|L4h@9#j0=KGallF==-!44kFLeRV LV|;(~& -#include - -void audioCallback(void *userdata, uint8_t *stream, int len) { - AudioContext *aud; - SDL_assert(len == 834 * 4); - - aud = userdata; - if (!aud->filled) { - /* too little data, play silence */ - SDL_memset4(stream, 0, 834); - return; - } - SDL_memcpy4(stream, aud->buffers[aud->current], 834); - ++aud->current; - aud->current %= 2; - aud->filled -= 1; -} - -int audioInit(AudioContext *aud) { - SDL_AudioSpec spec; - spec.freq = 41700; - spec.format = AUDIO_S16; - spec.channels = 2; - spec.samples = 834; - spec.callback = &audioCallback; - spec.userdata = aud; - - aud->id = SDL_OpenAudioDevice(NULL, 0, &spec, NULL, 0); - aud->paused = true; - if (!aud->id) { - fprintf(stderr, "could not open audio device: %s\n", SDL_GetError()); - return -1; - } - aud->current = 0; - aud->filled = 0; - return 0; -} - -int audioUpdate(AudioContext *aud, void *data, uint32_t bytes) { - int filled; - if (!aud->id) return -1; - SDL_assert(bytes == 834 * 4); - - SDL_LockAudioDevice(aud->id); - if (aud->filled < 2) { - int next = (aud->current + aud->filled) % 2; - SDL_memcpy4(aud->buffers[next], data, bytes / 4); - aud->filled += 1; - } - filled = aud->filled; - SDL_UnlockAudioDevice(aud->id); - - if (aud->paused) { - SDL_PauseAudioDevice(aud->id, false); - aud->paused = false; - } - - while (filled > 1) { - SDL_Delay(0); - filled = aud->filled; - } - - return 0; -} \ No newline at end of file diff --git a/audio.h b/audio.h deleted file mode 100644 index 644724f..0000000 --- a/audio.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef SHROOMS_VB_NATIVE_AUDIO_ -#define SHROOMS_VB_NATIVE_AUDIO_ - -#include -#include - -typedef struct { - SDL_AudioDeviceID id; - bool paused; - uint32_t buffers[2][834]; - int current; - int filled; -} AudioContext; - -int audioInit(AudioContext *aud); -int audioUpdate(AudioContext *aud, void *data, uint32_t bytes); - -#endif \ No newline at end of file diff --git a/cli.c b/cli.c deleted file mode 100644 index 7328a09..0000000 --- a/cli.c +++ /dev/null @@ -1,11 +0,0 @@ -#include -#include - -int parseCLIArgs(int argc, char **argv, CLIArgs *args) { - if (argc != 2) { - fprintf(stderr, "usage: %s /path/to/rom.vb\n", argv[0]); - return 1; - } - args->filename = argv[1]; - return 0; -} \ No newline at end of file diff --git a/cli.h b/cli.h deleted file mode 100644 index 580265d..0000000 --- a/cli.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef SHROOMS_VB_NATIVE_CLI_ -#define SHROOMS_VB_NATIVE_CLI_ - -typedef struct { - char *filename; -} CLIArgs; - -int parseCLIArgs(int argc, char **argv, CLIArgs *args); - -#endif \ No newline at end of file diff --git a/controller.c b/controller.c deleted file mode 100644 index 54979a4..0000000 --- a/controller.c +++ /dev/null @@ -1,65 +0,0 @@ -#include - -#define VB_PWR 0x0001 -#define VB_SGN 0x0002 -#define VB_A 0x0004 -#define VB_B 0x0008 -#define VB_RT 0x0010 -#define VB_LT 0x0020 -#define VB_RU 0x0040 -#define VB_RR 0x0080 -#define VB_LR 0x0100 -#define VB_LL 0x0200 -#define VB_LD 0x0400 -#define VB_LU 0x0800 -#define VB_STA 0x1000 -#define VB_SEL 0x2000 -#define VB_RL 0x4000 -#define VB_RD 0x8000 - -static uint16_t symToMask(SDL_KeyCode sym) { - switch (sym) { - default: return 0; - case SDLK_a: - return VB_SEL; - case SDLK_s: - return VB_STA; - case SDLK_d: - return VB_B; - case SDLK_f: - return VB_A; - case SDLK_e: - return VB_LT; - case SDLK_r: - return VB_RT; - case SDLK_i: - return VB_RU; - case SDLK_j: - return VB_RL; - case SDLK_k: - return VB_RD; - case SDLK_l: - return VB_RR; - case SDLK_UP: - return VB_LU; - case SDLK_LEFT: - return VB_LL; - case SDLK_DOWN: - return VB_LD; - case SDLK_RIGHT: - return VB_LR; - } -} - -void ctrlInit(ControllerState *ctrl) { - ctrl->keys = VB_SGN; -} -void ctrlKeyDown(ControllerState *ctrl, SDL_Keycode sym) { - ctrl->keys |= symToMask(sym); -} -void ctrlKeyUp(ControllerState *ctrl, SDL_Keycode sym) { - ctrl->keys &= ~symToMask(sym); -} -uint16_t ctrlKeys(ControllerState *ctrl) { - return ctrl->keys; -} \ No newline at end of file diff --git a/controller.h b/controller.h deleted file mode 100644 index fe9dabe..0000000 --- a/controller.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef SHROOMS_VB_NATIVE_CONTROLLER_ -#define SHROOMS_VB_NATIVE_CONTROLLER_ - -#include -#include - -typedef struct { - uint16_t keys; -} ControllerState; - -void ctrlInit(ControllerState *ctrl); -void ctrlKeyDown(ControllerState *ctrl, SDL_Keycode sym); -void ctrlKeyUp(ControllerState *ctrl, SDL_Keycode sym); -uint16_t ctrlKeys(ControllerState *ctrl); - -#endif \ No newline at end of file diff --git a/game.c b/game.c deleted file mode 100644 index 3461611..0000000 --- a/game.c +++ /dev/null @@ -1,71 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -typedef struct { - GraphicsContext *gfx; - AudioContext aud; - int16_t audioBuffer[834 * 2]; -} GameState; - -int onFrame(VB *sim) { - static uint8_t leftEye[384*224]; - static uint8_t rightEye[384*224]; - GameState *state; - void *samples; - uint32_t samplePairs; - - state = vbGetUserData(sim); - - vbGetPixels(sim, leftEye, 1, 384, rightEye, 1, 384); - gfxUpdateLeftEye(state->gfx, leftEye); - gfxUpdateRightEye(state->gfx, rightEye); - - samples = vbGetSamples(sim, NULL, NULL, &samplePairs); - audioUpdate(&state->aud, samples, samplePairs * 4); - vbSetSamples(sim, samples, VB_S16, 834); - gfxRender(state->gfx); - return 1; -} - -#define MAX_STEP_CLOCKS 20000000 - -int runGame(VB *sim, GraphicsContext *gfx) { - uint32_t clocks; - SDL_Event event; - GameState state; - ControllerState ctrl; - - state.gfx = gfx; - audioInit(&state.aud); - vbSetSamples(sim, &state.audioBuffer, VB_S16, 834); - vbSetUserData(sim, &state); - vbSetFrameCallback(sim, &onFrame); - - ctrlInit(&ctrl); - - gfxUpdateLeftEye(gfx, LEFT_EYE_DEFAULT); - gfxUpdateRightEye(gfx, RIGHT_EYE_DEFAULT); - - while (1) { - clocks = MAX_STEP_CLOCKS; - vbEmulate(sim, &clocks); - - while (SDL_PollEvent(&event)) { - if (event.type == SDL_QUIT) { - return 0; - } - if (event.type == SDL_KEYDOWN) { - ctrlKeyDown(&ctrl, event.key.keysym.sym); - } - if (event.type == SDL_KEYUP) { - ctrlKeyUp(&ctrl, event.key.keysym.sym); - } - } - vbSetKeys(sim, ctrlKeys(&ctrl)); - } -} diff --git a/game.h b/game.h deleted file mode 100644 index 9984eea..0000000 --- a/game.h +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef SHROOMS_VB_NATIVE_GAME_ -#define SHROOMS_VB_NATIVE_GAME_ - -#include "graphics.h" -#include "shrooms-vb-core/core/vb.h" - -int runGame(VB *sim, GraphicsContext *gfx); - -#endif \ No newline at end of file diff --git a/graphics.c b/graphics.c deleted file mode 100644 index 9aa589c..0000000 --- a/graphics.c +++ /dev/null @@ -1,92 +0,0 @@ -#include - -static void copyScreenTexture(uint8_t *dst, const uint8_t *src, int pitch) { - int x, y, i; - uint8_t color; - int delta = pitch / 384; - for (y = 0; y < 224; ++y) { - for (x = 0; x < 384; x += 1) { - color = src[(y * 384) + x]; - for (i = 0; i < delta; ++i) { - dst[(y * pitch) + (x * delta) + i] = color; - } - } - } -} - -int gfxInit(GraphicsContext *gfx) { - gfx->window = SDL_CreateWindow("Shrooms VB", - SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, - 1536, 896, SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI); - if (!gfx->window) { - fprintf(stderr, "Error creating window: %s\n", SDL_GetError()); - return 1; - } - - gfx->renderer = SDL_CreateRenderer(gfx->window, -1, 0); - if (!gfx->renderer) { - fprintf(stderr, "Error creating renderer: %s\n", SDL_GetError()); - goto cleanup_window; - } - - gfx->winSurface = SDL_GetWindowSurface(gfx->window); - if (!gfx->winSurface) { - fprintf(stderr, "Error getting surface: %s\n", SDL_GetError()); - goto cleanup_window; - } - - gfx->leftEye = SDL_CreateTexture(gfx->renderer, SDL_PIXELFORMAT_RGB24, SDL_TEXTUREACCESS_STREAMING, 384, 224); - if (!gfx->leftEye) { - fprintf(stderr, "Error creating left eye texture: %s\n", SDL_GetError()); - goto cleanup_window; - } - SDL_SetTextureColorMod(gfx->leftEye, 0xff, 0, 0); - - gfx->rightEye = SDL_CreateTexture(gfx->renderer, SDL_PIXELFORMAT_RGB24, SDL_TEXTUREACCESS_STREAMING, 384, 224); - if (!gfx->rightEye) { - fprintf(stderr, "Error creating left eye texture: %s\n", SDL_GetError()); - goto cleanup_left_eye; - } - SDL_SetTextureColorMod(gfx->rightEye, 0, 0xc6, 0xf0); - SDL_SetTextureBlendMode(gfx->rightEye, SDL_BLENDMODE_ADD); - - return 0; - -cleanup_left_eye: - SDL_DestroyTexture(gfx->leftEye); -cleanup_window: - SDL_DestroyWindow(gfx->window); - return 1; -} - -void gfxDestroy(GraphicsContext *gfx) { - SDL_DestroyTexture(gfx->rightEye); - SDL_DestroyTexture(gfx->leftEye); - SDL_DestroyWindow(gfx->window); -} - -static void gfxUpdateEye(SDL_Texture *eye, const uint8_t *bytes) { - void *target; - int pitch; - if (SDL_LockTexture(eye, NULL, &target, &pitch)) { - fprintf(stderr, "Error locking buffer for eye: %s\n", SDL_GetError()); - return; - } - copyScreenTexture(target, bytes, pitch); - SDL_UnlockTexture(eye); -} - -void gfxUpdateLeftEye(GraphicsContext *gfx, const uint8_t *bytes) { - gfxUpdateEye(gfx->leftEye, bytes); -} -void gfxUpdateRightEye(GraphicsContext *gfx, const uint8_t *bytes) { - gfxUpdateEye(gfx->rightEye, bytes); -} - -void gfxRender(GraphicsContext *gfx) { - SDL_RenderClear(gfx->renderer); - SDL_RenderCopy(gfx->renderer, gfx->leftEye, NULL, NULL); - SDL_RenderCopy(gfx->renderer, gfx->rightEye, NULL, NULL); - SDL_RenderPresent(gfx->renderer); -} - diff --git a/graphics.h b/graphics.h deleted file mode 100644 index aa4485b..0000000 --- a/graphics.h +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef SHROOMS_VB_NATIVE_GRAPHICS_ -#define SHROOMS_VB_NATIVE_GRAPHICS_ - -#include - -typedef struct { - SDL_Window *window; - SDL_Surface *winSurface; - SDL_Renderer *renderer; - SDL_Texture *leftEye; - SDL_Texture *rightEye; -} GraphicsContext; - -int gfxInit(GraphicsContext *gfx); -void gfxDestroy(GraphicsContext *gfx); - -void gfxUpdateLeftEye(GraphicsContext *gfx, const uint8_t *bytes); -void gfxUpdateRightEye(GraphicsContext *gfx, const uint8_t *bytes); - -void gfxRender(GraphicsContext *gfx); - -#endif diff --git a/main.c b/main.c deleted file mode 100644 index eb7eedf..0000000 --- a/main.c +++ /dev/null @@ -1,85 +0,0 @@ -#include -#include -#include -#include -#include "shrooms-vb-core/core/vb.h" -#include - -uint8_t *readROM(char *filename, uint32_t *size) { - FILE *file = fopen(filename, "rb"); - uint8_t *rom; - long filesize; - - if (!file) { - perror("could not open file"); - return NULL; - } - - if (fseek(file, 0, SEEK_END)) { - perror("could not seek file end"); - return NULL; - } - filesize = ftell(file); - if (filesize == -1) { - perror("could not read file size"); - return NULL; - } - if (fseek(file, 0, SEEK_SET)) { - perror("could not seek file start"); - return NULL; - } - - *size = (uint32_t) filesize; - rom = malloc(*size); - if (!rom) { - perror("could not allocate ROM"); - return NULL; - } - fread(rom, 1, *size, file); - if (ferror(file)) { - perror("could not read file"); - return NULL; - } - if (fclose(file)) { - perror("could not close file"); - return NULL; - } - - return rom; -} - -int main(int argc, char **argv) { - VB *sim; - uint8_t *rom; - uint32_t romSize; - GraphicsContext gfx; - CLIArgs args; - int status; - - if (parseCLIArgs(argc, argv, &args)) { - return 1; - } - - rom = readROM(args.filename, &romSize); - if (!rom) { - return 1; - } - - sim = malloc(vbSizeOf()); - vbInit(sim); - vbSetCartROM(sim, rom, romSize); - - if (SDL_Init(SDL_INIT_EVERYTHING)) { - fprintf(stderr, "Error initializing SDL: %s\n", SDL_GetError()); - return 1; - } - - if (gfxInit(&gfx)) { - SDL_Quit(); - return 1; - } - - status = runGame(sim, &gfx); - SDL_Quit(); - return status; -} diff --git a/makefile b/makefile deleted file mode 100644 index 71115dc..0000000 --- a/makefile +++ /dev/null @@ -1,48 +0,0 @@ -CC?=gcc -LD?=ld -SHROOMSFLAGS=shrooms-vb-core/core/vb.c -I shrooms-vb-core/core -msys_version := $(if $(findstring Msys, $(shell uname -o)),$(word 1, $(subst ., ,$(shell uname -r))),0) - -ifeq ($(msys_version), 0) -SDL2FLAGS=$(shell pkg-config sdl2 --cflags --libs) -BINLINKFLAGS=-z noexecstack -else -SDL2FLAGS=$(shell pkg-config sdl2 --cflags --libs) -mwindows -mconsole -BINLINKFLAGS= -endif - -.PHONY: clean build -clean: - @rm -rf shrooms-vb output - -CFILES := $(foreach dir,./,$(notdir $(wildcard $(dir)/*.c))) -BINFILES := $(foreach dir,assets/,$(notdir $(wildcard $(dir)/*.bin))) - -COBJS := $(CFILES:%.c=output/%.o) -SHROOMSOBJS := output/vb.o -BINOBJS := $(BINFILES:%.bin=output/%.o) - -OFILES := $(COBJS) $(SHROOMSOBJS) $(BINOBJS) - -output/%.o: %.c - @mkdir -p output - @$(CC) -c -o $@ $< -I . \ - -I shrooms-vb-core/core $(SDL2FLAGS) \ - -O3 -flto -fno-strict-aliasing \ - -Werror -std=c90 -Wall -Wextra -Wpedantic - -output/vb.o: shrooms-vb-core/core/vb.c - @mkdir -p output - @$(CC) -c -o $@ $< -I . \ - -I shrooms-vb-core/core $(SDL2FLAGS) \ - -O3 -flto -fno-strict-aliasing \ - -Werror -std=c90 -Wall -Wextra -Wpedantic - -output/%.o: assets/%.bin - @mkdir -p output - @$(LD) -r -b binary $(BINLINKFLAGS) -o $@ $< - -shrooms-vb: $(OFILES) - @$(CC) -o $@ $(OFILES) $(SDL2FLAGS) -flto - -build: shrooms-vb \ No newline at end of file From a69247dd33e01f80918e6faef7f7d588d73c262b Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sat, 9 Nov 2024 18:14:18 -0500 Subject: [PATCH 16/20] Support multiple windows, start on input UI --- Cargo.toml | 2 +- src/app.rs | 545 ++++++---------------------------------------- src/app/common.rs | 248 +++++++++++++++++++++ src/app/game.rs | 357 ++++++++++++++++++++++++++++++ src/app/input.rs | 142 ++++++++++++ src/emulator.rs | 1 + src/main.rs | 5 +- 7 files changed, 824 insertions(+), 476 deletions(-) create mode 100644 src/app/common.rs create mode 100644 src/app/game.rs create mode 100644 src/app/input.rs diff --git a/Cargo.toml b/Cargo.toml index 855efdb..23abe96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ bitflags = "2" bytemuck = { version = "1", features = ["derive"] } clap = { version = "4", features = ["derive"] } cpal = "0.15" -imgui = "0.12" +imgui = { version = "0.12", features = ["tables-api"] } imgui-wgpu = { git = "https://github.com/Yatekii/imgui-wgpu-rs", rev = "2edd348" } imgui-winit-support = "0.13" itertools = "0.13" diff --git a/src/app.rs b/src/app.rs index 96d0a1d..421fbd8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,518 +1,117 @@ -use imgui::*; -use imgui_wgpu::{Renderer, RendererConfig}; -use imgui_winit_support::WinitPlatform; -use pollster::block_on; -use std::{sync::Arc, time::Instant}; -use wgpu::util::DeviceExt as _; -#[cfg(target_os = "windows")] -use winit::platform::windows::{CornerPreference, WindowAttributesExtWindows as _}; +use std::{collections::HashMap, fmt::Debug}; + +use game::GameWindow; use winit::{ application::ApplicationHandler, - dpi::{LogicalSize, PhysicalSize}, event::{Event, WindowEvent}, - event_loop::ActiveEventLoop, - window::Window, + event_loop::{ActiveEventLoop, EventLoopProxy}, + window::WindowId, }; -use crate::{ - controller::ControllerState, - emulator::{EmulatorClient, EmulatorCommand}, - renderer::GameRenderer, -}; +use crate::emulator::EmulatorClient; -struct ImguiState { - context: imgui::Context, - platform: WinitPlatform, - renderer: Renderer, - clear_color: wgpu::Color, - last_frame: Instant, - last_cursor: Option, -} - -struct AppWindow { - device: wgpu::Device, - queue: Arc, - window: Arc, - surface_desc: wgpu::SurfaceConfiguration, - surface: wgpu::Surface<'static>, - hidpi_factor: f64, - pipeline: wgpu::RenderPipeline, - bind_group: wgpu::BindGroup, - imgui: Option, -} - -impl AppWindow { - fn setup_gpu(event_loop: &ActiveEventLoop, client: &EmulatorClient) -> Self { - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::PRIMARY, - ..Default::default() - }); - - let window = { - let size = LogicalSize::new(384, 244); - - let attributes = Window::default_attributes() - .with_inner_size(size) - .with_title("Shrooms VB"); - #[cfg(target_os = "windows")] - let attributes = attributes.with_corner_preference(CornerPreference::DoNotRound); - Arc::new(event_loop.create_window(attributes).unwrap()) - }; - - let size = window.inner_size(); - let hidpi_factor = window.scale_factor(); - let surface = instance.create_surface(window.clone()).unwrap(); - - let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::HighPerformance, - compatible_surface: Some(&surface), - force_fallback_adapter: false, - })) - .unwrap(); - - let (device, queue) = - block_on(adapter.request_device(&wgpu::DeviceDescriptor::default(), None)).unwrap(); - let queue = Arc::new(queue); - let eyes = Arc::new(GameRenderer::create_texture(&device, "eye")); - client.send_command(EmulatorCommand::SetRenderer(GameRenderer { - queue: queue.clone(), - eyes: eyes.clone(), - })); - let eyes = eyes.create_view(&wgpu::TextureViewDescriptor::default()); - let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); - let colors = Colors { - left: [1.0, 0.0, 0.0, 1.0], - right: [0.0, 0.7734375, 0.9375, 1.0], - }; - let color_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("colors"), - contents: bytemuck::bytes_of(&colors), - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - }); - let texture_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("texture bind group layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }, - ], - }); - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("bind group"), - layout: &texture_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&eyes), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&sampler), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: color_buf.as_entire_binding(), - }, - ], - }); - - // Set up swap chain - let surface_desc = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: wgpu::TextureFormat::Bgra8UnormSrgb, - width: size.width, - height: size.height, - present_mode: wgpu::PresentMode::Fifo, - desired_maximum_frame_latency: 2, - alpha_mode: wgpu::CompositeAlphaMode::Auto, - view_formats: vec![wgpu::TextureFormat::Bgra8Unorm], - }; - - surface.configure(&device, &surface_desc); - - let shader = device.create_shader_module(wgpu::include_wgsl!("anaglyph.wgsl")); - let render_pipeline_layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("render pipeline layout"), - bind_group_layouts: &[&texture_bind_group_layout], - push_constant_ranges: &[], - }); - let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("render pipeline"), - layout: Some(&render_pipeline_layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: "vs_main", - buffers: &[], - compilation_options: wgpu::PipelineCompilationOptions::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: "fs_main", - targets: &[Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Bgra8UnormSrgb, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: wgpu::PipelineCompilationOptions::default(), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - strip_index_format: None, - front_face: wgpu::FrontFace::Ccw, - cull_mode: Some(wgpu::Face::Back), - polygon_mode: wgpu::PolygonMode::Fill, - unclipped_depth: false, - conservative: false, - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - cache: None, - }); - - let imgui = None; - Self { - device, - queue, - window, - surface_desc, - surface, - hidpi_factor, - pipeline: render_pipeline, - bind_group, - imgui, - } - } - - fn setup_imgui(&mut self) { - let mut context = imgui::Context::create(); - let mut platform = imgui_winit_support::WinitPlatform::new(&mut context); - platform.attach_window( - context.io_mut(), - &self.window, - imgui_winit_support::HiDpiMode::Default, - ); - context.set_ini_filename(None); - - let font_size = (16.0 * self.hidpi_factor) as f32; - context.io_mut().font_global_scale = (1.0 / self.hidpi_factor) as f32; - - context.fonts().add_font(&[FontSource::TtfData { - data: include_bytes!("../assets/selawk.ttf"), - size_pixels: font_size, - config: Some(imgui::FontConfig { - oversample_h: 1, - pixel_snap_h: true, - size_pixels: font_size, - ..Default::default() - }), - }]); - - let style = context.style_mut(); - style.use_light_colors(); - - // - // Set up dear imgui wgpu renderer - // - let clear_color = wgpu::Color::BLACK; - - let renderer_config = RendererConfig { - texture_format: self.surface_desc.format, - ..Default::default() - }; - - let renderer = Renderer::new(&mut context, &self.device, &self.queue, renderer_config); - - let last_frame = Instant::now(); - let last_cursor = None; - - self.imgui = Some(ImguiState { - context, - platform, - renderer, - clear_color, - last_frame, - last_cursor, - }) - } - - fn new(event_loop: &ActiveEventLoop, client: &EmulatorClient) -> Self { - let mut window = Self::setup_gpu(event_loop, client); - window.setup_imgui(); - window - } -} +mod common; +mod game; +mod input; pub struct App { - window: Option, + windows: HashMap>, + focused_window: Option, client: EmulatorClient, - controller: ControllerState, + proxy: EventLoopProxy, } impl App { - pub fn new(client: EmulatorClient) -> Self { - let controller = ControllerState::new(); - client.send_command(EmulatorCommand::SetKeys(controller.pressed())); + pub fn new(client: EmulatorClient, proxy: EventLoopProxy) -> Self { Self { - window: None, + windows: HashMap::new(), + focused_window: None, client, - controller, + proxy, } } + + fn active_window(&mut self) -> Option<&mut Box> { + let active_window = self.focused_window?; + self.windows.get_mut(&active_window) + } } -impl ApplicationHandler for App { +impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { - self.window = Some(AppWindow::new(event_loop, &self.client)); + let mut window = GameWindow::new(event_loop, self.client.clone(), self.proxy.clone()); + window.init(); + self.focused_window = Some(window.id()); + self.windows.insert(window.id(), Box::new(window)); } fn window_event( &mut self, event_loop: &ActiveEventLoop, - window_id: winit::window::WindowId, + window_id: WindowId, event: WindowEvent, ) { - let window = self.window.as_mut().unwrap(); - let imgui = window.imgui.as_mut().unwrap(); - - match &event { - WindowEvent::Resized(size) => { - window.surface_desc.width = size.width; - window.surface_desc.height = size.height; - window - .surface - .configure(&window.device, &window.surface_desc); + if let WindowEvent::Focused(focused) = event { + if focused { + self.focused_window = Some(window_id); + } else { + self.focused_window = None; } - WindowEvent::CloseRequested => event_loop.exit(), - WindowEvent::KeyboardInput { event, .. } => { - if self.controller.key_event(event) { - self.client - .send_command(EmulatorCommand::SetKeys(self.controller.pressed())); - } - } - WindowEvent::RedrawRequested => { - let now = Instant::now(); - imgui - .context - .io_mut() - .update_delta_time(now - imgui.last_frame); - imgui.last_frame = now; - - let frame = match window.surface.get_current_texture() { - Ok(frame) => frame, - Err(e) => { - eprintln!("dropped frame: {e:?}"); - return; - } - }; - imgui - .platform - .prepare_frame(imgui.context.io_mut(), &window.window) - .expect("Failed to prepare frame"); - let ui = imgui.context.frame(); - let mut menu_height = 0.0; - ui.main_menu_bar(|| { - menu_height = ui.window_size()[1]; - ui.menu("ROM", || { - if ui.menu_item("Open ROM") { - let rom = native_dialog::FileDialog::new() - .add_filter("Virtual Boy ROMs", &["vb", "vbrom"]) - .show_open_single_file() - .unwrap(); - if let Some(path) = rom { - self.client.send_command(EmulatorCommand::LoadGame(path)); - } - } - if ui.menu_item("Quit") { - event_loop.exit(); - } - }); - ui.menu("Emulation", || { - let has_game = self.client.has_game(); - if self.client.is_running() { - if ui.menu_item_config("Pause").enabled(has_game).build() { - self.client.send_command(EmulatorCommand::Pause); - } - } else if ui.menu_item_config("Resume").enabled(has_game).build() { - self.client.send_command(EmulatorCommand::Resume); - } - if ui.menu_item_config("Reset").enabled(has_game).build() { - self.client.send_command(EmulatorCommand::Reset); - } - }); - ui.menu("Video", || { - let current_dims = PhysicalSize::new( - window.surface_desc.width, - window.surface_desc.height, - ) - .to_logical(window.hidpi_factor); - for scale in 1..=4 { - let label = format!("x{scale}"); - let dims = LogicalSize::new(384 * scale, 224 * scale + 20); - let selected = dims == current_dims; - if ui.menu_item_config(label).selected(selected).build() { - if let Some(size) = window.window.request_inner_size(dims) { - window.surface_desc.width = size.width; - window.surface_desc.height = size.height; - window - .surface - .configure(&window.device, &window.surface_desc); - } - } - } - }); - }); - - let mut encoder: wgpu::CommandEncoder = window - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - - if imgui.last_cursor != ui.mouse_cursor() { - imgui.last_cursor = ui.mouse_cursor(); - imgui.platform.prepare_render(ui, &window.window); - } - - let view = frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: None, - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(imgui.clear_color), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - // Draw the game - rpass.set_pipeline(&window.pipeline); - let window_width = window.surface_desc.width as f32; - let window_height = window.surface_desc.height as f32; - let menu_height = menu_height * window.hidpi_factor as f32; - let ((x, y), (width, height)) = - compute_game_bounds(window_width, window_height, menu_height); - rpass.set_viewport(x, y, width, height, 0.0, 1.0); - rpass.set_bind_group(0, &window.bind_group, &[]); - rpass.draw(0..6, 0..1); - - // Draw the menu on top of the game - rpass.set_viewport(0.0, 0.0, window_width, window_height, 0.0, 1.0); - imgui - .renderer - .render( - imgui.context.render(), - &window.queue, - &window.device, - &mut rpass, - ) - .expect("Rendering failed"); - - drop(rpass); - - window.queue.submit(Some(encoder.finish())); - - frame.present(); - } - _ => (), } - - imgui.platform.handle_event::<()>( - imgui.context.io_mut(), - &window.window, - &Event::WindowEvent { window_id, event }, - ); + let Some(window) = self.windows.get_mut(&window_id) else { + return; + }; + window.handle_event(event_loop, &Event::WindowEvent { window_id, event }); } - fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: ()) { - let window = self.window.as_mut().unwrap(); - let imgui = window.imgui.as_mut().unwrap(); - imgui.platform.handle_event::<()>( - imgui.context.io_mut(), - &window.window, - &Event::UserEvent(event), - ); + fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) { + match event { + UserEvent::OpenWindow(mut window) => { + window.init(); + self.windows.insert(window.id(), window); + } + UserEvent::CloseWindow(window_id) => { + self.windows.remove(&window_id); + } + } } fn device_event( &mut self, - _event_loop: &ActiveEventLoop, + event_loop: &ActiveEventLoop, device_id: winit::event::DeviceId, event: winit::event::DeviceEvent, ) { - let window = self.window.as_mut().unwrap(); - let imgui = window.imgui.as_mut().unwrap(); - imgui.platform.handle_event::<()>( - imgui.context.io_mut(), - &window.window, - &Event::DeviceEvent { device_id, event }, - ); + let Some(window) = self.active_window() else { + return; + }; + window.handle_event(event_loop, &Event::DeviceEvent { device_id, event }); } - fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { - let window = self.window.as_mut().unwrap(); - let imgui = window.imgui.as_mut().unwrap(); - window.window.request_redraw(); - imgui.platform.handle_event::<()>( - imgui.context.io_mut(), - &window.window, - &Event::AboutToWait, - ); + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + let Some(window) = self.active_window() else { + return; + }; + window.handle_event(event_loop, &Event::AboutToWait); } } -fn compute_game_bounds( - window_width: f32, - window_height: f32, - menu_height: f32, -) -> ((f32, f32), (f32, f32)) { - let available_width = window_width; - let available_height = window_height - menu_height; - - let width = available_width.min(available_height * 384.0 / 224.0); - let height = available_height.min(available_width * 224.0 / 384.0); - let x = (available_width - width) / 2.0; - let y = menu_height + (available_height - height) / 2.0; - ((x, y), (width, height)) +pub trait AppWindow { + fn id(&self) -> WindowId; + fn init(&mut self); + fn handle_event(&mut self, event_loop: &ActiveEventLoop, event: &Event); } -#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] -#[repr(C)] -struct Colors { - left: [f32; 4], - right: [f32; 4], +pub enum UserEvent { + OpenWindow(Box), + CloseWindow(WindowId), +} + +impl Debug for UserEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::OpenWindow(window) => f.debug_tuple("OpenWindow").field(&window.id()).finish(), + Self::CloseWindow(window_id) => f.debug_tuple("CloseWindow").field(window_id).finish(), + } + } } diff --git a/src/app/common.rs b/src/app/common.rs new file mode 100644 index 0000000..9aaf8b0 --- /dev/null +++ b/src/app/common.rs @@ -0,0 +1,248 @@ +use std::{ + ops::{Deref, DerefMut}, + sync::Arc, + time::Instant, +}; + +use imgui::{FontSource, MouseCursor, SuspendedContext, WindowToken}; +use imgui_wgpu::{Renderer, RendererConfig}; +use imgui_winit_support::WinitPlatform; +use pollster::block_on; +#[cfg(target_os = "windows")] +use winit::platform::windows::{CornerPreference, WindowAttributesExtWindows as _}; +use winit::{ + dpi::{LogicalSize, PhysicalSize, Size}, + event_loop::ActiveEventLoop, + window::{Window, WindowAttributes}, +}; + +pub struct WindowStateBuilder<'a> { + event_loop: &'a ActiveEventLoop, + attributes: WindowAttributes, +} +impl<'a> WindowStateBuilder<'a> { + pub fn new(event_loop: &'a ActiveEventLoop) -> Self { + let attributes = Window::default_attributes(); + #[cfg(target_os = "windows")] + let attributes = attributes.with_corner_preference(CornerPreference::DoNotRound); + Self { + event_loop, + attributes, + } + } + + pub fn with_title>(self, title: T) -> Self { + Self { + attributes: self.attributes.with_title(title), + ..self + } + } + + pub fn with_inner_size>(self, size: S) -> Self { + Self { + attributes: self.attributes.with_inner_size(size), + ..self + } + } + + pub fn build(self) -> WindowState { + WindowState::new(self.event_loop, self.attributes) + } +} + +#[derive(Debug)] +pub struct WindowState { + pub device: wgpu::Device, + pub queue: Arc, + pub window: Arc, + pub surface_desc: wgpu::SurfaceConfiguration, + pub surface: wgpu::Surface<'static>, + pub hidpi_factor: f64, +} + +impl WindowState { + fn new(event_loop: &ActiveEventLoop, attributes: WindowAttributes) -> Self { + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::PRIMARY, + ..Default::default() + }); + + let window = Arc::new(event_loop.create_window(attributes).unwrap()); + + let size = window.inner_size(); + let hidpi_factor = window.scale_factor(); + let surface = instance.create_surface(window.clone()).unwrap(); + + let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: Some(&surface), + force_fallback_adapter: false, + })) + .unwrap(); + + let (device, queue) = + block_on(adapter.request_device(&wgpu::DeviceDescriptor::default(), None)).unwrap(); + let queue = Arc::new(queue); + + // Set up swap chain + let surface_desc = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Fifo, + desired_maximum_frame_latency: 2, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![wgpu::TextureFormat::Bgra8Unorm], + }; + + surface.configure(&device, &surface_desc); + + Self { + device, + queue, + window, + surface_desc, + surface, + hidpi_factor, + } + } + + pub fn logical_size(&self) -> LogicalSize { + PhysicalSize::new(self.surface_desc.width, self.surface_desc.height) + .to_logical(self.hidpi_factor) + } + + pub fn handle_resize(&mut self, size: &PhysicalSize) { + self.surface_desc.width = size.width; + self.surface_desc.height = size.height; + self.surface.configure(&self.device, &self.surface_desc); + } +} + +pub struct ImguiState { + pub context: ContextGuard, + pub platform: WinitPlatform, + pub renderer: Renderer, + pub clear_color: wgpu::Color, + pub last_frame: Instant, + pub last_cursor: Option, +} +impl ImguiState { + pub fn new(window: &WindowState) -> Self { + let mut context_guard = ContextGuard::new(); + let mut context = context_guard.lock().unwrap(); + + let mut platform = imgui_winit_support::WinitPlatform::new(&mut context); + platform.attach_window( + context.io_mut(), + &window.window, + imgui_winit_support::HiDpiMode::Default, + ); + context.set_ini_filename(None); + + let font_size = (16.0 * window.hidpi_factor) as f32; + context.io_mut().font_global_scale = (1.0 / window.hidpi_factor) as f32; + + context.fonts().add_font(&[FontSource::TtfData { + data: include_bytes!("../../assets/selawk.ttf"), + size_pixels: font_size, + config: Some(imgui::FontConfig { + oversample_h: 1, + pixel_snap_h: true, + size_pixels: font_size, + ..Default::default() + }), + }]); + + let style = context.style_mut(); + style.use_light_colors(); + + // + // Set up dear imgui wgpu renderer + // + let renderer_config = RendererConfig { + texture_format: window.surface_desc.format, + ..Default::default() + }; + + let renderer = Renderer::new(&mut context, &window.device, &window.queue, renderer_config); + + let last_frame = Instant::now(); + let last_cursor = None; + + drop(context); + Self { + context: context_guard, + platform, + renderer, + clear_color: wgpu::Color::BLACK, + last_frame, + last_cursor, + } + } +} + +pub struct ContextGuard { + value: Option, +} + +impl ContextGuard { + fn new() -> Self { + Self { + value: Some(SuspendedContext::create()), + } + } + + pub fn lock(&mut self) -> Option> { + let sus = self.value.take()?; + match sus.activate() { + Ok(ctx) => Some(ContextLock { + ctx: Some(ctx), + holder: self, + }), + Err(sus) => { + self.value = Some(sus); + None + } + } + } +} + +pub struct ContextLock<'a> { + ctx: Option, + holder: &'a mut ContextGuard, +} + +impl<'a> Deref for ContextLock<'a> { + type Target = imgui::Context; + fn deref(&self) -> &Self::Target { + self.ctx.as_ref().unwrap() + } +} + +impl<'a> DerefMut for ContextLock<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.ctx.as_mut().unwrap() + } +} + +impl<'a> Drop for ContextLock<'a> { + fn drop(&mut self) { + self.holder.value = self.ctx.take().map(|c| c.suspend()) + } +} + +pub trait UiExt { + fn fullscreen_window(&self) -> Option>; +} + +impl UiExt for imgui::Ui { + fn fullscreen_window(&self) -> Option> { + self.window("fullscreen") + .position([0.0, 0.0], imgui::Condition::Always) + .size(self.window_size(), imgui::Condition::Always) + .flags(imgui::WindowFlags::NO_DECORATION) + .begin() + } +} diff --git a/src/app/game.rs b/src/app/game.rs new file mode 100644 index 0000000..41c50cc --- /dev/null +++ b/src/app/game.rs @@ -0,0 +1,357 @@ +use std::{sync::Arc, time::Instant}; +use wgpu::util::DeviceExt as _; +use winit::{ + dpi::LogicalSize, + event::{Event, KeyEvent, WindowEvent}, + event_loop::{ActiveEventLoop, EventLoopProxy}, + window::WindowId, +}; + +use crate::{ + controller::ControllerState, + emulator::{EmulatorClient, EmulatorCommand}, + renderer::GameRenderer, +}; + +use super::{ + common::{ImguiState, WindowState, WindowStateBuilder}, + input::InputWindow, + AppWindow, UserEvent, +}; + +pub struct GameWindow { + window: WindowState, + imgui: Option, + pipeline: wgpu::RenderPipeline, + bind_group: wgpu::BindGroup, + client: EmulatorClient, + controller: ControllerState, + proxy: EventLoopProxy, +} + +impl GameWindow { + pub fn new( + event_loop: &ActiveEventLoop, + client: EmulatorClient, + proxy: EventLoopProxy, + ) -> Self { + let window = WindowStateBuilder::new(event_loop) + .with_title("Shrooms VB") + .with_inner_size(LogicalSize::new(384, 244)) + .build(); + let device = &window.device; + + let eyes = Arc::new(GameRenderer::create_texture(device, "eye")); + client.send_command(EmulatorCommand::SetRenderer(GameRenderer { + queue: window.queue.clone(), + eyes: eyes.clone(), + })); + let eyes = eyes.create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); + let colors = Colors { + left: [1.0, 0.0, 0.0, 1.0], + right: [0.0, 0.7734375, 0.9375, 1.0], + }; + let color_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("colors"), + contents: bytemuck::bytes_of(&colors), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + let texture_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("texture bind group layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], + }); + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("bind group"), + layout: &texture_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&eyes), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: color_buf.as_entire_binding(), + }, + ], + }); + + let shader = device.create_shader_module(wgpu::include_wgsl!("../anaglyph.wgsl")); + let render_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("render pipeline layout"), + bind_group_layouts: &[&texture_bind_group_layout], + push_constant_ranges: &[], + }); + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("render pipeline"), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Bgra8UnormSrgb, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: Some(wgpu::Face::Back), + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + cache: None, + }); + + let controller = ControllerState::new(); + client.send_command(EmulatorCommand::SetKeys(controller.pressed())); + Self { + window, + imgui: None, + pipeline: render_pipeline, + bind_group, + client, + controller, + proxy, + } + } + + fn draw(&mut self, event_loop: &ActiveEventLoop) { + let window = &mut self.window; + let imgui = self.imgui.as_mut().unwrap(); + let mut context = imgui.context.lock().unwrap(); + + let now = Instant::now(); + context.io_mut().update_delta_time(now - imgui.last_frame); + imgui.last_frame = now; + + let frame = match window.surface.get_current_texture() { + Ok(frame) => frame, + Err(e) => { + eprintln!("dropped frame: {e:?}"); + return; + } + }; + imgui + .platform + .prepare_frame(context.io_mut(), &window.window) + .expect("Failed to prepare frame"); + let ui = context.new_frame(); + let mut menu_height = 0.0; + ui.main_menu_bar(|| { + menu_height = ui.window_size()[1]; + ui.menu("ROM", || { + if ui.menu_item("Open ROM") { + let rom = native_dialog::FileDialog::new() + .add_filter("Virtual Boy ROMs", &["vb", "vbrom"]) + .show_open_single_file() + .unwrap(); + if let Some(path) = rom { + self.client.send_command(EmulatorCommand::LoadGame(path)); + } + } + if ui.menu_item("Quit") { + event_loop.exit(); + } + }); + ui.menu("Emulation", || { + let has_game = self.client.has_game(); + if self.client.is_running() { + if ui.menu_item_config("Pause").enabled(has_game).build() { + self.client.send_command(EmulatorCommand::Pause); + } + } else if ui.menu_item_config("Resume").enabled(has_game).build() { + self.client.send_command(EmulatorCommand::Resume); + } + if ui.menu_item_config("Reset").enabled(has_game).build() { + self.client.send_command(EmulatorCommand::Reset); + } + }); + ui.menu("Video", || { + let current_dims = window.logical_size(); + for scale in 1..=4 { + let label = format!("x{scale}"); + let dims = LogicalSize::new(384 * scale, 224 * scale + 20); + let selected = dims == current_dims; + if ui.menu_item_config(label).selected(selected).build() { + if let Some(size) = window.window.request_inner_size(dims) { + window.handle_resize(&size); + } + } + } + }); + ui.menu("Input", || { + if ui.menu_item("Map Input") { + let input_window = Box::new(InputWindow::new(event_loop, self.proxy.clone())); + self.proxy + .send_event(UserEvent::OpenWindow(input_window)) + .unwrap(); + } + }); + }); + + let mut encoder: wgpu::CommandEncoder = window + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + + if imgui.last_cursor != ui.mouse_cursor() { + imgui.last_cursor = ui.mouse_cursor(); + imgui.platform.prepare_render(ui, &window.window); + } + + let view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(imgui.clear_color), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + // Draw the game + rpass.set_pipeline(&self.pipeline); + let window_width = window.surface_desc.width as f32; + let window_height = window.surface_desc.height as f32; + let menu_height = menu_height * window.hidpi_factor as f32; + let ((x, y), (width, height)) = + compute_game_bounds(window_width, window_height, menu_height); + rpass.set_viewport(x, y, width, height, 0.0, 1.0); + rpass.set_bind_group(0, &self.bind_group, &[]); + rpass.draw(0..6, 0..1); + + // Draw the menu on top of the game + rpass.set_viewport(0.0, 0.0, window_width, window_height, 0.0, 1.0); + imgui + .renderer + .render(context.render(), &window.queue, &window.device, &mut rpass) + .expect("Rendering failed"); + + drop(rpass); + + window.queue.submit(Some(encoder.finish())); + + frame.present(); + } + + fn handle_key_event(&mut self, event: &KeyEvent) { + if self.controller.key_event(event) { + self.client + .send_command(EmulatorCommand::SetKeys(self.controller.pressed())); + } + } +} + +impl AppWindow for GameWindow { + fn id(&self) -> WindowId { + self.window.window.id() + } + + fn init(&mut self) { + self.imgui = Some(ImguiState::new(&self.window)); + } + + fn handle_event(&mut self, event_loop: &ActiveEventLoop, event: &Event) { + match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::Resized(size) => self.window.handle_resize(size), + WindowEvent::CloseRequested => event_loop.exit(), + WindowEvent::KeyboardInput { event, .. } => self.handle_key_event(event), + WindowEvent::RedrawRequested => self.draw(event_loop), + _ => (), + }, + Event::AboutToWait => { + self.window.window.request_redraw(); + } + _ => (), + } + let window = &self.window; + let Some(imgui) = self.imgui.as_mut() else { + return; + }; + let mut context = imgui.context.lock().unwrap(); + imgui + .platform + .handle_event(context.io_mut(), &window.window, event); + } +} + +fn compute_game_bounds( + window_width: f32, + window_height: f32, + menu_height: f32, +) -> ((f32, f32), (f32, f32)) { + let available_width = window_width; + let available_height = window_height - menu_height; + + let width = available_width.min(available_height * 384.0 / 224.0); + let height = available_height.min(available_width * 224.0 / 384.0); + let x = (available_width - width) / 2.0; + let y = menu_height + (available_height - height) / 2.0; + ((x, y), (width, height)) +} + +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +struct Colors { + left: [f32; 4], + right: [f32; 4], +} diff --git a/src/app/input.rs b/src/app/input.rs new file mode 100644 index 0000000..276f297 --- /dev/null +++ b/src/app/input.rs @@ -0,0 +1,142 @@ +use std::time::Instant; + +use winit::{ + event::{Event, WindowEvent}, + event_loop::{ActiveEventLoop, EventLoopProxy}, +}; + +use super::{ + common::{ImguiState, UiExt as _, WindowState, WindowStateBuilder}, + AppWindow, UserEvent, +}; + +pub struct InputWindow { + window: WindowState, + imgui: Option, + proxy: EventLoopProxy, +} + +impl InputWindow { + pub fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy) -> Self { + let window = WindowStateBuilder::new(event_loop) + .with_title("Map Inputs") + .build(); + Self { + window, + imgui: None, + proxy, + } + } + + fn draw(&mut self) { + let window = &mut self.window; + let imgui = self.imgui.as_mut().unwrap(); + let mut context = imgui.context.lock().unwrap(); + + let now = Instant::now(); + context.io_mut().update_delta_time(now - imgui.last_frame); + imgui.last_frame = now; + + let frame = match window.surface.get_current_texture() { + Ok(frame) => frame, + Err(e) => { + eprintln!("dropped frame: {e:?}"); + return; + } + }; + imgui + .platform + .prepare_frame(context.io_mut(), &window.window) + .expect("Failed to prepare frame"); + let ui = context.new_frame(); + + if let Some(window) = ui.fullscreen_window() { + if let Some(table) = ui.begin_table("controls", 2) { + ui.table_next_row(); + + ui.table_next_column(); + ui.text("Key"); + + ui.table_next_column(); + ui.text("Value"); + table.end(); + } + window.end(); + } + let mut encoder: wgpu::CommandEncoder = window + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + + if imgui.last_cursor != ui.mouse_cursor() { + imgui.last_cursor = ui.mouse_cursor(); + imgui.platform.prepare_render(ui, &window.window); + } + + let view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(imgui.clear_color), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + // Draw the game + imgui + .renderer + .render(context.render(), &window.queue, &window.device, &mut rpass) + .expect("Rendering failed"); + + drop(rpass); + + window.queue.submit(Some(encoder.finish())); + + frame.present(); + } +} + +impl AppWindow for InputWindow { + fn id(&self) -> winit::window::WindowId { + self.window.window.id() + } + + fn init(&mut self) { + self.imgui = Some(ImguiState::new(&self.window)); + } + + fn handle_event(&mut self, _: &ActiveEventLoop, event: &Event) { + match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::Resized(size) => self.window.handle_resize(size), + WindowEvent::CloseRequested => self + .proxy + .send_event(UserEvent::CloseWindow(self.id())) + .unwrap(), + WindowEvent::RedrawRequested => self.draw(), + _ => (), + }, + Event::AboutToWait => { + self.window.window.request_redraw(); + } + _ => (), + } + + let window = &self.window; + let Some(imgui) = self.imgui.as_mut() else { + return; + }; + let mut context = imgui.context.lock().unwrap(); + imgui + .platform + .handle_event(context.io_mut(), &window.window, event); + } +} diff --git a/src/emulator.rs b/src/emulator.rs index 8682209..6e113a8 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -160,6 +160,7 @@ pub enum EmulatorCommand { SetKeys(VBKey), } +#[derive(Clone)] pub struct EmulatorClient { queue: mpsc::Sender, running: Arc, diff --git a/src/main.rs b/src/main.rs index 7a588ae..bca1441 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,8 +36,9 @@ fn main() -> Result<()> { emulator.run(); }); - let event_loop = EventLoop::new().unwrap(); + let event_loop = EventLoop::with_user_event().build().unwrap(); event_loop.set_control_flow(ControlFlow::Poll); - event_loop.run_app(&mut App::new(client))?; + let proxy = event_loop.create_proxy(); + event_loop.run_app(&mut App::new(client, proxy))?; Ok(()) } From 5cb36d0bcc00229031c2b887e980ad0de67982d8 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 10 Nov 2024 14:05:10 -0500 Subject: [PATCH 17/20] Functional input binding --- src/app.rs | 62 ++++++++++++++---------- src/app/common.rs | 11 ++++- src/app/game.rs | 46 +++++++++++------- src/app/input.rs | 105 ++++++++++++++++++++++++++++++++++++----- src/controller.rs | 36 +++++--------- src/input.rs | 71 ++++++++++++++++++++++++++++ src/main.rs | 1 + src/shrooms_vb_core.rs | 3 +- 8 files changed, 254 insertions(+), 81 deletions(-) create mode 100644 src/input.rs diff --git a/src/app.rs b/src/app.rs index 421fbd8..a5d62ac 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,8 @@ -use std::{collections::HashMap, fmt::Debug}; +use std::{ + collections::HashMap, + fmt::Debug, + sync::{Arc, RwLock}, +}; use game::GameWindow; use winit::{ @@ -8,7 +12,11 @@ use winit::{ window::WindowId, }; -use crate::emulator::EmulatorClient; +use crate::{ + controller::ControllerState, + emulator::{EmulatorClient, EmulatorCommand}, + input::InputMapper, +}; mod common; mod game; @@ -16,32 +24,35 @@ mod input; pub struct App { windows: HashMap>, - focused_window: Option, client: EmulatorClient, + input_mapper: Arc>, + controller: ControllerState, proxy: EventLoopProxy, } impl App { pub fn new(client: EmulatorClient, proxy: EventLoopProxy) -> Self { + let input_mapper = Arc::new(RwLock::new(InputMapper::new())); + let controller = ControllerState::new(input_mapper.clone()); Self { windows: HashMap::new(), - focused_window: None, client, + input_mapper, + controller, proxy, } } - - fn active_window(&mut self) -> Option<&mut Box> { - let active_window = self.focused_window?; - self.windows.get_mut(&active_window) - } } impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { - let mut window = GameWindow::new(event_loop, self.client.clone(), self.proxy.clone()); + let mut window = GameWindow::new( + event_loop, + self.client.clone(), + self.input_mapper.clone(), + self.proxy.clone(), + ); window.init(); - self.focused_window = Some(window.id()); self.windows.insert(window.id(), Box::new(window)); } @@ -51,11 +62,10 @@ impl ApplicationHandler for App { window_id: WindowId, event: WindowEvent, ) { - if let WindowEvent::Focused(focused) = event { - if focused { - self.focused_window = Some(window_id); - } else { - self.focused_window = None; + if let WindowEvent::KeyboardInput { event, .. } = &event { + if self.controller.key_event(event) { + self.client + .send_command(EmulatorCommand::SetKeys(self.controller.pressed())); } } let Some(window) = self.windows.get_mut(&window_id) else { @@ -82,17 +92,21 @@ impl ApplicationHandler for App { device_id: winit::event::DeviceId, event: winit::event::DeviceEvent, ) { - let Some(window) = self.active_window() else { - return; - }; - window.handle_event(event_loop, &Event::DeviceEvent { device_id, event }); + for window in self.windows.values_mut() { + window.handle_event( + event_loop, + &Event::DeviceEvent { + device_id, + event: event.clone(), + }, + ); + } } fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { - let Some(window) = self.active_window() else { - return; - }; - window.handle_event(event_loop, &Event::AboutToWait); + for window in self.windows.values_mut() { + window.handle_event(event_loop, &Event::AboutToWait); + } } } diff --git a/src/app/common.rs b/src/app/common.rs index 9aaf8b0..a5ec14f 100644 --- a/src/app/common.rs +++ b/src/app/common.rs @@ -235,14 +235,23 @@ impl<'a> Drop for ContextLock<'a> { pub trait UiExt { fn fullscreen_window(&self) -> Option>; + fn right_align_text>(&self, text: T, space: f32); } impl UiExt for imgui::Ui { fn fullscreen_window(&self) -> Option> { self.window("fullscreen") .position([0.0, 0.0], imgui::Condition::Always) - .size(self.window_size(), imgui::Condition::Always) + .size(self.io().display_size, imgui::Condition::Always) .flags(imgui::WindowFlags::NO_DECORATION) .begin() } + + fn right_align_text>(&self, text: T, space: f32) { + let width = self.calc_text_size(text.as_ref())[0]; + let [left, y] = self.cursor_pos(); + let right = left + space; + self.set_cursor_pos([right - width, y]); + self.text(text); + } } diff --git a/src/app/game.rs b/src/app/game.rs index 41c50cc..5b00c51 100644 --- a/src/app/game.rs +++ b/src/app/game.rs @@ -1,15 +1,18 @@ -use std::{sync::Arc, time::Instant}; +use std::{ + sync::{Arc, RwLock}, + time::Instant, +}; use wgpu::util::DeviceExt as _; use winit::{ dpi::LogicalSize, - event::{Event, KeyEvent, WindowEvent}, + event::{Event, WindowEvent}, event_loop::{ActiveEventLoop, EventLoopProxy}, window::WindowId, }; use crate::{ - controller::ControllerState, emulator::{EmulatorClient, EmulatorCommand}, + input::InputMapper, renderer::GameRenderer, }; @@ -25,7 +28,7 @@ pub struct GameWindow { pipeline: wgpu::RenderPipeline, bind_group: wgpu::BindGroup, client: EmulatorClient, - controller: ControllerState, + input_mapper: Arc>, proxy: EventLoopProxy, } @@ -33,6 +36,7 @@ impl GameWindow { pub fn new( event_loop: &ActiveEventLoop, client: EmulatorClient, + input_mapper: Arc>, proxy: EventLoopProxy, ) -> Self { let window = WindowStateBuilder::new(event_loop) @@ -153,15 +157,13 @@ impl GameWindow { cache: None, }); - let controller = ControllerState::new(); - client.send_command(EmulatorCommand::SetKeys(controller.pressed())); Self { window, imgui: None, pipeline: render_pipeline, bind_group, client, - controller, + input_mapper, proxy, } } @@ -170,6 +172,7 @@ impl GameWindow { let window = &mut self.window; let imgui = self.imgui.as_mut().unwrap(); let mut context = imgui.context.lock().unwrap(); + let mut new_size = None; let now = Instant::now(); context.io_mut().update_delta_time(now - imgui.last_frame); @@ -226,13 +229,18 @@ impl GameWindow { if ui.menu_item_config(label).selected(selected).build() { if let Some(size) = window.window.request_inner_size(dims) { window.handle_resize(&size); + new_size = Some(size); } } } }); ui.menu("Input", || { - if ui.menu_item("Map Input") { - let input_window = Box::new(InputWindow::new(event_loop, self.proxy.clone())); + if ui.menu_item("Bind Inputs") { + let input_window = Box::new(InputWindow::new( + event_loop, + self.input_mapper.clone(), + self.proxy.clone(), + )); self.proxy .send_event(UserEvent::OpenWindow(input_window)) .unwrap(); @@ -287,17 +295,21 @@ impl GameWindow { drop(rpass); + if let Some(size) = new_size { + imgui.platform.handle_event::( + context.io_mut(), + &window.window, + &Event::WindowEvent { + window_id: window.window.id(), + event: WindowEvent::Resized(size), + }, + ); + } + window.queue.submit(Some(encoder.finish())); frame.present(); } - - fn handle_key_event(&mut self, event: &KeyEvent) { - if self.controller.key_event(event) { - self.client - .send_command(EmulatorCommand::SetKeys(self.controller.pressed())); - } - } } impl AppWindow for GameWindow { @@ -307,6 +319,7 @@ impl AppWindow for GameWindow { fn init(&mut self) { self.imgui = Some(ImguiState::new(&self.window)); + self.window.window.request_redraw(); } fn handle_event(&mut self, event_loop: &ActiveEventLoop, event: &Event) { @@ -314,7 +327,6 @@ impl AppWindow for GameWindow { Event::WindowEvent { event, .. } => match event { WindowEvent::Resized(size) => self.window.handle_resize(size), WindowEvent::CloseRequested => event_loop.exit(), - WindowEvent::KeyboardInput { event, .. } => self.handle_key_event(event), WindowEvent::RedrawRequested => self.draw(event_loop), _ => (), }, diff --git a/src/app/input.rs b/src/app/input.rs index 276f297..bf56196 100644 --- a/src/app/input.rs +++ b/src/app/input.rs @@ -1,29 +1,62 @@ -use std::time::Instant; - -use winit::{ - event::{Event, WindowEvent}, - event_loop::{ActiveEventLoop, EventLoopProxy}, +use std::{ + sync::{Arc, RwLock}, + time::Instant, }; +use winit::{ + dpi::LogicalSize, + event::{Event, KeyEvent, WindowEvent}, + event_loop::{ActiveEventLoop, EventLoopProxy}, + platform::modifier_supplement::KeyEventExtModifierSupplement, +}; + +use crate::{input::InputMapper, shrooms_vb_core::VBKey}; + use super::{ - common::{ImguiState, UiExt as _, WindowState, WindowStateBuilder}, + common::{ImguiState, UiExt, WindowState, WindowStateBuilder}, AppWindow, UserEvent, }; pub struct InputWindow { window: WindowState, imgui: Option, + input_mapper: Arc>, proxy: EventLoopProxy, + now_binding: Option, } +const KEY_NAMES: [(VBKey, &str); 14] = [ + (VBKey::LU, "Up"), + (VBKey::LD, "Down"), + (VBKey::LL, "Left"), + (VBKey::LR, "Right"), + (VBKey::SEL, "Select"), + (VBKey::STA, "Start"), + (VBKey::B, "B"), + (VBKey::A, "A"), + (VBKey::LT, "L-Trigger"), + (VBKey::RT, "R-Trigger"), + (VBKey::RU, "R-Up"), + (VBKey::RD, "R-Down"), + (VBKey::RL, "R-Left"), + (VBKey::RR, "R-Right"), +]; + impl InputWindow { - pub fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy) -> Self { + pub fn new( + event_loop: &ActiveEventLoop, + input_mapper: Arc>, + proxy: EventLoopProxy, + ) -> Self { let window = WindowStateBuilder::new(event_loop) - .with_title("Map Inputs") + .with_title("Bind Inputs") + .with_inner_size(LogicalSize::new(600, 400)) .build(); Self { window, imgui: None, + input_mapper, + now_binding: None, proxy, } } @@ -50,17 +83,50 @@ impl InputWindow { .expect("Failed to prepare frame"); let ui = context.new_frame(); - if let Some(window) = ui.fullscreen_window() { + let mut render_key_bindings = || { if let Some(table) = ui.begin_table("controls", 2) { + let binding_names = { + let mapper = self.input_mapper.read().unwrap(); + mapper.binding_names() + }; ui.table_next_row(); - ui.table_next_column(); - ui.text("Key"); + for (key, name) in KEY_NAMES { + let binding = binding_names.get(&key).map(|s| s.as_str()); + ui.table_next_column(); + let [space, _] = ui.content_region_avail(); + ui.group(|| { + ui.right_align_text(name, space * 0.20); + ui.same_line(); + let label_text = if self.now_binding == Some(key) { + "Press any input" + } else { + binding.unwrap_or("") + }; + let label = format!("{}##{}", label_text, name); + if ui.button_with_size(label, [space * 0.60, 0.0]) { + self.now_binding = Some(key); + } + }); + ui.same_line(); + if ui.button(format!("Clear##{name}")) { + let mut mapper = self.input_mapper.write().unwrap(); + mapper.clear_binding(key); + } + } - ui.table_next_column(); - ui.text("Value"); table.end(); } + }; + + if let Some(window) = ui.fullscreen_window() { + if let Some(tabs) = ui.tab_bar("tabs") { + if let Some(tab) = ui.tab_item("Player 1") { + render_key_bindings(); + tab.end(); + } + tabs.end(); + } window.end(); } let mut encoder: wgpu::CommandEncoder = window @@ -102,6 +168,17 @@ impl InputWindow { frame.present(); } + + fn try_bind_key(&mut self, event: &KeyEvent) { + if !event.state.is_pressed() { + return; + } + let Some(vb) = self.now_binding.take() else { + return; + }; + let mut mapper = self.input_mapper.write().unwrap(); + mapper.bind_key(vb, event.key_without_modifiers()); + } } impl AppWindow for InputWindow { @@ -111,6 +188,7 @@ impl AppWindow for InputWindow { fn init(&mut self) { self.imgui = Some(ImguiState::new(&self.window)); + self.window.window.request_redraw(); } fn handle_event(&mut self, _: &ActiveEventLoop, event: &Event) { @@ -121,6 +199,7 @@ impl AppWindow for InputWindow { .proxy .send_event(UserEvent::CloseWindow(self.id())) .unwrap(), + WindowEvent::KeyboardInput { event, .. } => self.try_bind_key(event), WindowEvent::RedrawRequested => self.draw(), _ => (), }, diff --git a/src/controller.rs b/src/controller.rs index 1e8995e..70861ab 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -1,17 +1,18 @@ -use winit::{ - event::{ElementState, KeyEvent}, - keyboard::{Key, NamedKey}, -}; +use std::sync::{Arc, RwLock}; -use crate::shrooms_vb_core::VBKey; +use winit::event::{ElementState, KeyEvent}; + +use crate::{input::InputMapper, shrooms_vb_core::VBKey}; pub struct ControllerState { + input_mapper: Arc>, pressed: VBKey, } impl ControllerState { - pub fn new() -> Self { + pub fn new(input_mapper: Arc>) -> Self { Self { + input_mapper, pressed: VBKey::SGN, } } @@ -21,7 +22,7 @@ impl ControllerState { } pub fn key_event(&mut self, event: &KeyEvent) -> bool { - let Some(input) = self.key_to_input(&event.logical_key) else { + let Some(input) = self.key_event_to_input(event) else { return false; }; match event.state { @@ -42,23 +43,8 @@ impl ControllerState { } } - fn key_to_input(&self, key: &Key) -> Option { - match key.as_ref() { - Key::Character("a") => Some(VBKey::SEL), - Key::Character("s") => Some(VBKey::STA), - Key::Character("d") => Some(VBKey::B), - Key::Character("f") => Some(VBKey::A), - Key::Character("e") => Some(VBKey::LT), - Key::Character("r") => Some(VBKey::RT), - Key::Character("i") => Some(VBKey::RU), - Key::Character("j") => Some(VBKey::RL), - Key::Character("k") => Some(VBKey::RD), - Key::Character("l") => Some(VBKey::RR), - Key::Named(NamedKey::ArrowUp) => Some(VBKey::LU), - Key::Named(NamedKey::ArrowLeft) => Some(VBKey::LL), - Key::Named(NamedKey::ArrowDown) => Some(VBKey::LD), - Key::Named(NamedKey::ArrowRight) => Some(VBKey::LR), - _ => None, - } + fn key_event_to_input(&self, event: &KeyEvent) -> Option { + let mapper = self.input_mapper.read().unwrap(); + mapper.key_event(event) } } diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..5367d60 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,71 @@ +use std::collections::HashMap; + +use winit::{ + event::KeyEvent, + keyboard::{Key, NamedKey}, + platform::modifier_supplement::KeyEventExtModifierSupplement, +}; + +use crate::shrooms_vb_core::VBKey; + +pub struct InputMapper { + vb_bindings: HashMap, + key_bindings: HashMap, +} + +impl InputMapper { + pub fn new() -> Self { + let mut mapper = Self { + vb_bindings: HashMap::new(), + key_bindings: HashMap::new(), + }; + mapper.bind_key(VBKey::SEL, Key::Character("a".into())); + mapper.bind_key(VBKey::STA, Key::Character("s".into())); + mapper.bind_key(VBKey::B, Key::Character("d".into())); + mapper.bind_key(VBKey::A, Key::Character("f".into())); + mapper.bind_key(VBKey::LT, Key::Character("e".into())); + mapper.bind_key(VBKey::RT, Key::Character("r".into())); + mapper.bind_key(VBKey::RU, Key::Character("i".into())); + mapper.bind_key(VBKey::RL, Key::Character("j".into())); + mapper.bind_key(VBKey::RD, Key::Character("k".into())); + mapper.bind_key(VBKey::RR, Key::Character("l".into())); + mapper.bind_key(VBKey::LU, Key::Named(NamedKey::ArrowUp)); + mapper.bind_key(VBKey::LL, Key::Named(NamedKey::ArrowLeft)); + mapper.bind_key(VBKey::LD, Key::Named(NamedKey::ArrowDown)); + mapper.bind_key(VBKey::LR, Key::Named(NamedKey::ArrowRight)); + mapper + } + + pub fn binding_names(&self) -> HashMap { + self.vb_bindings + .iter() + .map(|(k, v)| { + let name = match v { + Key::Character(char) => char.to_string(), + Key::Named(key) => format!("{:?}", key), + k => format!("{:?}", k), + }; + (*k, name) + }) + .collect() + } + + pub fn bind_key(&mut self, vb: VBKey, key: Key) { + if let Some(old) = self.vb_bindings.insert(vb, key.clone()) { + self.key_bindings.remove(&old); + } + self.key_bindings.insert(key, vb); + } + + pub fn clear_binding(&mut self, vb: VBKey) { + if let Some(old) = self.vb_bindings.remove(&vb) { + self.key_bindings.remove(&old); + } + } + + pub fn key_event(&self, event: &KeyEvent) -> Option { + self.key_bindings + .get(&event.key_without_modifiers()) + .cloned() + } +} diff --git a/src/main.rs b/src/main.rs index bca1441..1b79b00 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod app; mod audio; mod controller; mod emulator; +mod input; mod renderer; mod shrooms_vb_core; diff --git a/src/shrooms_vb_core.rs b/src/shrooms_vb_core.rs index 8fb1789..324689d 100644 --- a/src/shrooms_vb_core.rs +++ b/src/shrooms_vb_core.rs @@ -26,7 +26,8 @@ enum VBDataType { } bitflags! { - #[derive(Clone, Copy, Debug)] + #[repr(transparent)] + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct VBKey: u16 { const PWR = 0x0001; const SGN = 0x0002; From 6dc3697bafe8ab2ee2641a8bbc93c936a1c10cf1 Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 10 Nov 2024 15:03:07 -0500 Subject: [PATCH 18/20] Fix crash on windows when window is minimized --- src/app/common.rs | 13 ++++++++++--- src/app/game.rs | 19 +++++++++++++++++-- src/app/input.rs | 4 +++- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/app/common.rs b/src/app/common.rs index a5ec14f..396acb2 100644 --- a/src/app/common.rs +++ b/src/app/common.rs @@ -58,6 +58,7 @@ pub struct WindowState { pub surface_desc: wgpu::SurfaceConfiguration, pub surface: wgpu::Surface<'static>, pub hidpi_factor: f64, + pub minimized: bool, } impl WindowState { @@ -105,6 +106,7 @@ impl WindowState { surface_desc, surface, hidpi_factor, + minimized: false, } } @@ -114,9 +116,14 @@ impl WindowState { } pub fn handle_resize(&mut self, size: &PhysicalSize) { - self.surface_desc.width = size.width; - self.surface_desc.height = size.height; - self.surface.configure(&self.device, &self.surface_desc); + if size.width > 0 && size.height > 0 { + self.minimized = false; + self.surface_desc.width = size.width; + self.surface_desc.height = size.height; + self.surface.configure(&self.device, &self.surface_desc); + } else { + self.minimized = true; + } } } diff --git a/src/app/game.rs b/src/app/game.rs index 5b00c51..efbeae0 100644 --- a/src/app/game.rs +++ b/src/app/game.rs @@ -30,6 +30,7 @@ pub struct GameWindow { client: EmulatorClient, input_mapper: Arc>, proxy: EventLoopProxy, + paused_due_to_minimize: bool, } impl GameWindow { @@ -165,6 +166,7 @@ impl GameWindow { client, input_mapper, proxy, + paused_due_to_minimize: false, } } @@ -181,7 +183,9 @@ impl GameWindow { let frame = match window.surface.get_current_texture() { Ok(frame) => frame, Err(e) => { - eprintln!("dropped frame: {e:?}"); + if !self.window.minimized { + eprintln!("dropped frame: {e:?}"); + } return; } }; @@ -325,7 +329,18 @@ impl AppWindow for GameWindow { fn handle_event(&mut self, event_loop: &ActiveEventLoop, event: &Event) { match event { Event::WindowEvent { event, .. } => match event { - WindowEvent::Resized(size) => self.window.handle_resize(size), + WindowEvent::Resized(size) => { + self.window.handle_resize(size); + if self.window.minimized { + if self.client.is_running() { + self.client.send_command(EmulatorCommand::Pause); + self.paused_due_to_minimize = true; + } + } else if self.paused_due_to_minimize { + self.client.send_command(EmulatorCommand::Resume); + self.paused_due_to_minimize = false; + } + } WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::RedrawRequested => self.draw(event_loop), _ => (), diff --git a/src/app/input.rs b/src/app/input.rs index bf56196..d5ffe7e 100644 --- a/src/app/input.rs +++ b/src/app/input.rs @@ -73,7 +73,9 @@ impl InputWindow { let frame = match window.surface.get_current_texture() { Ok(frame) => frame, Err(e) => { - eprintln!("dropped frame: {e:?}"); + if !self.window.minimized { + eprintln!("dropped frame: {e:?}"); + } return; } }; From e8b706df2089adfd72e9191eba42da80257c496f Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 10 Nov 2024 15:53:14 -0500 Subject: [PATCH 19/20] Use high-priority thread for emulation --- Cargo.lock | 21 +++++++++++++++++++++ Cargo.toml | 1 + src/main.rs | 27 ++++++++++++++++----------- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8db4c14..a61b2f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1892,6 +1892,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + [[package]] name = "same-file" version = "1.0.6" @@ -1972,6 +1978,7 @@ dependencies = [ "pollster", "rtrb", "rubato", + "thread-priority", "wgpu", "winit", ] @@ -2118,6 +2125,20 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "thread-priority" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe075d7053dae61ac5413a34ea7d4913b6e6207844fd726bdd858b37ff72bf5" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "libc", + "log", + "rustversion", + "winapi", +] + [[package]] name = "tiny-skia" version = "0.11.4" diff --git a/Cargo.toml b/Cargo.toml index 23abe96..73f97d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ num-traits = "0.2" pollster = "0.4" rtrb = "0.3" rubato = "0.16" +thread-priority = "1" wgpu = "22.1" winit = "0.30" diff --git a/src/main.rs b/src/main.rs index 1b79b00..b8de1cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,10 @@ -use std::{path::PathBuf, process, thread}; +use std::{path::PathBuf, process}; use anyhow::Result; use app::App; use clap::Parser; use emulator::EmulatorBuilder; +use thread_priority::{ThreadBuilder, ThreadPriority}; use winit::event_loop::{ControlFlow, EventLoop}; mod app; @@ -26,16 +27,20 @@ fn main() -> Result<()> { if let Some(path) = args.rom { builder = builder.with_rom(&path); } - thread::spawn(move || { - let mut emulator = match builder.build() { - Ok(e) => e, - Err(err) => { - eprintln!("Error initializing emulator: {err}"); - process::exit(1); - } - }; - emulator.run(); - }); + + ThreadBuilder::default() + .name("Emulator".to_owned()) + .priority(ThreadPriority::Max) + .spawn_careless(move || { + let mut emulator = match builder.build() { + Ok(e) => e, + Err(err) => { + eprintln!("Error initializing emulator: {err}"); + process::exit(1); + } + }; + emulator.run(); + })?; let event_loop = EventLoop::with_user_event().build().unwrap(); event_loop.set_control_flow(ControlFlow::Poll); From 99d69703235b2e0cd3ec7f6123143951d6c6a1fd Mon Sep 17 00:00:00 2001 From: Simon Gellis Date: Sun, 10 Nov 2024 16:08:49 -0500 Subject: [PATCH 20/20] Block the emulator thread when idle --- src/emulator.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/emulator.rs b/src/emulator.rs index 6e113a8..38fdab6 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -3,7 +3,7 @@ use std::{ path::{Path, PathBuf}, sync::{ atomic::{AtomicBool, Ordering}, - mpsc::{self, TryRecvError}, + mpsc::{self, RecvError, TryRecvError}, Arc, }, }; @@ -94,19 +94,33 @@ impl Emulator { let mut eye_contents = vec![0u8; 384 * 224 * 2]; let mut audio_samples = vec![]; loop { + let mut idle = true; if self.running.load(Ordering::Acquire) { + idle = false; self.sim.emulate_frame(); } if let Some(renderer) = &mut self.renderer { if self.sim.read_pixels(&mut eye_contents) { + idle = false; renderer.render(&eye_contents); } } self.sim.read_samples(&mut audio_samples); if !audio_samples.is_empty() { + idle = false; self.audio.update(&audio_samples); audio_samples.clear(); } + if idle { + // The game is paused, and we have output all the video/audio we have. + // Block the thread until a new command comes in. + match self.commands.recv() { + Ok(command) => self.handle_command(command), + Err(RecvError) => { + return; + } + } + } loop { match self.commands.try_recv() { Ok(command) => self.handle_command(command),