diff --git a/Cargo.lock b/Cargo.lock index a61b2f2..4990382 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,9 +42,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" [[package]] name = "alsa" @@ -106,9 +106,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.17" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -155,9 +155,24 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" + +[[package]] +name = "arboard" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" +dependencies = [ + "clipboard-win", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "parking_lot", + "x11rb", +] [[package]] name = "arrayref" @@ -177,12 +192,6 @@ 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" @@ -192,6 +201,182 @@ dependencies = [ "libloading", ] +[[package]] +name = "ashpd" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c39d707614dbcc6bed00015539f488d8e3fe3e66ed60961efc0c90f4b380b3" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand", + "raw-window-handle", + "serde", + "serde_repr", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", + "tracing", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "async-signal" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -213,7 +398,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools 0.13.0", + "itertools", "proc-macro2", "quote", "regex", @@ -264,6 +449,19 @@ dependencies = [ "objc2", ] +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -290,6 +488,12 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.8.0" @@ -324,9 +528,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.34" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ "jobserver", "libc", @@ -366,12 +570,6 @@ 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 = "clang-sys" version = "1.8.1" @@ -385,9 +583,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" dependencies = [ "clap_builder", "clap_derive", @@ -395,9 +593,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" dependencies = [ "anstream", "anstyle", @@ -419,38 +617,17 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" [[package]] -name = "cocoa" -version = "0.25.0" +name = "clipboard-win" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" 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", + "error-code", ] [[package]] @@ -529,6 +706,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -542,7 +729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "core-graphics-types", "foreign-types", "libc", @@ -555,7 +742,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "libc", ] @@ -582,8 +769,7 @@ dependencies = [ [[package]] name = "cpal" version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +source = "git+https://github.com/sidit77/cpal.git?rev=66ed6be#66ed6bec97f25ec7f02a82f50d1aa9aef733a58e" dependencies = [ "alsa", "core-foundation-sys", @@ -631,33 +817,23 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "dlib" version = "0.5.2" @@ -688,12 +864,164 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" +[[package]] +name = "ecolor" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775cfde491852059e386c4e1deb4aef381c617dc364184c6f6afee99b87c402b" +dependencies = [ + "bytemuck", + "emath", +] + +[[package]] +name = "egui" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53eafabcce0cb2325a59a98736efe0bf060585b437763f8c476957fb274bb974" +dependencies = [ + "ahash", + "emath", + "epaint", + "log", + "nohash-hasher", +] + +[[package]] +name = "egui-wgpu" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00fd5d06d8405397e64a928fa0ef3934b3c30273ea7603e3dc4627b1f7a1a82" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "epaint", + "log", + "thiserror", + "type-map", + "web-time", + "wgpu", + "winit", +] + +[[package]] +name = "egui-winit" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9c430f4f816340e8e8c1b20eec274186b1be6bc4c7dfc467ed50d57abc36c6" +dependencies = [ + "ahash", + "arboard", + "egui", + "log", + "raw-window-handle", + "smithay-clipboard", + "web-time", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_extras" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3c1f5cd8dfe2ade470a218696c66cf556fcfd701e7830fa2e9f4428292a2a1" +dependencies = [ + "ahash", + "egui", + "enum-map", + "log", + "mime_guess2", +] + [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "emath" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1fe0049ce51d0fb414d029e668dd72eb30bc2b739bf34296ed97bd33df544f3" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", + "serde", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "enumflags2" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "epaint" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a32af8da821bd4f43f2c137e295459ee2e1661d87ca8779dfa0eaf45d870e20f" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "log", + "nohash-hasher", + "parking_lot", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483440db0b7993cf77a20314f08311dbe95675092405518c0677aa08c151a3ea" + [[package]] name = "equivalent" version = "1.0.1" @@ -710,6 +1038,45 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "error-code" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foreign-types" version = "0.5.0" @@ -737,6 +1104,89 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -758,6 +1208,40 @@ dependencies = [ "wasi", ] +[[package]] +name = "gilrs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb2c998745a3c1ac90f64f4f7b3a54219fd3612d7705e7798212935641ed18f" +dependencies = [ + "fnv", + "gilrs-core", + "log", + "uuid", + "vec_map", +] + +[[package]] +name = "gilrs-core" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495af945e45efd6386227613cd9fb7bd7c43d3c095040e30c5304c489e6abed5" +dependencies = [ + "core-foundation 0.10.0", + "inotify", + "io-kit-sys", + "js-sys", + "libc", + "libudev-sys", + "log", + "nix", + "uuid", + "vec_map", + "wasm-bindgen", + "web-sys", + "windows 0.58.0", +] + [[package]] name = "gl_generator" version = "0.14.0" @@ -860,9 +1344,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" [[package]] name = "hassle-rs" @@ -891,6 +1375,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hexf-parse" version = "0.2.1" @@ -907,50 +1397,142 @@ dependencies = [ ] [[package]] -name = "imgui" -version = "0.12.0" +name = "icu_collections" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8addafa5cecf0515812226e806913814e02ce38d10215778082af5174abe5669" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "imgui-sys", - "mint", - "parking_lot", + "displaydoc", + "yoke", + "zerofrom", + "zerovec", ] [[package]] -name = "imgui-sys" -version = "0.12.0" +name = "icu_locid" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ead193f9f4b60398e8b8f4ab1483e2321640d87aeebdaa3e5f44c55633ccd804" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ - "cc", - "cfg-if", - "chlorine", - "mint", + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", ] [[package]] -name = "imgui-wgpu" -version = "0.25.0" -source = "git+https://github.com/Yatekii/imgui-wgpu-rs?rev=2edd348#2edd348a0fc11e9e72f19060c34a6e45c760b116" +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" dependencies = [ - "bytemuck", - "imgui", - "log", + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", "smallvec", - "wgpu", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", ] [[package]] -name = "imgui-winit-support" -version = "0.13.0" +name = "icu_normalizer_data" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff7fcccfa9efab56c94274c0fec9939bb14149342b49e6a425883a5b7dda6a3f" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ - "imgui", - "winit", + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -960,7 +1542,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.15.1", +] + +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.6.0", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "io-kit-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" +dependencies = [ + "core-foundation-sys", + "mach2", ] [[package]] @@ -969,15 +1581,6 @@ 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" @@ -1046,9 +1649,9 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "libc" -version = "0.2.161" +version = "0.2.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" [[package]] name = "libloading" @@ -1071,12 +1674,28 @@ dependencies = [ "redox_syscall 0.5.7", ] +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "litrs" version = "0.4.1" @@ -1132,6 +1751,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "metal" version = "0.29.0" @@ -1147,18 +1775,28 @@ dependencies = [ "paste", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess2" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a3333bb1609500601edc766a39b4c1772874a4ce26022f4d866854dc020c41" +dependencies = [ + "mime", + "unicase", +] + [[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" - [[package]] name = "naga" version = "22.1.0" @@ -1180,29 +1818,6 @@ 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" @@ -1228,7 +1843,7 @@ dependencies = [ "log", "ndk-sys 0.6.0+11769913", "num_enum", - "raw-window-handle 0.6.2", + "raw-window-handle", "thiserror", ] @@ -1256,6 +1871,25 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", + "memoffset", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nom" version = "7.1.3" @@ -1334,17 +1968,6 @@ 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" @@ -1548,15 +2171,6 @@ 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" @@ -1595,6 +2209,16 @@ dependencies = [ "libredox", ] +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "owned_ttf_parser" version = "0.25.0" @@ -1604,6 +2228,12 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.3" @@ -1665,6 +2295,23 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.31" @@ -1673,9 +2320,9 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "polling" -version = "3.7.3" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", @@ -1686,12 +2333,27 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + [[package]] name = "pollster" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "presser" version = "0.3.1" @@ -1749,18 +2411,42 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[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.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" - [[package]] name = "raw-window-handle" version = "0.6.2" @@ -1794,17 +2480,6 @@ 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" @@ -1819,9 +2494,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -1840,6 +2515,28 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "rfd" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f6f80a9b882647d9014673ca9925d30ffc9750f2eed2b4490e189eaebd01e8" +dependencies = [ + "ashpd", + "block2", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "pollster 0.3.0", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.48.0", +] + [[package]] name = "rtrb" version = "0.3.1" @@ -1881,9 +2578,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.38" +version = "0.38.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" dependencies = [ "bitflags 2.6.0", "errno", @@ -1934,18 +2631,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", @@ -1968,21 +2676,33 @@ dependencies = [ "cc", "clap", "cpal", - "imgui", - "imgui-wgpu", - "imgui-winit-support", - "itertools 0.13.0", - "native-dialog", + "egui", + "egui-wgpu", + "egui-winit", + "egui_extras", + "gilrs", + "itertools", "num-derive", "num-traits", - "pollster", + "pollster 0.4.0", + "rfd", "rtrb", "rubato", "thread-priority", "wgpu", + "windows 0.58.0", "winit", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.9" @@ -2032,6 +2752,17 @@ dependencies = [ "xkeysym", ] +[[package]] +name = "smithay-clipboard" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" +dependencies = [ + "libc", + "smithay-client-toolkit", + "wayland-backend", +] + [[package]] name = "smol_str" version = "0.2.2" @@ -2050,6 +2781,12 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -2096,6 +2833,30 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "tempfile" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -2107,18 +2868,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.66" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.66" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", @@ -2164,6 +2925,16 @@ dependencies = [ "strict-num", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "toml_datetime" version = "0.6.8" @@ -2188,14 +2959,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "tracing-core" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] [[package]] name = "transpose" @@ -2213,6 +2999,32 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5902c5d130972a0000f60860bfbf46f7ca3db5391eddfedd1b8728bd9dc96c0e" +[[package]] +name = "type-map" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" +dependencies = [ + "rustc-hash", +] + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + [[package]] name = "unicode-ident" version = "1.0.13" @@ -2237,28 +3049,60 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version_check" 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" @@ -2472,13 +3316,21 @@ dependencies = [ ] [[package]] -name = "wfd" -version = "0.1.7" +name = "webbrowser" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e713040b67aae5bf1a0ae3e1ebba8cc29ab2b90da9aa1bff6e09031a8a41d7a8" +checksum = "2e5f07fb9bc8de2ddfe6b24a71a75430673fd679e568c48b52716cef1cfae923" dependencies = [ - "libc", - "winapi", + "block2", + "core-foundation 0.10.0", + "home", + "jni", + "log", + "ndk-context", + "objc2", + "objc2-foundation", + "url", + "web-sys", ] [[package]] @@ -2495,7 +3347,7 @@ dependencies = [ "naga", "parking_lot", "profiling", - "raw-window-handle 0.6.2", + "raw-window-handle", "smallvec", "static_assertions", "wasm-bindgen", @@ -2523,7 +3375,7 @@ dependencies = [ "once_cell", "parking_lot", "profiling", - "raw-window-handle 0.6.2", + "raw-window-handle", "rustc-hash", "smallvec", "thiserror", @@ -2565,7 +3417,7 @@ dependencies = [ "parking_lot", "profiling", "range-alloc", - "raw-window-handle 0.6.2", + "raw-window-handle", "renderdoc-sys", "rustc-hash", "smallvec", @@ -2587,18 +3439,6 @@ 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" @@ -2653,6 +3493,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" dependencies = [ "windows-core 0.54.0", + "windows-implement 0.53.0", + "windows-interface 0.53.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", "windows-targets 0.52.6", ] @@ -2671,10 +3523,67 @@ version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" dependencies = [ - "windows-result", + "windows-result 0.1.2", "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "windows-interface" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -2684,6 +3593,25 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -2693,6 +3621,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2904,7 +3841,7 @@ dependencies = [ "calloop", "cfg_aliases 0.2.1", "concurrent-queue", - "core-foundation", + "core-foundation 0.9.4", "core-graphics", "cursor-icon", "dpi", @@ -2919,7 +3856,7 @@ dependencies = [ "orbclient", "percent-encoding", "pin-project", - "raw-window-handle 0.6.2", + "raw-window-handle", "redox_syscall 0.4.1", "rustix", "sctk-adwaita", @@ -2950,6 +3887,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "x11-dl" version = "2.21.0" @@ -2988,6 +3937,16 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "xkbcommon-dl" version = "0.4.2" @@ -3009,9 +3968,96 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" +checksum = "af310deaae937e48a26602b730250b4949e125f468f11e6990be3e5304ddd96f" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1162094dc63b1629fcc44150bcceeaa80798cd28bcbe7fa987b65a034c258608" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-util", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd2dcdce3e2727f7d74b7e33b5a89539b3cc31049562137faf7ae4eb86cd16d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.87", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "856b7a38811f71846fd47856ceee8bccaec8399ff53fb370247e66081ace647b" +dependencies = [ + "serde", + "static_assertions", + "winnow", + "zvariant", +] [[package]] name = "zerocopy" @@ -3019,6 +4065,7 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] @@ -3032,3 +4079,89 @@ dependencies = [ "quote", "syn 2.0.87", ] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "zvariant" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1200ee6ac32f1e5a312e455a949a4794855515d34f9909f4a3e082d14e1a56f" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "url", + "winnow", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687e3b97fae6c9104fbbd36c73d27d149abf04fb874e2efbd84838763daa8916" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.87", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20d1d011a38f12360e5fcccceeff5e2c42a8eb7f27f0dcba97a0862ede05c9c6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "static_assertions", + "syn 2.0.87", + "winnow", +] diff --git a/Cargo.toml b/Cargo.toml index 73f97d5..2db1fb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,21 +8,26 @@ anyhow = "1" bitflags = "2" bytemuck = { version = "1", features = ["derive"] } clap = { version = "4", features = ["derive"] } -cpal = "0.15" -imgui = { version = "0.12", features = ["tables-api"] } -imgui-wgpu = { git = "https://github.com/Yatekii/imgui-wgpu-rs", rev = "2edd348" } -imgui-winit-support = "0.13" +cpal = { git = "https://github.com/sidit77/cpal.git", rev = "66ed6be" } +egui = "0.29" +egui_extras = "0.29" +egui-winit = "0.29" +egui-wgpu = { version = "0.29", features = ["winit"] } +gilrs = "0.11" itertools = "0.13" -native-dialog = "0.7" num-derive = "0.4" num-traits = "0.2" pollster = "0.4" +rfd = "0.15" rtrb = "0.3" rubato = "0.16" thread-priority = "1" wgpu = "22.1" winit = "0.30" +[target.'cfg(windows)'.dependencies] +windows = { version = "0.58", features = ["Win32_System_Threading"] } + [build-dependencies] cc = "1" diff --git a/assets/selawk.ttf b/assets/selawik.ttf similarity index 100% rename from assets/selawk.ttf rename to assets/selawik.ttf diff --git a/build.rs b/build.rs index 17bd565..ab23bf9 100644 --- a/build.rs +++ b/build.rs @@ -5,8 +5,10 @@ fn main() { cc::Build::new() .include(Path::new("shrooms-vb-core/core")) .opt_level(2) - .flag_if_supported("-flto") .flag_if_supported("-fno-strict-aliasing") + .define("VB_LITTLE_ENDIAN", None) + .define("VB_SIGNED_PROPAGATE", None) + .define("VB_DIV_GENERIC", None) .file(Path::new("shrooms-vb-core/core/vb.c")) .compile("vb"); } diff --git a/shrooms-vb-core b/shrooms-vb-core index ae22c95..18b2c58 160000 --- a/shrooms-vb-core +++ b/shrooms-vb-core @@ -1 +1 @@ -Subproject commit ae22c95dbee3d0b338168bfdf98143e6eddc6c70 +Subproject commit 18b2c589e6cacec5a0bd0f450cedf2f8fe3a2bc8 diff --git a/src/app.rs b/src/app.rs index a5d62ac..b7c0fb0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,131 +1,368 @@ -use std::{ - collections::HashMap, - fmt::Debug, - sync::{Arc, RwLock}, -}; +use std::{collections::HashSet, num::NonZero, sync::Arc, thread}; -use game::GameWindow; +use egui::{ + ahash::{HashMap, HashMapExt}, + Context, FontData, FontDefinitions, FontFamily, TextWrapMode, ViewportBuilder, ViewportCommand, + ViewportId, ViewportInfo, +}; +use gilrs::{EventType, Gilrs}; use winit::{ application::ApplicationHandler, - event::{Event, WindowEvent}, + event::WindowEvent, event_loop::{ActiveEventLoop, EventLoopProxy}, - window::WindowId, + window::Window, }; use crate::{ - controller::ControllerState, - emulator::{EmulatorClient, EmulatorCommand}, - input::InputMapper, + controller::ControllerManager, + emulator::{EmulatorClient, SimId}, + input::MappingProvider, + window::{AppWindow, GameWindow, InputWindow}, }; -mod common; -mod game; -mod input; - -pub struct App { - windows: HashMap>, +pub struct Application { client: EmulatorClient, - input_mapper: Arc>, - controller: ControllerState, proxy: EventLoopProxy, + mappings: MappingProvider, + controllers: ControllerManager, + viewports: HashMap, + focused: Option, } -impl App { +impl Application { 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(), - client, - input_mapper, - controller, - proxy, + let mappings = MappingProvider::new(); + let controllers = ControllerManager::new(client.clone(), &mappings); + { + let mappings = mappings.clone(); + let proxy = proxy.clone(); + thread::spawn(|| process_gamepad_input(mappings, proxy)); } + Self { + client, + proxy, + mappings, + controllers, + viewports: HashMap::new(), + focused: None, + } + } + + fn open(&mut self, event_loop: &ActiveEventLoop, window: Box) { + let viewport_id = window.viewport_id(); + if self.viewports.contains_key(&viewport_id) { + return; + } + self.viewports + .insert(viewport_id, Viewport::new(event_loop, window)); } } -impl ApplicationHandler for App { +impl ApplicationHandler for Application { fn resumed(&mut self, event_loop: &ActiveEventLoop) { - let mut window = GameWindow::new( - event_loop, - self.client.clone(), - self.input_mapper.clone(), - self.proxy.clone(), - ); - window.init(); - self.windows.insert(window.id(), Box::new(window)); + let app = GameWindow::new(self.client.clone(), self.proxy.clone(), SimId::Player1); + let wrapper = Viewport::new(event_loop, Box::new(app)); + self.focused = Some(wrapper.id()); + self.viewports.insert(wrapper.id(), wrapper); } fn window_event( &mut self, event_loop: &ActiveEventLoop, - window_id: WindowId, + window_id: winit::window::WindowId, event: WindowEvent, ) { - 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 { + let Some(viewport) = self + .viewports + .values_mut() + .find(|v| v.window.id() == window_id) + else { return; }; - window.handle_event(event_loop, &Event::WindowEvent { window_id, 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); + let viewport_id = viewport.id(); + match &event { + WindowEvent::KeyboardInput { event, .. } => { + self.controllers.handle_key_event(event); + viewport.app.handle_key_event(event); } - UserEvent::CloseWindow(window_id) => { - self.windows.remove(&window_id); + WindowEvent::Focused(new_focused) => { + self.focused = new_focused.then_some(viewport_id); } + _ => {} + } + let mut queue_redraw = false; + let mut inactive_viewports = HashSet::new(); + match viewport.on_window_event(event) { + Some(Action::Redraw) => { + for viewport in self.viewports.values_mut() { + match viewport.redraw(event_loop) { + Some(Action::Redraw) => { + queue_redraw = true; + } + Some(Action::Close) => { + inactive_viewports.insert(viewport.id()); + } + None => {} + } + } + } + Some(Action::Close) => { + inactive_viewports.insert(viewport_id); + } + None => {} + } + self.viewports + .retain(|k, _| !inactive_viewports.contains(k)); + match self.viewports.get(&ViewportId::ROOT) { + Some(viewport) => { + if queue_redraw { + viewport.window.request_redraw(); + } + } + None => event_loop.exit(), } } fn device_event( &mut self, - event_loop: &ActiveEventLoop, - device_id: winit::event::DeviceId, + _event_loop: &ActiveEventLoop, + _device_id: winit::event::DeviceId, event: winit::event::DeviceEvent, ) { - for window in self.windows.values_mut() { - window.handle_event( - event_loop, - &Event::DeviceEvent { - device_id, - event: event.clone(), - }, - ); + if let winit::event::DeviceEvent::MouseMotion { delta } = event { + let Some(viewport) = self + .focused + .as_ref() + .and_then(|id| self.viewports.get_mut(id)) + else { + return; + }; + viewport.state.on_mouse_motion(delta); } } - fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { - for window in self.windows.values_mut() { - window.handle_event(event_loop, &Event::AboutToWait); + fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) { + match event { + UserEvent::GamepadEvent(event) => { + self.controllers.handle_gamepad_event(&event); + let Some(viewport) = self + .focused + .as_ref() + .and_then(|id| self.viewports.get_mut(id)) + else { + return; + }; + viewport.app.handle_gamepad_event(&event); + } + UserEvent::OpenInput => { + let input = InputWindow::new(self.mappings.clone()); + self.open(event_loop, Box::new(input)); + } + UserEvent::OpenPlayer2 => { + let p2 = GameWindow::new(self.client.clone(), self.proxy.clone(), SimId::Player2); + self.open(event_loop, Box::new(p2)); + } + } + } + + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + if let Some(viewport) = self.viewports.get(&ViewportId::ROOT) { + viewport.window.request_redraw(); } } } -pub trait AppWindow { - fn id(&self) -> WindowId; - fn init(&mut self); - fn handle_event(&mut self, event_loop: &ActiveEventLoop, event: &Event); +struct Viewport { + painter: egui_wgpu::winit::Painter, + ctx: Context, + info: ViewportInfo, + commands: Vec, + builder: ViewportBuilder, + window: Arc, + state: egui_winit::State, + app: Box, +} +impl Viewport { + pub fn new(event_loop: &ActiveEventLoop, mut app: Box) -> Self { + let mut painter = egui_wgpu::winit::Painter::new( + egui_wgpu::WgpuConfiguration::default(), + 1, + None, + false, + true, + ); + + let ctx = Context::default(); + let mut fonts = FontDefinitions::empty(); + fonts.font_data.insert( + "Selawik".into(), + FontData::from_static(include_bytes!("../assets/selawik.ttf")), + ); + fonts + .families + .get_mut(&FontFamily::Proportional) + .unwrap() + .insert(0, "Selawik".into()); + ctx.set_fonts(fonts); + ctx.style_mut(|s| { + s.wrap_mode = Some(TextWrapMode::Extend); + s.visuals.menu_rounding = Default::default(); + }); + + let mut info = ViewportInfo::default(); + let builder = app.initial_viewport(); + let (window, state) = create_window_and_state(&ctx, event_loop, &builder, &mut painter); + egui_winit::update_viewport_info(&mut info, &ctx, &window, true); + + app.on_init(painter.render_state().as_ref().unwrap()); + Self { + painter, + ctx, + info, + commands: vec![], + builder, + window, + state, + app, + } + } + + pub fn id(&self) -> ViewportId { + self.app.viewport_id() + } + + pub fn on_window_event(&mut self, event: WindowEvent) -> Option { + let response = self.state.on_window_event(&self.window, &event); + egui_winit::update_viewport_info( + &mut self.info, + self.state.egui_ctx(), + &self.window, + false, + ); + + match event { + WindowEvent::RedrawRequested => Some(Action::Redraw), + WindowEvent::CloseRequested => Some(Action::Close), + WindowEvent::Resized(size) => { + let (Some(width), Some(height)) = + (NonZero::new(size.width), NonZero::new(size.height)) + else { + return None; + }; + self.painter + .on_window_resized(ViewportId::ROOT, width, height); + None + } + _ if response.repaint => Some(Action::Redraw), + _ => None, + } + } + + fn redraw(&mut self, event_loop: &ActiveEventLoop) -> Option { + let mut input = self.state.take_egui_input(&self.window); + input.viewports = std::iter::once((ViewportId::ROOT, self.info.clone())).collect(); + let mut output = self.ctx.run(input, |ctx| { + self.app.show(ctx); + }); + let clipped_primitives = self.ctx.tessellate(output.shapes, output.pixels_per_point); + self.painter.paint_and_update_textures( + ViewportId::ROOT, + output.pixels_per_point, + [0.0, 0.0, 0.0, 0.0], + &clipped_primitives, + &output.textures_delta, + false, + ); + + self.state + .handle_platform_output(&self.window, output.platform_output); + + let Some(mut viewport_output) = output.viewport_output.remove(&ViewportId::ROOT) else { + return Some(Action::Close); + }; + + let (mut deferred_commands, recreate) = self.builder.patch(viewport_output.builder); + if recreate { + let (window, state) = + create_window_and_state(&self.ctx, event_loop, &self.builder, &mut self.painter); + egui_winit::update_viewport_info(&mut self.info, &self.ctx, &window, true); + self.window = window; + self.state = state; + } + self.commands.append(&mut deferred_commands); + self.commands.append(&mut viewport_output.commands); + egui_winit::process_viewport_commands( + &self.ctx, + &mut self.info, + std::mem::take(&mut self.commands), + &self.window, + &mut HashSet::default(), + ); + + if self.info.close_requested() { + Some(Action::Close) + } else { + Some(Action::Redraw) + } + } } +impl Drop for Viewport { + fn drop(&mut self) { + self.app.on_destroy(); + } +} + +#[derive(Debug)] pub enum UserEvent { - OpenWindow(Box), - CloseWindow(WindowId), + GamepadEvent(gilrs::Event), + OpenInput, + OpenPlayer2, } -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(), +pub enum Action { + Redraw, + Close, +} + +fn create_window_and_state( + ctx: &Context, + event_loop: &ActiveEventLoop, + builder: &ViewportBuilder, + painter: &mut egui_wgpu::winit::Painter, +) -> (Arc, egui_winit::State) { + pollster::block_on(painter.set_window(ViewportId::ROOT, None)).unwrap(); + let window = Arc::new(egui_winit::create_window(ctx, event_loop, builder).unwrap()); + pollster::block_on(painter.set_window(ViewportId::ROOT, Some(window.clone()))).unwrap(); + let state = egui_winit::State::new( + ctx.clone(), + ViewportId::ROOT, + event_loop, + Some(window.scale_factor() as f32), + event_loop.system_theme(), + painter.max_texture_side(), + ); + (window, state) +} + +fn process_gamepad_input(mappings: MappingProvider, proxy: EventLoopProxy) { + let Ok(mut gilrs) = Gilrs::new() else { + eprintln!("could not connect gamepad listener"); + return; + }; + while let Some(event) = gilrs.next_event_blocking(None) { + if event.event == EventType::Connected { + let Some(gamepad) = gilrs.connected_gamepad(event.id) else { + continue; + }; + mappings.handle_gamepad_connect(&gamepad); + } + if event.event == EventType::Disconnected { + mappings.handle_gamepad_disconnect(event.id); + } + if proxy.send_event(UserEvent::GamepadEvent(event)).is_err() { + // main thread has closed! we done + return; } } } diff --git a/src/app/common.rs b/src/app/common.rs deleted file mode 100644 index 396acb2..0000000 --- a/src/app/common.rs +++ /dev/null @@ -1,264 +0,0 @@ -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, - pub minimized: bool, -} - -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, - minimized: false, - } - } - - 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) { - 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; - } - } -} - -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>; - 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.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 deleted file mode 100644 index efbeae0..0000000 --- a/src/app/game.rs +++ /dev/null @@ -1,384 +0,0 @@ -use std::{ - sync::{Arc, RwLock}, - time::Instant, -}; -use wgpu::util::DeviceExt as _; -use winit::{ - dpi::LogicalSize, - event::{Event, WindowEvent}, - event_loop::{ActiveEventLoop, EventLoopProxy}, - window::WindowId, -}; - -use crate::{ - emulator::{EmulatorClient, EmulatorCommand}, - input::InputMapper, - 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, - input_mapper: Arc>, - proxy: EventLoopProxy, - paused_due_to_minimize: bool, -} - -impl GameWindow { - pub fn new( - event_loop: &ActiveEventLoop, - client: EmulatorClient, - input_mapper: Arc>, - 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, - }); - - Self { - window, - imgui: None, - pipeline: render_pipeline, - bind_group, - client, - input_mapper, - proxy, - paused_due_to_minimize: false, - } - } - - 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 mut new_size = None; - - 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) => { - if !self.window.minimized { - 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); - new_size = Some(size); - } - } - } - }); - ui.menu("Input", || { - 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(); - } - }); - }); - - 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); - - 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(); - } -} - -impl AppWindow for GameWindow { - fn id(&self) -> WindowId { - self.window.window.id() - } - - 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) { - match event { - Event::WindowEvent { event, .. } => match event { - 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), - _ => (), - }, - 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 deleted file mode 100644 index d5ffe7e..0000000 --- a/src/app/input.rs +++ /dev/null @@ -1,223 +0,0 @@ -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, 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, - input_mapper: Arc>, - proxy: EventLoopProxy, - ) -> Self { - let window = WindowStateBuilder::new(event_loop) - .with_title("Bind Inputs") - .with_inner_size(LogicalSize::new(600, 400)) - .build(); - Self { - window, - imgui: None, - input_mapper, - now_binding: 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) => { - if !self.window.minimized { - 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 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(); - - 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); - } - } - - 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 - .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(); - } - - 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 { - fn id(&self) -> winit::window::WindowId { - self.window.window.id() - } - - fn init(&mut self) { - self.imgui = Some(ImguiState::new(&self.window)); - self.window.window.request_redraw(); - } - - 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::KeyboardInput { event, .. } => self.try_bind_key(event), - 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/audio.rs b/src/audio.rs index 2486830..2d97949 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use anyhow::{bail, Result}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use itertools::Itertools; @@ -95,7 +97,7 @@ impl Audio { } while self.sample_sink.slots() < self.sampler.output_frames_max() * 2 { - std::hint::spin_loop(); + std::thread::sleep(Duration::from_micros(500)); } } } diff --git a/src/controller.rs b/src/controller.rs index 70861ab..fe2ec14 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -1,50 +1,128 @@ use std::sync::{Arc, RwLock}; -use winit::event::{ElementState, KeyEvent}; +use gilrs::{ev::Code, Event as GamepadEvent, EventType, GamepadId}; +use winit::{ + event::{ElementState, KeyEvent}, + keyboard::PhysicalKey, +}; -use crate::{input::InputMapper, shrooms_vb_core::VBKey}; +use crate::{ + emulator::{EmulatorClient, EmulatorCommand, SimId, VBKey}, + input::{InputMapping, MappingProvider}, +}; -pub struct ControllerState { - input_mapper: Arc>, - pressed: VBKey, +pub struct Controller { + pub sim_id: SimId, + state: VBKey, + mapping: Arc>, } -impl ControllerState { - pub fn new(input_mapper: Arc>) -> Self { +impl Controller { + pub fn new(sim_id: SimId, mappings: &MappingProvider) -> Self { Self { - input_mapper, - pressed: VBKey::SGN, + sim_id, + state: VBKey::SGN, + mapping: mappings.for_sim(sim_id).clone(), } } - pub fn pressed(&self) -> VBKey { - self.pressed - } - - pub fn key_event(&mut self, event: &KeyEvent) -> bool { - let Some(input) = self.key_event_to_input(event) else { - return false; - }; + pub fn key_event(&mut self, event: &KeyEvent) -> Option { + let keys = self.map_keys(&event.physical_key)?; 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 - } + ElementState::Pressed => self.update_state(keys, VBKey::empty()), + ElementState::Released => self.update_state(VBKey::empty(), keys), } } - fn key_event_to_input(&self, event: &KeyEvent) -> Option { - let mapper = self.input_mapper.read().unwrap(); - mapper.key_event(event) + pub fn gamepad_event(&mut self, event: &GamepadEvent) -> Option { + let (pressed, released) = match event.event { + EventType::ButtonPressed(_, code) => { + let mappings = self.map_button(&event.id, &code)?; + (mappings, VBKey::empty()) + } + EventType::ButtonReleased(_, code) => { + let mappings = self.map_button(&event.id, &code)?; + (VBKey::empty(), mappings) + } + EventType::AxisChanged(_, value, code) => { + let (neg, pos) = self.map_axis(&event.id, &code)?; + let mut pressed = VBKey::empty(); + let mut released = VBKey::empty(); + if value < -0.75 { + pressed = pressed.union(neg); + } + if value > 0.75 { + pressed = pressed.union(pos); + } + if value > -0.65 { + released = released.union(neg); + } + if value < 0.65 { + released = released.union(pos); + } + (pressed, released) + } + _ => { + return None; + } + }; + self.update_state(pressed, released) + } + + fn update_state(&mut self, pressed: VBKey, released: VBKey) -> Option { + let old_state = self.state; + self.state = self.state.union(pressed).difference(released); + if self.state != old_state { + Some(self.state) + } else { + None + } + } + + fn map_keys(&self, key: &PhysicalKey) -> Option { + self.mapping.read().unwrap().map_keyboard(key) + } + + fn map_button(&self, id: &GamepadId, code: &Code) -> Option { + self.mapping.read().unwrap().map_button(id, code) + } + + fn map_axis(&self, id: &GamepadId, code: &Code) -> Option<(VBKey, VBKey)> { + self.mapping.read().unwrap().map_axis(id, code) + } +} + +pub struct ControllerManager { + client: EmulatorClient, + controllers: [Controller; 2], +} + +impl ControllerManager { + pub fn new(client: EmulatorClient, mappings: &MappingProvider) -> Self { + Self { + client, + controllers: [ + Controller::new(SimId::Player1, mappings), + Controller::new(SimId::Player2, mappings), + ], + } + } + + pub fn handle_key_event(&mut self, event: &KeyEvent) { + for controller in &mut self.controllers { + if let Some(pressed) = controller.key_event(event) { + self.client + .send_command(EmulatorCommand::SetKeys(controller.sim_id, pressed)); + } + } + } + + pub fn handle_gamepad_event(&mut self, event: &GamepadEvent) { + for controller in &mut self.controllers { + if let Some(pressed) = controller.gamepad_event(event) { + self.client + .send_command(EmulatorCommand::SetKeys(controller.sim_id, pressed)); + } + } } } diff --git a/src/emulator.rs b/src/emulator.rs index 38fdab6..fd04520 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -1,8 +1,9 @@ use std::{ + collections::HashMap, fs, path::{Path, PathBuf}, sync::{ - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicUsize, Ordering}, mpsc::{self, RecvError, TryRecvError}, Arc, }, @@ -10,17 +11,37 @@ use std::{ use anyhow::Result; -use crate::{ - audio::Audio, - renderer::GameRenderer, - shrooms_vb_core::{CoreVB, VBKey}, -}; +use crate::{audio::Audio, graphics::TextureSink}; +pub use shrooms_vb_core::VBKey; +use shrooms_vb_core::{Sim, EXPECTED_FRAME_SIZE}; + +mod shrooms_vb_core; pub struct EmulatorBuilder { rom: Option, commands: mpsc::Receiver, - running: Arc, - has_game: Arc, + sim_count: Arc, + running: Arc<[AtomicBool; 2]>, + has_game: Arc<[AtomicBool; 2]>, + audio_on: Arc<[AtomicBool; 2]>, + linked: Arc, +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] +pub enum SimId { + Player1, + Player2, +} +impl SimId { + pub const fn values() -> [Self; 2] { + [Self::Player1, Self::Player2] + } + pub const fn to_index(self) -> usize { + match self { + Self::Player1 => 0, + Self::Player2 => 1, + } + } } impl EmulatorBuilder { @@ -29,13 +50,19 @@ impl EmulatorBuilder { let builder = Self { rom: None, commands, - running: Arc::new(AtomicBool::new(false)), - has_game: Arc::new(AtomicBool::new(false)), + sim_count: Arc::new(AtomicUsize::new(0)), + running: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]), + has_game: Arc::new([AtomicBool::new(false), AtomicBool::new(false)]), + audio_on: Arc::new([AtomicBool::new(true), AtomicBool::new(true)]), + linked: Arc::new(AtomicBool::new(false)), }; let client = EmulatorClient { queue, + sim_count: builder.sim_count.clone(), running: builder.running.clone(), has_game: builder.has_game.clone(), + audio_on: builder.audio_on.clone(), + linked: builder.linked.clone(), }; (builder, client) } @@ -48,69 +75,170 @@ impl EmulatorBuilder { } pub fn build(self) -> Result { - let mut emulator = Emulator::new(self.commands, self.running, self.has_game)?; + let mut emulator = Emulator::new( + self.commands, + self.sim_count, + self.running, + self.has_game, + self.audio_on, + self.linked, + )?; if let Some(path) = self.rom { - emulator.load_rom(&path)?; + emulator.load_rom(SimId::Player1, &path)?; } Ok(emulator) } } pub struct Emulator { - sim: CoreVB, + sims: Vec, audio: Audio, commands: mpsc::Receiver, - renderer: Option, - running: Arc, - has_game: Arc, + sim_count: Arc, + running: Arc<[AtomicBool; 2]>, + has_game: Arc<[AtomicBool; 2]>, + audio_on: Arc<[AtomicBool; 2]>, + linked: Arc, + renderers: HashMap, } impl Emulator { fn new( commands: mpsc::Receiver, - running: Arc, - has_game: Arc, + sim_count: Arc, + running: Arc<[AtomicBool; 2]>, + has_game: Arc<[AtomicBool; 2]>, + audio_on: Arc<[AtomicBool; 2]>, + linked: Arc, ) -> Result { Ok(Self { - sim: CoreVB::new(), + sims: vec![], audio: Audio::init()?, commands, - renderer: None, + sim_count, running, has_game, + audio_on, + linked, + renderers: HashMap::new(), }) } - pub fn load_rom(&mut self, path: &Path) -> Result<()> { + pub fn load_rom(&mut self, sim_id: SimId, path: &Path) -> Result<()> { let bytes = fs::read(path)?; - self.sim.reset(); - self.sim.load_rom(bytes)?; - self.has_game.store(true, Ordering::Release); - self.running.store(true, Ordering::Release); + self.reset_sim(sim_id, Some(bytes))?; Ok(()) } + pub fn start_second_sim(&mut self, rom: Option) -> Result<()> { + let bytes = if let Some(path) = rom { + Some(fs::read(path)?) + } else { + self.sims.first().and_then(|s| s.clone_rom()) + }; + self.reset_sim(SimId::Player2, bytes)?; + self.link_sims(); + Ok(()) + } + + fn reset_sim(&mut self, sim_id: SimId, new_rom: Option>) -> Result<()> { + let index = sim_id.to_index(); + while self.sims.len() <= index { + self.sims.push(Sim::new()); + } + self.sim_count.store(self.sims.len(), Ordering::Relaxed); + let sim = &mut self.sims[index]; + sim.reset(); + if let Some(bytes) = new_rom { + sim.load_rom(bytes)?; + self.has_game[index].store(true, Ordering::Release); + } + if self.has_game[index].load(Ordering::Acquire) { + self.running[index].store(true, Ordering::Release); + } + Ok(()) + } + + fn link_sims(&mut self) { + let (first, second) = self.sims.split_at_mut(1); + let Some(first) = first.first_mut() else { + return; + }; + let Some(second) = second.first_mut() else { + return; + }; + first.link(second); + self.linked.store(true, Ordering::Release); + } + + fn unlink_sims(&mut self) { + let Some(first) = self.sims.first_mut() else { + return; + }; + first.unlink(); + self.linked.store(false, Ordering::Release); + } + + pub fn stop_second_sim(&mut self) { + self.renderers.remove(&SimId::Player2); + self.sims.truncate(1); + self.sim_count.store(self.sims.len(), Ordering::Relaxed); + self.running[SimId::Player2.to_index()].store(false, Ordering::Release); + self.has_game[SimId::Player2.to_index()].store(false, Ordering::Release); + self.linked.store(false, Ordering::Release); + } + pub fn run(&mut self) { 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(); + let p1_running = self.running[SimId::Player1.to_index()].load(Ordering::Acquire); + let p2_running = self.running[SimId::Player2.to_index()].load(Ordering::Acquire); + let mut idle = p1_running || p2_running; + if p1_running && p2_running { + Sim::emulate_many(&mut self.sims); + } else if p1_running { + self.sims[SimId::Player1.to_index()].emulate(); + } else if p2_running { + self.sims[SimId::Player2.to_index()].emulate(); } - if let Some(renderer) = &mut self.renderer { - if self.sim.read_pixels(&mut eye_contents) { + + for sim_id in SimId::values() { + let Some(renderer) = self.renderers.get_mut(&sim_id) else { + continue; + }; + let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { + continue; + }; + if sim.read_pixels(&mut eye_contents) { idle = false; - renderer.render(&eye_contents); + if renderer.queue_render(&eye_contents).is_err() { + self.renderers.remove(&sim_id); + } } } - self.sim.read_samples(&mut audio_samples); - if !audio_samples.is_empty() { - idle = false; - self.audio.update(&audio_samples); - audio_samples.clear(); + let p1_audio = + p1_running && self.audio_on[SimId::Player1.to_index()].load(Ordering::Acquire); + let p2_audio = + p2_running && self.audio_on[SimId::Player2.to_index()].load(Ordering::Acquire); + let weight = if p1_audio && p2_audio { 0.5 } else { 1.0 }; + if p1_audio { + if let Some(sim) = self.sims.get_mut(SimId::Player1.to_index()) { + sim.read_samples(&mut audio_samples, weight); + } } + if p2_audio { + if let Some(sim) = self.sims.get_mut(SimId::Player2.to_index()) { + sim.read_samples(&mut audio_samples, weight); + } + } + if audio_samples.is_empty() { + audio_samples.resize(EXPECTED_FRAME_SIZE, 0.0); + } else { + 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. @@ -137,28 +265,54 @@ impl Emulator { fn handle_command(&mut self, command: EmulatorCommand) { match command { - EmulatorCommand::SetRenderer(renderer) => { - self.renderer = Some(renderer); + EmulatorCommand::SetRenderer(sim_id, renderer) => { + self.renderers.insert(sim_id, renderer); } - EmulatorCommand::LoadGame(path) => { - if let Err(error) = self.load_rom(&path) { + EmulatorCommand::LoadGame(sim_id, path) => { + if let Err(error) = self.load_rom(sim_id, &path) { eprintln!("error loading rom: {}", error); } } - EmulatorCommand::Pause => { - self.running.store(false, Ordering::Release); - } - EmulatorCommand::Resume => { - if self.has_game.load(Ordering::Acquire) { - self.running.store(true, Ordering::Relaxed); + EmulatorCommand::StartSecondSim(path) => { + if let Err(error) = self.start_second_sim(path) { + eprintln!("error starting second sim: {}", error); } } - EmulatorCommand::Reset => { - self.sim.reset(); - self.running.store(true, Ordering::Release); + EmulatorCommand::StopSecondSim => { + self.stop_second_sim(); } - EmulatorCommand::SetKeys(keys) => { - self.sim.set_keys(keys); + EmulatorCommand::Pause => { + for sim in SimId::values() { + self.running[sim.to_index()].store(false, Ordering::Release); + } + } + EmulatorCommand::Resume => { + for sim_id in SimId::values() { + let index = sim_id.to_index(); + if self.has_game[index].load(Ordering::Acquire) { + self.running[index].store(true, Ordering::Relaxed); + } + } + } + EmulatorCommand::SetAudioEnabled(p1, p2) => { + self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release); + self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release); + } + EmulatorCommand::Link => { + self.link_sims(); + } + EmulatorCommand::Unlink => { + self.unlink_sims(); + } + EmulatorCommand::Reset(sim_id) => { + if let Err(error) = self.reset_sim(sim_id, None) { + eprintln!("error resetting sim: {}", error); + } + } + EmulatorCommand::SetKeys(sim_id, keys) => { + if let Some(sim) = self.sims.get_mut(sim_id.to_index()) { + sim.set_keys(keys); + } } } } @@ -166,27 +320,44 @@ impl Emulator { #[derive(Debug)] pub enum EmulatorCommand { - SetRenderer(GameRenderer), - LoadGame(PathBuf), + SetRenderer(SimId, TextureSink), + LoadGame(SimId, PathBuf), + StartSecondSim(Option), + StopSecondSim, Pause, Resume, - Reset, - SetKeys(VBKey), + SetAudioEnabled(bool, bool), + Link, + Unlink, + Reset(SimId), + SetKeys(SimId, VBKey), } #[derive(Clone)] pub struct EmulatorClient { queue: mpsc::Sender, - running: Arc, - has_game: Arc, + sim_count: Arc, + running: Arc<[AtomicBool; 2]>, + has_game: Arc<[AtomicBool; 2]>, + audio_on: Arc<[AtomicBool; 2]>, + linked: Arc, } impl EmulatorClient { - pub fn is_running(&self) -> bool { - self.running.load(Ordering::Acquire) + pub fn has_player_2(&self) -> bool { + self.sim_count.load(Ordering::Acquire) == 2 } - pub fn has_game(&self) -> bool { - self.has_game.load(Ordering::Acquire) + pub fn is_running(&self, sim_id: SimId) -> bool { + self.running[sim_id.to_index()].load(Ordering::Acquire) + } + pub fn has_game(&self, sim_id: SimId) -> bool { + self.has_game[sim_id.to_index()].load(Ordering::Acquire) + } + pub fn are_sims_linked(&self) -> bool { + self.linked.load(Ordering::Acquire) + } + pub fn is_audio_enabled(&self, sim_id: SimId) -> bool { + self.audio_on[sim_id.to_index()].load(Ordering::Acquire) } pub fn send_command(&self, command: EmulatorCommand) { if let Err(err) = self.queue.send(command) { diff --git a/src/shrooms_vb_core.rs b/src/emulator/shrooms_vb_core.rs similarity index 78% rename from src/shrooms_vb_core.rs rename to src/emulator/shrooms_vb_core.rs index 324689d..fe2fa27 100644 --- a/src/shrooms_vb_core.rs +++ b/src/emulator/shrooms_vb_core.rs @@ -25,6 +25,12 @@ enum VBDataType { F32 = 5, } +#[repr(i32)] +#[derive(FromPrimitive, ToPrimitive)] +enum VBOption { + PseudoHalt = 0, +} + bitflags! { #[repr(transparent)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -54,6 +60,8 @@ type OnFrame = extern "C" fn(sim: *mut VB) -> c_int; extern "C" { #[link_name = "vbEmulate"] fn vb_emulate(sim: *mut VB, cycles: *mut u32) -> c_int; + #[link_name = "vbEmulateEx"] + fn vb_emulate_ex(sims: *mut *mut VB, count: c_uint, 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"] @@ -81,10 +89,14 @@ extern "C" { 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 = "vbSetKeys"] + fn vb_set_keys(sim: *mut VB, keys: u16) -> u16; + #[link_name = "vbSetOption"] + fn vb_set_option(sim: *mut VB, key: VBOption, value: c_int); + #[link_name = "vbSetPeer"] + fn vb_set_peer(sim: *mut VB, peer: *mut VB); #[link_name = "vbSetSamples"] fn vb_set_samples( sim: *mut VB, @@ -108,19 +120,21 @@ extern "C" fn on_frame(sim: *mut VB) -> i32 { const AUDIO_CAPACITY_SAMPLES: usize = 834 * 4; const AUDIO_CAPACITY_FLOATS: usize = AUDIO_CAPACITY_SAMPLES * 2; +pub const EXPECTED_FRAME_SIZE: usize = 834 * 2; struct VBState { frame_seen: bool, } -pub struct CoreVB { +#[repr(transparent)] +pub struct Sim { sim: *mut VB, } // SAFETY: the memory pointed to by sim is valid -unsafe impl Send for CoreVB {} +unsafe impl Send for Sim {} -impl CoreVB { +impl Sim { pub fn new() -> Self { // init the VB instance itself let size = unsafe { vb_size_of() }; @@ -128,6 +142,7 @@ impl CoreVB { let memory = vec![0u64; size.div_ceil(4)]; let sim: *mut VB = Box::into_raw(memory.into_boxed_slice()).cast(); unsafe { vb_init(sim) }; + unsafe { vb_set_option(sim, VBOption::PseudoHalt, 1) }; unsafe { vb_reset(sim) }; // set up userdata @@ -140,7 +155,7 @@ impl CoreVB { 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 } + Sim { sim } } pub fn reset(&mut self) { @@ -162,6 +177,17 @@ impl CoreVB { } } + pub fn clone_rom(&self) -> Option> { + let mut size = 0; + let rom = unsafe { vb_get_cart_rom(self.sim, &mut size) }; + if rom.is_null() { + return None; + } + // SAFETY: rom definitely points to a valid array of `size` bytes + let slice: &[u8] = unsafe { slice::from_raw_parts(rom.cast(), size as usize) }; + Some(slice.to_vec()) + } + fn unload_rom(&mut self) -> Option> { let mut size = 0; let rom = unsafe { vb_get_cart_rom(self.sim, &mut size) }; @@ -173,11 +199,26 @@ impl CoreVB { Some(vec) } - pub fn emulate_frame(&mut self) { + pub fn link(&mut self, peer: &mut Sim) { + unsafe { vb_set_peer(self.sim, peer.sim) }; + } + + pub fn unlink(&mut self) { + unsafe { vb_set_peer(self.sim, ptr::null_mut()) }; + } + + pub fn emulate(&mut self) { let mut cycles = 20_000_000; unsafe { vb_emulate(self.sim, &mut cycles) }; } + pub fn emulate_many(sims: &mut [Sim]) { + let mut cycles = 20_000_000; + let count = sims.len() as c_uint; + let sims = sims.as_mut_ptr().cast(); + unsafe { vb_emulate_ex(sims, count, &mut cycles) }; + } + 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. @@ -203,14 +244,17 @@ impl CoreVB { true } - pub fn read_samples(&mut self, samples: &mut Vec) { + pub fn read_samples(&mut self, samples: &mut Vec, weight: f32) { 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); + let read_samples: &[f32] = + unsafe { slice::from_raw_parts(ptr.cast(), position as usize * 2) }; + samples.resize(read_samples.len(), 0.0); + for (index, sample) in read_samples.iter().enumerate() { + samples[index] += sample * weight; + } unsafe { vb_set_samples( @@ -227,7 +271,7 @@ impl CoreVB { } } -impl Drop for CoreVB { +impl Drop for Sim { fn drop(&mut self) { let ptr = unsafe { vb_get_samples(self.sim, ptr::null_mut(), ptr::null_mut(), ptr::null_mut()) }; @@ -243,6 +287,9 @@ impl Drop for CoreVB { // SAFETY: we made this pointer ourselves, we can for sure free it unsafe { drop(Box::from_raw(ptr)) }; + // If we're linked to another sim, unlink from them. + unsafe { vb_set_peer(self.sim, ptr::null_mut()) }; + 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) }; diff --git a/src/graphics.rs b/src/graphics.rs new file mode 100644 index 0000000..4c0f75f --- /dev/null +++ b/src/graphics.rs @@ -0,0 +1,143 @@ +use std::{ + sync::{ + atomic::{AtomicU64, Ordering}, + mpsc, Arc, Mutex, MutexGuard, + }, + thread, +}; + +use anyhow::{bail, Result}; +use itertools::Itertools as _; +use wgpu::{ + Device, Extent3d, ImageCopyTexture, ImageDataLayout, Origin3d, Queue, Texture, + TextureDescriptor, TextureFormat, TextureUsages, TextureView, TextureViewDescriptor, +}; + +#[derive(Debug)] +pub struct TextureSink { + buffers: Arc, + sink: mpsc::Sender, +} + +impl TextureSink { + pub fn new(device: &Device, queue: Arc) -> (Self, TextureView) { + let texture = Self::create_texture(device); + let view = texture.create_view(&TextureViewDescriptor::default()); + let buffers = Arc::new(BufferPool::new()); + let (sink, source) = mpsc::channel(); + let bufs = buffers.clone(); + thread::spawn(move || { + let mut local_buf = vec![0; 384 * 224 * 2]; + while let Ok(id) = source.recv() { + { + let Some(bytes) = bufs.read(id) else { + continue; + }; + local_buf.copy_from_slice(bytes.as_slice()); + } + Self::write_texture(&queue, &texture, local_buf.as_slice()); + } + }); + let sink = Self { buffers, sink }; + (sink, view) + } + + pub fn queue_render(&mut self, bytes: &[u8]) -> Result<()> { + let id = { + let (mut buf, id) = self.buffers.write()?; + buf.copy_from_slice(bytes); + id + }; + self.sink.send(id)?; + Ok(()) + } + + fn create_texture(device: &Device) -> Texture { + let desc = TextureDescriptor { + label: Some("eyes"), + size: Extent3d { + width: 384, + height: 224, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: TextureFormat::Rg8Unorm, + usage: TextureUsages::COPY_SRC + | TextureUsages::COPY_DST + | TextureUsages::TEXTURE_BINDING, + view_formats: &[TextureFormat::Rg8Unorm], + }; + device.create_texture(&desc) + } + + fn write_texture(queue: &Queue, texture: &Texture, bytes: &[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 * 2), + rows_per_image: Some(224), + }; + queue.write_texture(texture, bytes, data_layout, size); + } +} + +#[derive(Debug)] +struct BufferPool { + buffers: [Buffer; 3], +} +impl BufferPool { + fn new() -> Self { + Self { + buffers: std::array::from_fn(|i| Buffer::new(i as u64)), + } + } + + fn read(&self, id: u64) -> Option>> { + let buf = self + .buffers + .iter() + .find(|buf| buf.id.load(Ordering::Acquire) == id)?; + buf.data.lock().ok() + } + + fn write(&self) -> Result<(MutexGuard<'_, Vec>, u64)> { + let (min, max) = self + .buffers + .iter() + .minmax_by_key(|buf| buf.id.load(Ordering::Acquire)) + .into_option() + .unwrap(); + let Ok(lock) = min.data.lock() else { + bail!("lock was poisoned") + }; + let id = max.id.load(Ordering::Acquire) + 1; + min.id.store(id, Ordering::Release); + Ok((lock, id)) + } +} + +#[derive(Debug)] +struct Buffer { + data: Mutex>, + id: AtomicU64, +} +impl Buffer { + fn new(id: u64) -> Self { + Self { + data: Mutex::new(vec![0; 384 * 224 * 2]), + id: AtomicU64::new(id), + } + } +} diff --git a/src/input.rs b/src/input.rs index 5367d60..057d616 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,71 +1,344 @@ -use std::collections::HashMap; - -use winit::{ - event::KeyEvent, - keyboard::{Key, NamedKey}, - platform::modifier_supplement::KeyEventExtModifierSupplement, +use std::{ + collections::{hash_map::Entry, HashMap}, + sync::{Arc, RwLock}, }; -use crate::shrooms_vb_core::VBKey; +use gilrs::{ev::Code, Axis, Button, Gamepad, GamepadId}; +use winit::keyboard::{KeyCode, PhysicalKey}; -pub struct InputMapper { - vb_bindings: HashMap, - key_bindings: HashMap, +use crate::emulator::{SimId, VBKey}; + +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +struct DeviceId(u16, u16); + +#[derive(Clone)] +pub struct GamepadInfo { + pub id: GamepadId, + pub name: String, + device_id: DeviceId, + pub bound_to: Option, } -impl InputMapper { - pub fn new() -> Self { - let mut mapper = Self { - vb_bindings: HashMap::new(), - key_bindings: HashMap::new(), +pub trait Mappings { + fn mapping_names(&self) -> HashMap>; + fn clear_mappings(&mut self, key: VBKey); + fn clear_all_mappings(&mut self); + fn use_default_mappings(&mut self); +} + +pub struct GamepadMapping { + buttons: HashMap, + axes: HashMap, + default_buttons: HashMap, + default_axes: HashMap, +} + +impl GamepadMapping { + fn for_gamepad(gamepad: &Gamepad) -> Self { + let mut default_buttons = HashMap::new(); + let mut default_button = |btn: Button, key: VBKey| { + if let Some(code) = gamepad.button_code(btn) { + default_buttons.insert(code, key); + } }; - 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 + default_button(Button::South, VBKey::A); + default_button(Button::West, VBKey::B); + default_button(Button::RightTrigger, VBKey::RT); + default_button(Button::LeftTrigger, VBKey::LT); + default_button(Button::Start, VBKey::STA); + default_button(Button::Select, VBKey::SEL); + + let mut default_axes = HashMap::new(); + let mut default_axis = |axis: Axis, neg: VBKey, pos: VBKey| { + if let Some(code) = gamepad.axis_code(axis) { + default_axes.insert(code, (neg, pos)); + } + }; + default_axis(Axis::LeftStickX, VBKey::LL, VBKey::LR); + default_axis(Axis::LeftStickY, VBKey::LD, VBKey::LU); + default_axis(Axis::RightStickX, VBKey::RL, VBKey::RR); + default_axis(Axis::RightStickY, VBKey::RD, VBKey::RU); + default_axis(Axis::DPadX, VBKey::LL, VBKey::LR); + default_axis(Axis::DPadY, VBKey::LD, VBKey::LU); + + Self { + buttons: default_buttons.clone(), + axes: default_axes.clone(), + default_buttons, + default_axes, + } } - 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) - }) + pub fn add_button_mapping(&mut self, key: VBKey, code: Code) { + let entry = self.buttons.entry(code).or_insert(VBKey::empty()); + *entry = entry.union(key); + } + + pub fn add_axis_neg_mapping(&mut self, key: VBKey, code: Code) { + let entry = self + .axes + .entry(code) + .or_insert((VBKey::empty(), VBKey::empty())); + entry.0 = entry.0.union(key); + } + + pub fn add_axis_pos_mapping(&mut self, key: VBKey, code: Code) { + let entry = self + .axes + .entry(code) + .or_insert((VBKey::empty(), VBKey::empty())); + entry.1 = entry.1.union(key); + } +} + +impl Mappings for GamepadMapping { + fn mapping_names(&self) -> HashMap> { + let mut results: HashMap> = HashMap::new(); + for (axis, (left_keys, right_keys)) in &self.axes { + for key in left_keys.iter() { + results.entry(key).or_default().push(format!("-{axis}")); + } + for key in right_keys.iter() { + results.entry(key).or_default().push(format!("+{axis}")); + } + } + for (button, keys) in &self.buttons { + for key in keys.iter() { + results.entry(key).or_default().push(format!("{button}")); + } + } + results + } + + fn clear_mappings(&mut self, key: VBKey) { + self.axes.retain(|_, (left, right)| { + *left = left.difference(key); + *right = right.difference(key); + !(left.is_empty() && right.is_empty()) + }); + self.buttons.retain(|_, keys| { + *keys = keys.difference(key); + !keys.is_empty() + }); + } + + fn clear_all_mappings(&mut self) { + self.axes.clear(); + self.buttons.clear(); + } + + fn use_default_mappings(&mut self) { + self.axes = self.default_axes.clone(); + self.buttons = self.default_buttons.clone(); + } +} + +#[derive(Default)] +pub struct InputMapping { + keys: HashMap, + gamepads: HashMap>>, +} + +impl InputMapping { + pub fn map_keyboard(&self, key: &PhysicalKey) -> Option { + self.keys.get(key).copied() + } + + pub fn map_button(&self, id: &GamepadId, code: &Code) -> Option { + let mappings = self.gamepads.get(id)?.read().unwrap(); + mappings.buttons.get(code).copied() + } + + pub fn map_axis(&self, id: &GamepadId, code: &Code) -> Option<(VBKey, VBKey)> { + let mappings = self.gamepads.get(id)?.read().unwrap(); + mappings.axes.get(code).copied() + } + + pub fn add_keyboard_mapping(&mut self, key: VBKey, keyboard_key: PhysicalKey) { + let entry = self.keys.entry(keyboard_key).or_insert(VBKey::empty()); + *entry = entry.union(key); + } +} + +impl Mappings for InputMapping { + fn mapping_names(&self) -> HashMap> { + let mut results: HashMap> = HashMap::new(); + for (keyboard_key, keys) in &self.keys { + let name = match keyboard_key { + PhysicalKey::Code(code) => format!("{code:?}"), + k => format!("{:?}", k), + }; + for key in keys.iter() { + results.entry(key).or_default().push(name.clone()); + } + } + results + } + + fn clear_mappings(&mut self, key: VBKey) { + self.keys.retain(|_, keys| { + *keys = keys.difference(key); + !keys.is_empty() + }); + } + + fn clear_all_mappings(&mut self) { + self.keys.clear(); + } + + fn use_default_mappings(&mut self) { + self.keys.clear(); + let mut default_key = |code, key| { + self.keys.insert(PhysicalKey::Code(code), key); + }; + default_key(KeyCode::KeyA, VBKey::SEL); + default_key(KeyCode::KeyS, VBKey::STA); + default_key(KeyCode::KeyD, VBKey::B); + default_key(KeyCode::KeyF, VBKey::A); + default_key(KeyCode::KeyE, VBKey::LT); + default_key(KeyCode::KeyR, VBKey::RT); + default_key(KeyCode::KeyI, VBKey::RU); + default_key(KeyCode::KeyJ, VBKey::RL); + default_key(KeyCode::KeyK, VBKey::RD); + default_key(KeyCode::KeyL, VBKey::RR); + default_key(KeyCode::ArrowUp, VBKey::LU); + default_key(KeyCode::ArrowLeft, VBKey::LL); + default_key(KeyCode::ArrowDown, VBKey::LD); + default_key(KeyCode::ArrowRight, VBKey::LR); + } +} + +#[derive(Clone)] +pub struct MappingProvider { + device_mappings: Arc>>>>, + sim_mappings: HashMap>>, + gamepad_info: Arc>>, +} + +impl MappingProvider { + pub fn new() -> Self { + let mut mappings = HashMap::new(); + + let mut p1_mappings = InputMapping::default(); + p1_mappings.use_default_mappings(); + let p2_mappings = InputMapping::default(); + + mappings.insert(SimId::Player1, Arc::new(RwLock::new(p1_mappings))); + mappings.insert(SimId::Player2, Arc::new(RwLock::new(p2_mappings))); + Self { + device_mappings: Arc::new(RwLock::new(HashMap::new())), + gamepad_info: Arc::new(RwLock::new(HashMap::new())), + sim_mappings: mappings, + } + } + + pub fn for_sim(&self, sim_id: SimId) -> &Arc> { + self.sim_mappings.get(&sim_id).unwrap() + } + + pub fn for_gamepad(&self, gamepad_id: GamepadId) -> Option>> { + let lock = self.gamepad_info.read().unwrap(); + let device_id = lock.get(&gamepad_id)?.device_id; + drop(lock); + let lock = self.device_mappings.read().unwrap(); + lock.get(&device_id).cloned() + } + + pub fn handle_gamepad_connect(&self, gamepad: &Gamepad) { + let device_id = DeviceId( + gamepad.vendor_id().unwrap_or_default(), + gamepad.product_id().unwrap_or_default(), + ); + let mut lock = self.device_mappings.write().unwrap(); + let mappings = match lock.entry(device_id) { + Entry::Vacant(entry) => { + let mappings = GamepadMapping::for_gamepad(gamepad); + entry.insert(Arc::new(RwLock::new(mappings))) + } + Entry::Occupied(entry) => entry.into_mut(), + } + .clone(); + drop(lock); + let mut lock = self.gamepad_info.write().unwrap(); + let bound_to = SimId::values() + .into_iter() + .find(|sim_id| lock.values().all(|info| info.bound_to != Some(*sim_id))); + if let Entry::Vacant(entry) = lock.entry(gamepad.id()) { + let info = GamepadInfo { + id: *entry.key(), + name: gamepad.name().to_string(), + device_id, + bound_to, + }; + entry.insert(info); + } + drop(lock); + if let Some(sim_id) = bound_to { + self.for_sim(sim_id) + .write() + .unwrap() + .gamepads + .insert(gamepad.id(), mappings); + } + } + + pub fn handle_gamepad_disconnect(&self, gamepad_id: GamepadId) { + let mut lock = self.gamepad_info.write().unwrap(); + let Some(info) = lock.remove(&gamepad_id) else { + return; + }; + if let Some(sim_id) = info.bound_to { + self.for_sim(sim_id) + .write() + .unwrap() + .gamepads + .remove(&gamepad_id); + } + } + + pub fn assign_gamepad(&self, gamepad_id: GamepadId, sim_id: SimId) { + self.unassign_gamepad(gamepad_id); + let mut lock = self.gamepad_info.write().unwrap(); + let Some(info) = lock.get_mut(&gamepad_id) else { + return; + }; + info.bound_to = Some(sim_id); + let device_id = info.device_id; + drop(lock); + let Some(device_mappings) = self + .device_mappings + .write() + .unwrap() + .get(&device_id) + .cloned() + else { + return; + }; + self.for_sim(sim_id) + .write() + .unwrap() + .gamepads + .insert(gamepad_id, device_mappings); + } + + pub fn unassign_gamepad(&self, gamepad_id: GamepadId) { + let mut lock = self.gamepad_info.write().unwrap(); + let Some(info) = lock.get_mut(&gamepad_id) else { + return; + }; + if let Some(sim_id) = info.bound_to { + let mut sim_mapping = self.for_sim(sim_id).write().unwrap(); + sim_mapping.gamepads.remove(&gamepad_id); + } + info.bound_to = None; + } + + pub fn gamepad_info(&self) -> Vec { + self.gamepad_info + .read() + .unwrap() + .values() + .cloned() .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 b8de1cf..f747fca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::{path::PathBuf, process}; use anyhow::Result; -use app::App; +use app::Application; use clap::Parser; use emulator::EmulatorBuilder; use thread_priority::{ThreadBuilder, ThreadPriority}; @@ -11,16 +11,28 @@ mod app; mod audio; mod controller; mod emulator; +mod graphics; mod input; -mod renderer; -mod shrooms_vb_core; +mod window; #[derive(Parser)] struct Args { rom: Option, } +#[cfg(windows)] +fn set_process_priority_to_high() -> Result<()> { + use windows::Win32::{Foundation, System::Threading}; + let process = unsafe { Threading::GetCurrentProcess() }; + unsafe { Threading::SetPriorityClass(process, Threading::HIGH_PRIORITY_CLASS)? }; + unsafe { Foundation::CloseHandle(process)? }; + Ok(()) +} + fn main() -> Result<()> { + #[cfg(windows)] + set_process_priority_to_high()?; + let args = Args::parse(); let (mut builder, client) = EmulatorBuilder::new(); @@ -45,6 +57,6 @@ fn main() -> Result<()> { let event_loop = EventLoop::with_user_event().build().unwrap(); event_loop.set_control_flow(ControlFlow::Poll); let proxy = event_loop.create_proxy(); - event_loop.run_app(&mut App::new(client, proxy))?; + event_loop.run_app(&mut Application::new(client, proxy))?; Ok(()) } diff --git a/src/renderer.rs b/src/renderer.rs deleted file mode 100644 index a97e9e7..0000000 --- a/src/renderer.rs +++ /dev/null @@ -1,53 +0,0 @@ -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, -} - -impl GameRenderer { - pub fn render(&self, buffer: &[u8]) { - let texture = ImageCopyTexture { - texture: &self.eyes, - 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 * 2), - 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::Rg8Unorm, - usage: TextureUsages::COPY_SRC - | TextureUsages::COPY_DST - | TextureUsages::TEXTURE_BINDING, - view_formats: &[TextureFormat::Rg8Unorm], - }; - device.create_texture(&desc) - } -} diff --git a/src/window.rs b/src/window.rs new file mode 100644 index 0000000..fdc95d3 --- /dev/null +++ b/src/window.rs @@ -0,0 +1,24 @@ +use egui::{Context, ViewportBuilder, ViewportId}; +pub use game::GameWindow; +pub use input::InputWindow; +use winit::event::KeyEvent; + +mod game; +mod game_screen; +mod input; + +pub trait AppWindow { + fn viewport_id(&self) -> ViewportId; + fn initial_viewport(&self) -> ViewportBuilder; + fn show(&mut self, ctx: &Context); + fn on_init(&mut self, render_state: &egui_wgpu::RenderState) { + let _ = render_state; + } + fn on_destroy(&mut self) {} + fn handle_key_event(&mut self, event: &KeyEvent) { + let _ = event; + } + fn handle_gamepad_event(&mut self, event: &gilrs::Event) { + let _ = event; + } +} diff --git a/src/window/game.rs b/src/window/game.rs new file mode 100644 index 0000000..0ec7885 --- /dev/null +++ b/src/window/game.rs @@ -0,0 +1,181 @@ +use crate::{ + app::UserEvent, + emulator::{EmulatorClient, EmulatorCommand, SimId}, +}; +use egui::{ + menu, Button, CentralPanel, Color32, Context, Frame, Response, TopBottomPanel, Ui, + ViewportBuilder, ViewportCommand, ViewportId, WidgetText, +}; +use winit::event_loop::EventLoopProxy; + +use super::{game_screen::GameScreen, AppWindow}; + +pub struct GameWindow { + client: EmulatorClient, + proxy: EventLoopProxy, + sim_id: SimId, + screen: Option, +} + +impl GameWindow { + pub fn new(client: EmulatorClient, proxy: EventLoopProxy, sim_id: SimId) -> Self { + Self { + client, + proxy, + sim_id, + screen: None, + } + } + + fn show_menu(&mut self, ctx: &Context, ui: &mut Ui) { + ui.menu_button("ROM", |ui| { + if ui.button("Open ROM").clicked() { + let rom = rfd::FileDialog::new() + .add_filter("Virtual Boy ROMs", &["vb", "vbrom"]) + .pick_file(); + if let Some(path) = rom { + self.client + .send_command(EmulatorCommand::LoadGame(SimId::Player1, path)); + } + ui.close_menu(); + } + if ui.button("Quit").clicked() { + ctx.send_viewport_cmd(ViewportCommand::Close); + } + }); + ui.menu_button("Emulation", |ui| { + let has_game = self.client.has_game(self.sim_id); + if self.client.is_running(self.sim_id) { + if ui.add_enabled(has_game, Button::new("Pause")).clicked() { + self.client.send_command(EmulatorCommand::Pause); + ui.close_menu(); + } + } else if ui.add_enabled(has_game, Button::new("Resume")).clicked() { + self.client.send_command(EmulatorCommand::Resume); + ui.close_menu(); + } + if ui.add_enabled(has_game, Button::new("Reset")).clicked() { + self.client + .send_command(EmulatorCommand::Reset(self.sim_id)); + ui.close_menu(); + } + }); + ui.menu_button("Video", |ui| { + let current_dims = ctx.input(|i| i.viewport().inner_rect.unwrap()); + let current_dims = current_dims.max - current_dims.min; + + for scale in 1..=4 { + let label = format!("x{scale}"); + let scale = scale as f32; + let dims = (384.0 * scale, 224.0 * scale + 22.0).into(); + if ui + .selectable_button((current_dims - dims).length() < 1.0, label) + .clicked() + { + ctx.send_viewport_cmd(ViewportCommand::InnerSize(dims)); + ui.close_menu(); + } + } + }); + ui.menu_button("Audio", |ui| { + let p1_enabled = self.client.is_audio_enabled(SimId::Player1); + let p2_enabled = self.client.is_audio_enabled(SimId::Player2); + if ui.selectable_button(p1_enabled, "Player 1").clicked() { + self.client + .send_command(EmulatorCommand::SetAudioEnabled(!p1_enabled, p2_enabled)); + ui.close_menu(); + } + if ui.selectable_button(p2_enabled, "Player 2").clicked() { + self.client + .send_command(EmulatorCommand::SetAudioEnabled(p1_enabled, !p2_enabled)); + ui.close_menu(); + } + }); + ui.menu_button("Input", |ui| { + if ui.button("Bind Inputs").clicked() { + self.proxy.send_event(UserEvent::OpenInput).unwrap(); + ui.close_menu(); + } + }); + ui.menu_button("Multiplayer", |ui| { + if self.sim_id == SimId::Player1 + && !self.client.has_player_2() + && ui.button("Open Player 2").clicked() + { + self.client + .send_command(EmulatorCommand::StartSecondSim(None)); + self.proxy.send_event(UserEvent::OpenPlayer2).unwrap(); + ui.close_menu(); + } + if self.client.has_player_2() { + let linked = self.client.are_sims_linked(); + if linked && ui.button("Unlink").clicked() { + self.client.send_command(EmulatorCommand::Unlink); + ui.close_menu(); + } + if !linked && ui.button("Link").clicked() { + self.client.send_command(EmulatorCommand::Link); + ui.close_menu(); + } + } + }); + } +} + +impl AppWindow for GameWindow { + fn viewport_id(&self) -> ViewportId { + match self.sim_id { + SimId::Player1 => ViewportId::ROOT, + SimId::Player2 => ViewportId::from_hash_of("Player2"), + } + } + + fn initial_viewport(&self) -> ViewportBuilder { + ViewportBuilder::default() + .with_title("Shrooms VB") + .with_inner_size((384.0, 246.0)) + } + + fn show(&mut self, ctx: &Context) { + TopBottomPanel::top("menubar") + .exact_height(22.0) + .show(ctx, |ui| { + menu::bar(ui, |ui| { + self.show_menu(ctx, ui); + }); + }); + let frame = Frame::central_panel(&ctx.style()).fill(Color32::BLACK); + CentralPanel::default().frame(frame).show(ctx, |ui| { + if let Some(screen) = self.screen.as_ref() { + ui.add(screen); + } + }); + } + + fn on_init(&mut self, render_state: &egui_wgpu::RenderState) { + let (screen, sink) = GameScreen::init(render_state); + self.client + .send_command(EmulatorCommand::SetRenderer(self.sim_id, sink)); + self.screen = Some(screen) + } + + fn on_destroy(&mut self) { + if self.sim_id == SimId::Player2 { + self.client.send_command(EmulatorCommand::StopSecondSim); + } + } +} + +trait UiExt { + fn selectable_button(&mut self, selected: bool, text: impl Into) -> Response; +} + +impl UiExt for Ui { + fn selectable_button(&mut self, selected: bool, text: impl Into) -> Response { + self.style_mut().visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT; + self.style_mut().visuals.widgets.hovered.bg_fill = Color32::TRANSPARENT; + self.style_mut().visuals.widgets.active.bg_fill = Color32::TRANSPARENT; + let mut selected = selected; + self.checkbox(&mut selected, text) + } +} diff --git a/src/window/game_screen.rs b/src/window/game_screen.rs new file mode 100644 index 0000000..c70c9bc --- /dev/null +++ b/src/window/game_screen.rs @@ -0,0 +1,206 @@ +use std::sync::Arc; + +use egui::Widget; +use wgpu::{util::DeviceExt as _, BindGroup, BindGroupLayout, RenderPipeline}; + +use crate::graphics::TextureSink; + +pub struct GameScreen { + bind_group: Arc, +} + +impl GameScreen { + fn init_pipeline(render_state: &egui_wgpu::RenderState) { + let device = &render_state.device; + + let 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 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: &[&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::Bgra8Unorm, + 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, + }); + + render_state + .renderer + .write() + .callback_resources + .insert(SharedGameScreenResources { + pipeline: render_pipeline, + bind_group_layout, + }); + } + + pub fn init(render_state: &egui_wgpu::RenderState) -> (Self, TextureSink) { + Self::init_pipeline(render_state); + + let device = &render_state.device; + let queue = &render_state.queue; + + let (sink, texture_view) = TextureSink::new(device, queue.clone()); + 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 renderer = render_state.renderer.read(); + let resources: &SharedGameScreenResources = renderer.callback_resources.get().unwrap(); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("bind group"), + layout: &resources.bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: color_buf.as_entire_binding(), + }, + ], + }); + + ( + Self { + bind_group: Arc::new(bind_group), + }, + sink, + ) + } +} + +impl Widget for &GameScreen { + fn ui(self, ui: &mut egui::Ui) -> egui::Response { + let response = ui.allocate_rect(ui.clip_rect(), egui::Sense::hover()); + let callback = egui_wgpu::Callback::new_paint_callback( + response.rect, + GameScreenCallback { + bind_group: self.bind_group.clone(), + }, + ); + ui.painter().add(callback); + response + } +} + +struct GameScreenCallback { + bind_group: Arc, +} + +impl egui_wgpu::CallbackTrait for GameScreenCallback { + fn paint( + &self, + info: egui::PaintCallbackInfo, + render_pass: &mut wgpu::RenderPass<'static>, + callback_resources: &egui_wgpu::CallbackResources, + ) { + let resources: &SharedGameScreenResources = callback_resources.get().unwrap(); + let viewport = info.viewport_in_pixels(); + let left = viewport.left_px as f32; + let top = viewport.top_px as f32; + let width = viewport.width_px as f32; + let height = viewport.height_px as f32; + let aspect_ratio = 384.0 / 224.0; + let w = width.min(height * aspect_ratio); + let h = height.min(width / aspect_ratio); + let x = left + (width - w) / 2.0; + let y = top + (height - h) / 2.0; + render_pass.set_pipeline(&resources.pipeline); + render_pass.set_bind_group(0, &self.bind_group, &[]); + render_pass.set_viewport(x, y, w, h, 0.0, 1.0); + render_pass.draw(0..6, 0..1); + } +} + +struct SharedGameScreenResources { + pipeline: RenderPipeline, + bind_group_layout: BindGroupLayout, +} + +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +struct Colors { + left: [f32; 4], + right: [f32; 4], +} diff --git a/src/window/input.rs b/src/window/input.rs new file mode 100644 index 0000000..c93c6e4 --- /dev/null +++ b/src/window/input.rs @@ -0,0 +1,257 @@ +use egui::{ + Button, CentralPanel, Context, Label, Layout, TopBottomPanel, Ui, ViewportBuilder, ViewportId, +}; +use egui_extras::{Column, TableBuilder}; +use gilrs::{EventType, GamepadId}; +use std::sync::RwLock; + +use crate::{ + emulator::{SimId, VBKey}, + input::{MappingProvider, Mappings}, +}; + +use super::AppWindow; + +pub struct InputWindow { + mappings: MappingProvider, + now_binding: Option, + active_tab: InputTab, +} + +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(mappings: MappingProvider) -> Self { + Self { + mappings, + now_binding: None, + active_tab: InputTab::Player1, + } + } + + fn show_bindings( + &mut self, + ui: &mut Ui, + mappings: &RwLock, + bind_message: &str, + ) { + ui.horizontal(|ui| { + if ui.button("Use defaults").clicked() { + mappings.write().unwrap().use_default_mappings(); + self.now_binding = None; + } + if ui.button("Clear all").clicked() { + mappings.write().unwrap().clear_all_mappings(); + self.now_binding = None; + } + }); + ui.separator(); + let mut names = { + let mapping = mappings.read().unwrap(); + mapping.mapping_names() + }; + TableBuilder::new(ui) + .column(Column::remainder()) + .column(Column::remainder()) + .cell_layout(Layout::left_to_right(egui::Align::Center)) + .body(|mut body| { + for keys in KEY_NAMES.chunks_exact(2) { + body.row(20.0, |mut row| { + for (key, name) in keys { + let binding = names.remove(key).map(|mut s| { + s.sort(); + s.join(", ") + }); + row.col(|ui| { + let size = ui.available_size_before_wrap(); + let width = size.x; + let height = size.y; + ui.add_sized((width * 0.2, height), Label::new(*name)); + let label_text = if self.now_binding == Some(*key) { + bind_message + } else { + binding.as_deref().unwrap_or("") + }; + if ui + .add_sized((width * 0.6, height), Button::new(label_text)) + .clicked() + { + self.now_binding = Some(*key); + } + if ui + .add_sized(ui.available_size(), Button::new("Clear")) + .clicked() + { + let mut mapping = mappings.write().unwrap(); + mapping.clear_mappings(*key); + self.now_binding = None; + } + }); + } + }); + } + }); + } + + fn show_key_bindings(&mut self, ui: &mut Ui, sim_id: SimId) { + let mappings = self.mappings.for_sim(sim_id).clone(); + self.show_bindings(ui, &mappings, "Press any key"); + } + + fn show_gamepads(&mut self, ui: &mut Ui) { + let mut gamepads = self.mappings.gamepad_info(); + gamepads.sort_by_key(|g| usize::from(g.id)); + + if gamepads.is_empty() { + ui.label("No gamepads connected."); + return; + } + + for (index, gamepad) in gamepads.into_iter().enumerate() { + ui.horizontal(|ui| { + ui.label(format!("Gamepad {index}: {}", gamepad.name)); + let mut bound_to = gamepad.bound_to; + let mut rebind = false; + rebind |= ui + .selectable_value(&mut bound_to, Some(SimId::Player1), "Player 1") + .changed(); + rebind |= ui + .selectable_value(&mut bound_to, Some(SimId::Player2), "Player 2") + .changed(); + rebind |= ui.selectable_value(&mut bound_to, None, "Nobody").changed(); + if rebind { + match bound_to { + Some(sim_id) => self.mappings.assign_gamepad(gamepad.id, sim_id), + None => self.mappings.unassign_gamepad(gamepad.id), + } + } + ui.separator(); + if ui.button("Rebind").clicked() { + self.active_tab = InputTab::RebindGamepad(gamepad.id); + } + }); + } + } + + fn show_gamepad_bindings(&mut self, ui: &mut Ui, gamepad_id: GamepadId) { + let Some(mappings) = self.mappings.for_gamepad(gamepad_id) else { + self.active_tab = InputTab::Gamepads; + return; + }; + self.show_bindings(ui, &mappings, "Press any input"); + } +} + +impl AppWindow for InputWindow { + fn viewport_id(&self) -> ViewportId { + ViewportId::from_hash_of("input") + } + + fn initial_viewport(&self) -> ViewportBuilder { + ViewportBuilder::default() + .with_title("Bind Inputs") + .with_inner_size((600.0, 400.0)) + } + + fn show(&mut self, ctx: &Context) { + TopBottomPanel::top("options").show(ctx, |ui| { + ui.horizontal(|ui| { + let old_active_tab = self.active_tab; + ui.selectable_value(&mut self.active_tab, InputTab::Player1, "Player 1"); + ui.selectable_value(&mut self.active_tab, InputTab::Player2, "Player 2"); + ui.selectable_value(&mut self.active_tab, InputTab::Gamepads, "Gamepads"); + if matches!(self.active_tab, InputTab::RebindGamepad(_)) { + let tab = self.active_tab; + ui.selectable_value(&mut self.active_tab, tab, "Rebind Gamepad"); + } + if old_active_tab != self.active_tab { + self.now_binding = None; + } + }); + }); + CentralPanel::default().show(ctx, |ui| { + match self.active_tab { + InputTab::Player1 => self.show_key_bindings(ui, SimId::Player1), + InputTab::Player2 => self.show_key_bindings(ui, SimId::Player2), + InputTab::Gamepads => self.show_gamepads(ui), + InputTab::RebindGamepad(id) => self.show_gamepad_bindings(ui, id), + }; + }); + } + + fn handle_key_event(&mut self, event: &winit::event::KeyEvent) { + if !event.state.is_pressed() { + return; + } + let sim_id = match self.active_tab { + InputTab::Player1 => SimId::Player1, + InputTab::Player2 => SimId::Player2, + _ => { + return; + } + }; + let Some(vb) = self.now_binding.take() else { + return; + }; + let mut mappings = self.mappings.for_sim(sim_id).write().unwrap(); + mappings.add_keyboard_mapping(vb, event.physical_key); + } + + fn handle_gamepad_event(&mut self, event: &gilrs::Event) { + let InputTab::RebindGamepad(gamepad_id) = self.active_tab else { + return; + }; + if gamepad_id != event.id { + return; + } + let Some(mappings) = self.mappings.for_gamepad(gamepad_id) else { + return; + }; + let Some(vb) = self.now_binding else { + return; + }; + match event.event { + EventType::ButtonPressed(_, code) => { + let mut mapping = mappings.write().unwrap(); + mapping.add_button_mapping(vb, code); + self.now_binding.take(); + } + EventType::AxisChanged(_, value, code) => { + if value < -0.75 { + let mut mapping = mappings.write().unwrap(); + mapping.add_axis_neg_mapping(vb, code); + self.now_binding.take(); + } + if value > 0.75 { + let mut mapping = mappings.write().unwrap(); + mapping.add_axis_pos_mapping(vb, code); + self.now_binding.take(); + } + } + _ => {} + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum InputTab { + Player1, + Player2, + Gamepads, + RebindGamepad(GamepadId), +}