diff options
30 files changed, 6396 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d661664 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2323 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.9.1", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log 0.4.27", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "assembler" +version = "2.3.0" +source = "git+git://benbridle.com/assembler?tag=v2.3.0#a9640fce1aaa5e80170ce4d2ac700f66cfffbb4b" +dependencies = [ + "inked", + "log 2.0.0", + "vagabond", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bedrock-asm" +version = "1.0.0" +source = "git+git://benbridle.com/bedrock-asm?tag=v1.0.0#f454238e433792fda0f66f807f7ec12d3e560fa2" +dependencies = [ + "assembler", + "log 2.0.0", + "switchboard", +] + +[[package]] +name = "bedrock-core" +version = "1.0.0" +source = "git+git://benbridle.com/bedrock-core?tag=v1.0.0#9e6a17a24c5dd5748270d07e59c13eca7810ba04" + +[[package]] +name = "bedrock-pc" +version = "0.0.1" +dependencies = [ + "bedrock-asm", + "bedrock-core", + "chrono", + "geometry", + "gilrs", + "inked", + "log 2.0.0", + "phosphor", + "switchboard", + "windows 0.58.0", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + +[[package]] +name = "buffer" +version = "1.1.0" +source = "git+git://benbridle.com/buffer?tag=v1.1.0#492cfb25742a2a6ca96671e6a9f3f3bfccf65b3e" +dependencies = [ + "colour", + "geometry", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytemuck" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.9.1", + "log 0.4.27", + "polling", + "rustix 0.38.44", + "slab", + "thiserror", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "colour" +version = "2.0.0" +source = "git+git://benbridle.com/colour?tag=v2.0.0#0ae93bec309d37cda72e137da83127bb48b22d65" +dependencies = [ + "proportion", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "ctor-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b" + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "drm" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1" +dependencies = [ + "bitflags 2.9.1", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c98727e48b7ccb4f4aea8cfe881e5b07f702d17b7875991881b41af7278d53" +dependencies = [ + "drm-sys", + "rustix 0.38.44", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd39dde40b6e196c2e8763f23d119ddb1a8714534bf7d77fa97a65b0feda3986" +dependencies = [ + "libc", + "linux-raw-sys 0.6.5", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "event-queue" +version = "1.2.0" +source = "git+git://benbridle.com/event-queue?tag=v1.2.0#f595c3757788a230b31396f32980c20fe0c092cd" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "geometry" +version = "1.0.0" +source = "git+git://benbridle.com/geometry?tag=v1.0.0#8547ad9c2f2867da9b0e655dd4ff0fe78f04ffba" + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "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 0.4.27", + "uuid", + "vec_map", +] + +[[package]] +name = "gilrs-core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d95ae10ce5aa99543a28cf74e41c11f3b9e3c14f0452bbde46024753cd683e" +dependencies = [ + "core-foundation 0.10.1", + "inotify", + "io-kit-sys", + "js-sys", + "libc", + "libudev-sys", + "log 0.4.27", + "nix", + "uuid", + "vec_map", + "wasm-bindgen", + "web-sys", + "windows 0.61.3", +] + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log 0.4.27", + "wasm-bindgen", + "windows-core 0.61.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inked" +version = "1.0.0" +source = "git+git://benbridle.com/inked?tag=v1.0.0#2954d37b638fa2c1dd3d51ff53f08f475aea6ea3" +dependencies = [ + "termcolor", +] + +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.9.1", + "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]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log 0.4.27", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.2", +] + +[[package]] +name = "libredox" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" +dependencies = [ + "bitflags 2.9.1", + "libc", + "redox_syscall 0.5.13", +] + +[[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.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "log" +version = "1.1.1" +source = "git+git://benbridle.com/log?tag=v1.1.1#930f3d0e2b82df1243f423c092a38546ea7533c3" + +[[package]] +name = "log" +version = "2.0.0" +source = "git+git://benbridle.com/log?tag=v2.0.0#a38d3dd487594f41151db57625410d1b786bebe4" +dependencies = [ + "inked", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.9.1", + "jni-sys", + "log 0.4.27", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.9.1", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.9.1", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.9.1", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2", + "objc2-contacts", + "objc2-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.9.1", + "block2", + "dispatch", + "libc", + "objc2", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.9.1", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.9.1", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.9.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.9.1", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "orbclient" +version = "0.3.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" +dependencies = [ + "libredox", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phosphor" +version = "3.3.0" +source = "git+git://benbridle.com/phosphor?tag=v3.3.0#92a62768fe6be9f72716c14f427767cce5c03b2f" +dependencies = [ + "buffer", + "event-queue", + "geometry", + "softbuffer", + "winit", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polling" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.0.7", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proportion" +version = "1.0.0" +source = "git+git://benbridle.com/proportion?tag=v1.0.0#291bb4de2a2e5940fbebd8d400f35c1fc3b0c7a2" + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.9.1", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log 0.4.27", + "memmap2", + "rustix 0.38.44", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "softbuffer" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "cfg_aliases", + "core-graphics 0.24.0", + "drm", + "fastrand", + "foreign-types", + "js-sys", + "log 0.4.27", + "memmap2", + "objc2", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall 0.5.13", + "rustix 0.38.44", + "tiny-xlib", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.59.0", + "x11rb", +] + +[[package]] +name = "switchboard" +version = "2.1.0" +source = "git+git://benbridle.com/switchboard?tag=v2.1.0#e6435712ba5b3ca36e99fc8cbe7755940f8b1f3f" +dependencies = [ + "log 1.1.1", + "paste", +] + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiny-xlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e" +dependencies = [ + "as-raw-xcb-connection", + "ctor-lite", + "libloading", + "pkg-config", + "tracing", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "vagabond" +version = "1.1.1" +source = "git+git://benbridle.com/vagabond?tag=v1.1.1#b190582517e6008ad1deff1859f15988e4efaa26" + +[[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 = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log 0.4.27", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121" +dependencies = [ + "cc", + "downcast-rs", + "rustix 0.38.44", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61" +dependencies = [ + "bitflags 2.9.1", + "rustix 0.38.44", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.9.1", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65317158dec28d00416cb16705934070aef4f8393353d41126c54264ae0f182" +dependencies = [ + "rustix 0.38.44", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd38cdad69b56ace413c6bcc1fbf5acc5e2ef4af9d5f8f1f9570c0c83eae175" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" +dependencies = [ + "dlib", + "log 0.4.27", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "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", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[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 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link", + "windows-threading", +] + +[[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", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link", +] + +[[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-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[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-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winit" +version = "0.30.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.9.1", + "block2", + "bytemuck", + "calloop", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "smithay-client-toolkit", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 0.38.44", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.9.1", + "dlib", + "log 0.4.27", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ef90f39 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "bedrock-pc" +version = "0.0.1" +authors = ["Ben Bridle"] +edition = "2024" +description = "Bedrock emulator" + +[dependencies] +bedrock-asm = { git = "git://benbridle.com/bedrock-asm", tag = "v1.0.0" } +bedrock-core = { git = "git://benbridle.com/bedrock-core", tag = "v1.0.0" } +phosphor = { git = "git://benbridle.com/phosphor", tag = "v3.3.0" } +geometry = { git = "git://benbridle.com/geometry", tag = "v1.0.0" } +inked = { git = "git://benbridle.com/inked", tag = "v1.0.0" } +log = { git = "git://benbridle.com/log", tag = "v2.0.0" } +switchboard = { git = "git://benbridle.com/switchboard", tag = "v2.1.0" } + +chrono = { version = "0.4.38" } +gilrs = "0.11.0" + +[target.'cfg(target_os = "windows")'.dependencies] +windows = { version = "0.58.0", features = ["Win32_Storage_FileSystem"] } + + +[profile.release] +lto=true +opt-level="s" +debug=false +strip=true +codegen-units=1 diff --git a/src/bin/br/config.rs b/src/bin/br/config.rs new file mode 100644 index 0000000..56d5190 --- /dev/null +++ b/src/bin/br/config.rs @@ -0,0 +1,122 @@ +use crate::*; + + +#[derive(Copy, Clone, PartialEq)] +pub enum Mode { + Graphical, + Headless, + Dynamic, +} + +impl Mode { + pub fn from_str(string: &str) -> Self { + match string { + "g" | "graphical" => Self::Graphical, + "h" | "headless" => Self::Headless, + "d" | "dynamic" => Self::Dynamic, + _ => fatal!("Invalid mode string '{string}'"), + } + } +} + + +pub fn parse_dimensions(string: &str) -> ScreenDimensions { + fn parse_inner(string: &str) -> Option<ScreenDimensions> { + let (w_str, h_str) = string.trim().split_once('x')?; + Some( ScreenDimensions { + width: w_str.parse().ok()?, + height: h_str.parse().ok()?, + } ) + } + let dimensions = parse_inner(string).unwrap_or_else(|| { + fatal!("Invalid dimensions string '{string}'"); + }); + if dimensions.is_zero() { + fatal!("Screen dimensions must be greater than zero"); + } + return dimensions; +} + + +pub fn parse_palette(string: &str) -> [Colour; 16] { + fn decode_ascii_hex_digit(ascii: u8) -> Option<u8> { + match ascii { + b'0'..=b'9' => Some(ascii - b'0'), + b'a'..=b'f' => Some(ascii - b'a' + 10), + b'A'..=b'F' => Some(ascii - b'A' + 10), + _ => { None } + } + } + fn parse_inner(string: &str) -> Option<[Colour; 16]> { + let mut c = Vec::new(); + for token in string.split(',') { + let mut bytes = token.bytes(); + if bytes.len() != 3 { return None; } + let r = decode_ascii_hex_digit(bytes.next().unwrap())?; + let g = decode_ascii_hex_digit(bytes.next().unwrap())?; + let b = decode_ascii_hex_digit(bytes.next().unwrap())?; + c.push(Colour::from_rgb(r*17, g*17, b*17)); + } + Some(match c.len() { + 2 => [ c[0], c[1], c[0], c[1], c[0], c[1], c[0], c[1], + c[0], c[1], c[0], c[1], c[0], c[1], c[0], c[1] ], + 3 => [ c[0], c[1], c[0], c[2], c[0], c[1], c[0], c[2], + c[0], c[1], c[0], c[2], c[0], c[1], c[0], c[2] ], + 4 => [ c[0], c[1], c[2], c[3], c[0], c[1], c[2], c[3], + c[0], c[1], c[2], c[3], c[0], c[1], c[2], c[3] ], + 8 => [ c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], + c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7] ], + 16 => [ c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], + c[8], c[9], c[10], c[11], c[12], c[13], c[14], c[15] ], + _ => return None, + }) + } + parse_inner(string).unwrap_or_else(|| { + fatal!("Invalid palette string '{string}'"); + }) +} + + +pub fn parse_metadata_colour(colour: Option<MetadataColour>) -> Option<Colour> { + let c = colour?; + Some(Colour::from_rgb(c.red, c.green, c.blue)) +} + + +pub fn parse_small_icon(bytes: Option<Vec<u8>>, bg: Colour, fg: Colour) -> Option<Icon> { + let rgba = sprite_data_to_rgb(&bytes?, 3, bg, fg); + match Icon::from_rgba(rgba, 24, 24) { + Ok(icon) => Some(icon), + Err(err) => unreachable!("Error while parsing small icon data: {err}"), + } +} + +pub fn parse_large_icon(bytes: Option<Vec<u8>>, bg: Colour, fg: Colour) -> Option<Icon> { + let rgba = sprite_data_to_rgb(&bytes?, 8, bg, fg); + match Icon::from_rgba(rgba, 64, 64) { + Ok(icon) => Some(icon), + Err(err) => unreachable!("Error while parsing large icon data: {err}"), + } +} + +fn sprite_data_to_rgb(bytes: &[u8], size: usize, bg: Colour, fg: Colour) -> Vec<u8> { + let sprites: Vec<&[u8]> = bytes.chunks_exact(8).collect(); + let mut rgba = Vec::new(); + for sprite_row in 0..size { + for pixel_row in 0..8 { + for sprite_column in 0..size { + let sprite = &sprites[sprite_column + (sprite_row * size)]; + let row = &sprite[pixel_row]; + for bit in 0..8 { + let state = row & (0x80 >> bit); + let colour = match state != 0 { + true => fg, + false => bg, + }; + rgba.extend_from_slice(&colour.as_rgba_array()); + } + } + } + } + return rgba; +} diff --git a/src/bin/br/load.rs b/src/bin/br/load.rs new file mode 100644 index 0000000..93a748c --- /dev/null +++ b/src/bin/br/load.rs @@ -0,0 +1,81 @@ +use crate::*; + +use std::io::Read; +use std::path::{Path, PathBuf}; + + +pub struct LoadedProgram { + pub bytecode: Vec<u8>, + pub path: Option<PathBuf>, +} + +/// Load program from path or standard input. +pub fn load_program(path: Option<&PathBuf>) -> LoadedProgram { + if let Some(path) = path { + if let Ok(program) = load_program_from_file(path) { + let length = program.bytecode.len(); + info!("Loaded program from {path:?} ({length} bytes)"); + return program; + } else if let Some(program) = load_program_from_env(path) { + let length = program.bytecode.len(); + info!("Loaded program from {path:?} ({length} bytes)"); + return program; + } else { + fatal!("Could not read program from {path:?}"); + } + } else { + info!("Reading program from standard input..."); + if let Ok(program) = load_program_from_stdin() { + let length = program.bytecode.len(); + info!("Loaded program from standard input ({length} bytes)"); + return program; + } else { + fatal!("Could not read program from standard input"); + } + } +} + +/// Attempt to load program from a directory in the BEDROCK_PATH environment variable. +fn load_program_from_env(path: &Path) -> Option<LoadedProgram> { + // Check that the path is a bare program name. + if path.is_relative() && path.components().count() == 1 { + for base_path in std::env::var("BEDROCK_PATH").ok()?.split(':') { + let mut base_path = PathBuf::from(base_path); + // Skip relative paths. + if !base_path.is_absolute() { continue; } + base_path.push(path); + if let Ok(program) = load_program_from_file(&base_path) { + return Some(program); + } + if path.extension().is_some() { continue; } + base_path.set_extension("br"); + if let Ok(program) = load_program_from_file(&base_path) { + return Some(program); + } + } + } + return None; +} + +/// Attempt to load program from a file path. +fn load_program_from_file(path: &Path) -> Result<LoadedProgram, std::io::Error> { + // Canonicalize paths so that symbolic links to program files resolve to + // the real program directory, which could contain a symbols file. + let path = match path.canonicalize() { + Ok(canonical) => canonical, + Err(_) => path.to_path_buf(), + }; + load_program_from_readable_source(std::fs::File::open(&path)?, Some(&path)) +} + +/// Attempt to load program from standard input. +fn load_program_from_stdin() -> Result<LoadedProgram, std::io::Error> { + load_program_from_readable_source(std::io::stdin(), None) +} + +/// Attempt to load program from a source that implements std::io::Read. +fn load_program_from_readable_source(source: impl Read, path: Option<&Path>) -> Result<LoadedProgram, std::io::Error> { + let mut bytecode = Vec::<u8>::new(); + source.take(65536).read_to_end(&mut bytecode)?; + return Ok(LoadedProgram { bytecode, path: path.map(|p| p.to_path_buf()) }); +} diff --git a/src/bin/br/main.rs b/src/bin/br/main.rs new file mode 100644 index 0000000..81c5ec9 --- /dev/null +++ b/src/bin/br/main.rs @@ -0,0 +1,217 @@ +#![feature(path_add_extension)] + +mod config; +mod load; +pub use config::*; +pub use load::*; + +use bedrock_asm::*; +use bedrock_core::*; +use bedrock_pc::*; +use log::*; +use switchboard::*; +use phosphor::*; + +use std::cmp::{min, max}; +use std::num::NonZeroU32; + + +fn main() { + let mut args = Switchboard::from_env(); + if let Some("asm") = args.peek() { + args.pop(); + assemble(args, "br asm"); + } + + // ----------------------------------------------------------------------- + + args.named("help").short('h'); + args.named("version"); + args.named("verbose").short('v'); + + if args.get("help").as_bool() { + print_help(); + std::process::exit(0); + } + if args.get("version").as_bool() { + let name = env!("CARGO_PKG_NAME"); + let version = env!("CARGO_PKG_VERSION"); + eprintln!("{name} v{version}"); + eprintln!("Written by Ben Bridle."); + std::process::exit(0); + } + if args.get("verbose").as_bool() { + log::set_log_level(log::LogLevel::Info); + } + + args.positional("source"); + args.named("debug").short('d'); + args.named("mode").default("dynamic"); + + args.named("palette"); + args.named("fullscreen").short('f'); + args.named("show-cursor").short('c'); + args.named("zoom").short('z').quick("3").default("1"); + args.named("size").short('s'); + args.named("decode-stdin").short('i'); + args.named("encode-stdout").short('o'); + args.raise_errors(); + + let source = args.get("source").as_path_opt(); + let debug = args.get("debug").as_bool(); + let mode = Mode::from_str(args.get("mode").as_str()); + let palette = args.get("palette").as_str_opt().map(parse_palette); + let fullscreen = args.get("fullscreen").as_bool(); + let show_cursor = args.get("show-cursor").as_bool(); + let zoom_raw = min(10, max(1, args.get("zoom").as_u32())); + let zoom = unsafe { NonZeroU32::new_unchecked(zoom_raw) }; + let dimensions = match args.get("size").as_str_opt() { + Some(string) => parse_dimensions(string), + None => DEFAULT_SCREEN_SIZE / (zoom_raw as u16), + }; + let decode_stdin = args.get("decode-stdin").as_bool(); + let encode_stdout = args.get("encode-stdout").as_bool(); + + // ----------------------------------------------------------------------- + + let LoadedProgram { bytecode, path } = load_program(source.as_ref()); + let mut title = String::from("Bedrock program"); + let mut icon = None; + + if let Some(metadata) = Metadata::from(&bytecode) { + let name = metadata.name().unwrap_or("unnamed".to_string()); + let authors = metadata.authors().unwrap_or_else(Vec::new); + let mut metadata_string = format!("Program is '{name}'"); + if authors.len() > 0 { + metadata_string.push_str(&format!(", by {}", authors[0])); } + if authors.len() > 1 { + metadata_string.push_str(" and others"); } + info!("{metadata_string}"); + + match name.split_once('/') { + Some((name, _version)) if !name.is_empty() => title = name.to_string(), + _ => title = name.to_string(), + } + let bg = parse_metadata_colour(metadata.bg_colour()).unwrap_or(Colour::rgb(0x1E1C26)); + let fg = parse_metadata_colour(metadata.fg_colour()).unwrap_or(Colour::rgb(0xED614F)); + match parse_large_icon(metadata.large_icon(), bg, fg) { + Some(large_icon) => icon = Some(large_icon), + None => match parse_small_icon(metadata.small_icon(), bg, fg) { + Some(small_icon) => icon = Some(small_icon), + None => (), + } + } + } else { + info!("Program does not contain metadata"); + } + + let symbols_path = path.as_ref().map(|p| { + let mut path = p.to_path_buf(); + path.add_extension("sym"); path + }); + + let config = EmulatorConfig { + dimensions, fullscreen, zoom, palette, show_cursor, + decode_stdin, encode_stdout, + symbols_path, title, icon, + }; + + if let Ok(phosphor) = Phosphor::new() { + match mode { + Mode::Dynamic => { + info!("Starting graphical emulator"); + let mut emulator = GraphicalEmulator::new(config, debug); + emulator.load_program(&bytecode); + emulator.run(phosphor, false); + } + Mode::Graphical => { + info!("Starting graphical emulator"); + let mut emulator = GraphicalEmulator::new(config, debug); + emulator.load_program(&bytecode); + emulator.run(phosphor, false); + } + Mode::Headless => { + info!("Starting headless emulator"); + let mut emulator = HeadlessEmulator::new(&config, debug); + emulator.load_program(&bytecode); + emulator.run(); + } + } + } else { + match mode { + Mode::Dynamic => { + info!("Could not start graphical event loop"); + info!("Starting headless emulator"); + let mut emulator = HeadlessEmulator::new(&config, debug); + emulator.load_program(&bytecode); + emulator.run(); + } + Mode::Graphical => { + fatal!("Could not start graphical event loop"); + } + Mode::Headless => { + info!("Starting headless emulator"); + let mut emulator = HeadlessEmulator::new(&config, debug); + emulator.load_program(&bytecode); + emulator.run(); + } + } + } +} + + +fn print_help() { + eprintln!("\ +Usage: br [source] + br asm [source] [destination] + +Emulator and assembler for the Bedrock computer system. + +To access the assembler, run `br asm`. To learn how to use the +assembler, run `br asm --help`. + +Usage: + To load a Bedrock program from a file, run `br <path>`, where + <path> is the path to an assembled Bedrock program. + + To load a Bedrock program from piped input, run `<command> | br`, + where <command> is a command that generates Bedrock bytecode. + + To assemble a Bedrock program from a source file and write to an + output file, run `br asm <source> <output>`, where <source> is the + path of the source file and <output> is the path to write to. + + To assemble and run a Bedrock program without saving to a file, + run `br asm <source> | br`, where <source> is the path of the + source file. + +Environment variables: + BEDROCK_PATH + A list of colon-separated paths that will be searched to find + a Bedrock program when the program doesn't exist in the current + directory. This allows the user to run frequently-used programs + from any directory. + BEDROCK_LIBS + A list of colon-separated paths that will be searched to find + Bedrock source code files to use as libraries when assembling a + Bedrock program. If a library file resolves an unresolved symbol + in the program being assembled, the library file will be merged + into the program. + +Arguments: + [program] Path to a Bedrock program to run + +Switches: + --fullscreen (-f) Start the program in fullscreen mode (toggle with F11) + --size=<dim> (-s) Set the initial window size in the format <width>x<height> + --zoom=<scale> (-z) Set the pixel size for the screen (change with F5/F6) + --show-cursor (-c) Show the operating system cursor over the window + --palette=<pal> Set a debug colour palette in the format <rgb>,... (toggle with F2) + --debug, (-d) Show debug information while the program is running + --decode-stdin (-i) Decode transmissions on standard input from text lines. + --encode-stdout (-o) Encode transmissions on standard output as text lines. + --help (-h) Print this help information + --verbose, (-v) Print additional information + --version Print the program version and exit +"); +} diff --git a/src/debug.rs b/src/debug.rs new file mode 100644 index 0000000..b007d9f --- /dev/null +++ b/src/debug.rs @@ -0,0 +1,126 @@ +use crate::*; + +use inked::{ink, InkedString}; + + +pub struct DebugState { + pub enabled: bool, + last_cycle: usize, + mark: Instant, + symbols: DebugSymbols, +} + +impl DebugState { + pub fn new<P: AsRef<Path>>(enabled: bool, symbols_path: Option<P>) -> Self { + Self { + enabled, + last_cycle: 0, + mark: Instant::now(), + symbols: DebugSymbols::from_path(symbols_path), + } + } + + pub fn debug_full(&mut self, core: &BedrockCore) { + if self.enabled { + let prev_pc = core.mem.pc.wrapping_sub(1); + let cycle = core.cycle; + let delta = core.cycle.saturating_sub(self.last_cycle); + let elapsed = self.mark.elapsed(); + + eprintln!(" PC: 0x{prev_pc:04x} Cycle: {cycle} (+{delta} in {elapsed:.2?})"); + eprint!("WST: "); debug_stack(&core.wst); + eprint!("RST: "); debug_stack(&core.rst); + // Print information about the nearest symbol. + if let Some(symbol) = self.symbols.for_address(prev_pc) { + let name = &symbol.name; + let address = &symbol.address; + let mut string = InkedString::new(); + string.push(ink!("SYM: ")); + string.push(ink!("@{name}").blue()); + string.push(ink!(" 0x{address:04X}")); + if let Some(location) = &symbol.location { + string.push(ink!(" ")); + string.push(ink!("{location}").dim()); + } + string.eprintln(); + } + } + self.last_cycle = core.cycle; + self.mark = Instant::now(); + } + + pub fn debug_timing(&mut self, core: &BedrockCore) { + if self.enabled { + let cycle = core.cycle; + let delta = core.cycle.saturating_sub(self.last_cycle); + let elapsed = self.mark.elapsed(); + + eprintln!("Cycle: {cycle} (+{delta} in {elapsed:.2?})"); + } + self.last_cycle = core.cycle; + self.mark = Instant::now(); + } +} + + +fn debug_stack(stack: &Stack) { + for i in 0..(stack.sp as usize) { + eprint!("{:02X} ", stack.mem[i]); + } + eprintln!(); +} + + + +pub struct DebugSymbols { + pub symbols: Vec<DebugSymbol> +} + +impl DebugSymbols { + /// Load debug symbols from a symbols file. + pub fn from_path<P: AsRef<Path>>(path: Option<P>) -> Self { + let mut symbols = Vec::new(); + if let Some(path) = path { + if let Ok(string) = std::fs::read_to_string(path) { + for line in string.lines() { + if let Some(symbol) = DebugSymbol::from_line(line) { + symbols.push(symbol); + } + } + } + } + symbols.sort_by_key(|s| s.address); + Self { symbols } + } + + /// Return the symbol matching a given program address. + pub fn for_address(&self, address: u16) -> Option<&DebugSymbol> { + if self.symbols.is_empty() { return None; } + match self.symbols.binary_search_by_key(&address, |s| s.address) { + Ok(index) => self.symbols.get(index), + Err(index) => self.symbols.get(index.checked_sub(1)?), + } + } +} + + +pub struct DebugSymbol { + pub address: u16, + pub name: String, + pub location: Option<String>, +} + +impl DebugSymbol { + pub fn from_line(line: &str) -> Option<Self> { + let (address, line) = line.split_once(' ')?; + let address = u16::from_str_radix(address, 16).ok()?; + if let Some((name, location)) = line.split_once(' ') { + let name = name.to_string(); + let location = Some(location.to_string()); + Some( DebugSymbol { address, name, location } ) + } else { + let name = line.to_string(); + Some( DebugSymbol { address, name, location: None } ) + } + } +} diff --git a/src/devices/clock_device.rs b/src/devices/clock_device.rs new file mode 100644 index 0000000..d06a92c --- /dev/null +++ b/src/devices/clock_device.rs @@ -0,0 +1,161 @@ +use crate::*; + +use chrono::prelude::*; + + +pub struct ClockDevice { + pub epoch: Instant, + pub uptime_read: u16, + + pub t1: CountdownTimer, + pub t2: CountdownTimer, + pub t3: CountdownTimer, + pub t4: CountdownTimer, +} + + +impl Device for ClockDevice { + fn read(&mut self, port: u8) -> u8 { + match port { + 0x0 => Local::now().year().saturating_sub(2000) as u8, + 0x1 => Local::now().month().saturating_sub(1) as u8, + 0x2 => Local::now().day().saturating_sub(1) as u8, + 0x3 => Local::now().hour() as u8, + 0x4 => Local::now().minute() as u8, + 0x5 => Local::now().second() as u8, + 0x6 => { self.uptime_read = self.uptime() as u16; + read_h!(self.uptime_read) }, + 0x7 => read_l!(self.uptime_read), + 0x8 => { self.t1.update(); read_h!(self.t1.read) }, + 0x9 => read_l!(self.t1.read), + 0xA => { self.t2.update(); read_h!(self.t2.read) }, + 0xB => read_l!(self.t2.read), + 0xC => { self.t3.update(); read_h!(self.t3.read) }, + 0xD => read_l!(self.t3.read), + 0xE => { self.t4.update(); read_h!(self.t4.read) }, + 0xF => read_l!(self.t4.read), + _ => unreachable!(), + } + } + + fn write(&mut self, port: u8, value: u8) -> Option<Signal> { + match port { + 0x0 => (), + 0x1 => (), + 0x2 => (), + 0x3 => (), + 0x4 => (), + 0x5 => (), + 0x6 => (), + 0x7 => (), + 0x8 => write_h!(self.t1.write, value), + 0x9 => { write_l!(self.t1.write, value); self.t1.commit() }, + 0xA => write_h!(self.t2.write, value), + 0xB => { write_l!(self.t2.write, value); self.t2.commit() }, + 0xC => write_h!(self.t3.write, value), + 0xD => { write_l!(self.t3.write, value); self.t3.commit() }, + 0xE => write_h!(self.t4.write, value), + 0xF => { write_l!(self.t4.write, value); self.t4.commit() }, + _ => unreachable!(), + }; + return None; + } + + fn wake(&mut self) -> bool { + let t1 = self.t1.wake(); + let t2 = self.t2.wake(); + let t3 = self.t3.wake(); + let t4 = self.t4.wake(); + return t1 | t2 | t3 | t4; + } + + fn reset(&mut self) { + self.epoch = Instant::now(); + self.uptime_read = 0; + self.t1.reset(); + self.t2.reset(); + self.t3.reset(); + self.t4.reset(); + } +} + + +impl ClockDevice { + pub fn new() -> Self { + Self { + epoch: Instant::now(), + uptime_read: 0, + + t1: CountdownTimer::new(), + t2: CountdownTimer::new(), + t3: CountdownTimer::new(), + t4: CountdownTimer::new(), + } + } + + pub fn uptime(&self) -> u64 { + (self.epoch.elapsed().as_nanos() * 256 / 1_000_000_000) as u64 + } +} + + + +pub struct CountdownTimer { + pub end: Option<Instant>, + pub read: u16, + pub write: u16, + pub wake: bool, +} + +impl CountdownTimer { + pub fn new() -> Self { + Self { + end: None, + read: 0, + write: 0, + wake: false, + } + } + + pub fn reset(&mut self) { + self.end = None; + self.read = 0; + self.write = 0; + self.wake = false; + } + + pub fn wake(&mut self) -> bool { + if let Some(end) = self.end { + if end <= Instant::now() { + self.end = None; + self.wake = true; + } + } + std::mem::take(&mut self.wake) + } + + pub fn update(&mut self) { + if let Some(end) = self.end { + let now = Instant::now(); + if end > now { + let nanos = (end - now).as_nanos(); + self.read = (nanos * 256 / 1_000_000_000) as u16; + } else { + self.read = 0; + self.end = None; + self.wake = true; + } + } else { + self.read = 0; + } + } + + pub fn commit(&mut self) { + if self.write > 0 { + let nanos = (self.write as u64) * 1_000_000_000 / 256; + self.end = Some(Instant::now() + Duration::from_nanos(nanos)); + } else { + self.end = None; + } + } +} diff --git a/src/devices/file_device.rs b/src/devices/file_device.rs new file mode 100644 index 0000000..7a64c8e --- /dev/null +++ b/src/devices/file_device.rs @@ -0,0 +1,376 @@ +use crate::*; + + +pub struct FileDevice { + pub base_path: PathBuf, + pub default_path: PathBuf, + + pub entry_buffer: BedrockPathBuffer, + pub action_buffer: BedrockPathBuffer, + pub path_buffer: BedrockPathBuffer, + + pub entry: Option<(Entry, BedrockFilePath)>, + pub cached_dir: Option<(Entry, BedrockFilePath)>, + + pub success: bool, + pub pointer_write: u32, + pub length_write: u32, + + pub enable_read: bool, + pub enable_write: bool, + pub enable_create: bool, + pub enable_move: bool, + pub enable_delete: bool, +} + + +impl Device for FileDevice { + fn read(&mut self, port: u8) -> u8 { + match port { + 0x0 => read_b!(self.entry.is_some()), + 0x1 => read_b!(self.success), + 0x2 => self.path_buffer.read(), + 0x3 => read_b!(self.entry_type()), + 0x4 => self.read_byte(), + 0x5 => self.read_byte(), + 0x6 => self.read_child_path(), + 0x7 => read_b!(self.child_type()), + 0x8 => read_hh!(self.pointer()), + 0x9 => read_hl!(self.pointer()), + 0xA => read_lh!(self.pointer()), + 0xB => read_ll!(self.pointer()), + 0xC => read_hh!(self.length()), + 0xD => read_hl!(self.length()), + 0xE => read_lh!(self.length()), + 0xF => read_ll!(self.length()), + _ => unreachable!(), + } + } + + fn write(&mut self, port: u8, value: u8) -> Option<Signal> { + match port { + 0x0 => self.write_to_entry_port(value), + 0x1 => self.write_to_action_port(value), + 0x2 => self.path_buffer.set_pointer(value), + 0x3 => self.ascend_to_parent(), + 0x4 => self.write_byte(value), + 0x5 => self.write_byte(value), + 0x6 => self.set_child_path(value), + 0x7 => self.descend_to_child(), + 0x8 => write_hh!(self.pointer_write, value), + 0x9 => write_hl!(self.pointer_write, value), + 0xA => write_lh!(self.pointer_write, value), + 0xB => {write_ll!(self.pointer_write, value); self.commit_pointer()}, + 0xC => write_hh!(self.length_write, value), + 0xD => write_hl!(self.length_write, value), + 0xE => write_lh!(self.length_write, value), + 0xF => {write_ll!(self.length_write, value); self.commit_length()}, + _ => unreachable!(), + }; + return None; + } + + fn wake(&mut self) -> bool { + false + } + + fn reset(&mut self) { + todo!() + } +} + + +impl FileDevice { + pub fn new() -> Self { + #[cfg(target_family = "unix")] + let default_base: PathBuf = PathBuf::from("/"); + #[cfg(target_family = "windows")] + let default_base: PathBuf = PathBuf::from(""); + + // TODO: I'm not at all confident that the default path is correct + // when not being set as the current directory. + Self { + base_path: default_base, + default_path: match std::env::current_dir() { + Ok(dir) => PathBuf::from(dir), + Err(_) => PathBuf::from(""), + }, + + entry_buffer: BedrockPathBuffer::new(), + action_buffer: BedrockPathBuffer::new(), + path_buffer: BedrockPathBuffer::new(), + + entry: None, + cached_dir: None, + + success: false, + pointer_write: 0, + length_write: 0, + + enable_read: true, + enable_write: true, + enable_create: true, + enable_move: true, + enable_delete: false, + } + } + + /// Safely close the current entry, cleaning up entry variables. + pub fn close(&mut self) { + self.entry_buffer.clear(); + self.action_buffer.clear(); + self.path_buffer.clear(); + self.flush(); + + if let Some((Entry::Directory(mut dir), path)) = std::mem::take(&mut self.entry) { + // Prevent the selected child from persisting when loading from cache. + dir.deselect_child(); + self.cached_dir = Some((Entry::Directory(dir), path)); + } + } + + /// Open the entry at the given Bedrock path. + pub fn open(&mut self, path: BedrockFilePath) -> Result<(), ()> { + match path.entry_type() { + Some(EntryType::File) => { + let open_result = std::fs::OpenOptions::new() + .read(self.enable_read) + .write(self.enable_write) + .open(path.as_path()); + // Keep the current entry open if we can't open the new path. + if let Ok(file) = open_result { + self.close(); + self.path_buffer.populate(path.as_buffer()); + self.entry = Some((Entry::File(BufferedFile::new(file)), path)); + return Ok(()); + }; + } + Some(EntryType::Directory) => { + // Attempt to use the cached directory. + if let Some((dir, cached_path)) = std::mem::take(&mut self.cached_dir) { + if cached_path == path { + self.close(); + self.path_buffer.populate(cached_path.as_buffer()); + self.entry = Some((dir, cached_path)); + return Ok(()); + } + } + // Keep the current entry open if we can't open the new path. + if let Some(listing) = DirectoryListing::from_path(&path) { + self.close(); + self.path_buffer.populate(path.as_buffer()); + self.entry = Some((Entry::Directory(listing), path)); + return Ok(()); + }; + } + // The entry either doesn't exist or is not a file or directory. + None => (), + } + return Err(()); + } + + /// Process a byte received from the entry port. + pub fn write_to_entry_port(&mut self, byte: u8) { + if let Some(buffer) = self.entry_buffer.write(byte) { + self.close(); + match BedrockFilePath::from_buffer(buffer, &self.base_path) { + Some(path) => self.success = self.open(path).is_ok(), + None => self.success = false, + }; + } + } + + /// Process a byte received from the action port. + pub fn write_to_action_port(&mut self, byte: u8) { + if let Some(buffer) = self.action_buffer.write(byte) { + let destination_blank = buffer[0] == 0x00; + let destination = BedrockFilePath::from_buffer(buffer, &self.base_path); + self.success = false; + + if let Some((_, source)) = &self.entry { + if destination_blank { + if self.enable_delete { + self.success = delete_entry(&source.as_path()); + } + } else if let Some(dest) = destination { + if self.enable_move { + self.success = move_entry(&source.as_path(), &dest.as_path()); + } + } + } else if let Some(dest) = destination { + if self.enable_create { + self.success = create_file(&dest.as_path()); + } + } + self.close(); + } + } + + /// Attempt to open the parent directory of the current entry. + pub fn ascend_to_parent(&mut self) { + if let Some((_, path)) = &self.entry { + match path.parent() { + Some(parent) => self.success = self.open(parent).is_ok(), + None => self.success = false, + }; + } else { + match BedrockFilePath::from_path(&self.default_path, &self.base_path) { + Some(default) => self.success = self.open(default).is_ok(), + None => self.success = false, + }; + } + } + + /// Attempt to open the selected child of the current directory. + pub fn descend_to_child(&mut self) { + if let Some((Entry::Directory(dir), _)) = &self.entry { + match dir.child_path() { + Some(child) => self.success = self.open(child).is_ok(), + None => self.success = false, + }; + } else { + self.success = false; + } + } + + /// Return true if the current entry is a directory. + pub fn entry_type(&self) -> bool { + match self.entry { + Some((Entry::Directory(_), _)) => true, + _ => false, + } + } + + /// Read a byte from the path buffer of the selected child. + pub fn read_child_path(&mut self) -> u8 { + match &mut self.entry { + Some((Entry::Directory(dir), _)) => dir.child_path_buffer().read(), + _ => 0, + } + } + + pub fn set_child_path(&mut self, byte: u8) { + if let Some((Entry::Directory(dir), _)) = &mut self.entry { + dir.child_path_buffer().set_pointer(byte); + } + } + + /// Return true if the selected child is a directory. + pub fn child_type(&self) -> bool { + match &self.entry { + Some((Entry::Directory(dir), _)) => match dir.child_type() { + Some(EntryType::Directory) => true, + _ => false, + } + _ => false, + } + } + + /// Read a byte from the current file. + pub fn read_byte(&mut self) -> u8 { + match &mut self.entry { + Some((Entry::File(file), _)) => file.read(), + _ => 0, + } + } + + /// Writes a byte to the currently-open file. + pub fn write_byte(&mut self, byte: u8) { + match &mut self.entry { + Some((Entry::File(file), _)) => file.write(byte), + _ => (), + } + } + + pub fn pointer(&mut self) -> u32 { + match &mut self.entry { + Some((Entry::File(file), _)) => file.pointer(), + Some((Entry::Directory(dir), _)) => dir.selected(), + _ => 0, + } + } + + pub fn commit_pointer(&mut self) { + match &mut self.entry { + Some((Entry::File(file), _)) => file.set_pointer(self.pointer_write), + Some((Entry::Directory(dir), _)) => dir.set_selected(self.pointer_write), + _ => (), + } + } + + pub fn length(&mut self) -> u32 { + match &mut self.entry { + Some((Entry::File(file), _)) => file.length(), + Some((Entry::Directory(dir), _)) => dir.length(), + _ => 0, + } + } + + pub fn commit_length(&mut self) { + match &mut self.entry { + Some((Entry::File(file), _)) => file.set_length(self.length_write), + _ => (), + } + } + + pub fn flush(&mut self) { + if let Some((Entry::File(buffered_file), _)) = &mut self.entry { + let _ = buffered_file; + } + } +} + + +impl Drop for FileDevice { + fn drop(&mut self) { + self.flush(); + } +} + + +/// Create a new file if it doesn't already exist, returning true if successful. +pub fn create_file(destination: &Path) -> bool { + if entry_exists(destination) { + false + } else { + if let Some(parent_path) = destination.parent() { + let _ = std::fs::create_dir_all(parent_path); + } + std::fs::OpenOptions::new().write(true).create_new(true) + .open(destination).is_ok() + } +} + +/// Move an entry from one location to another, returning true if successful. +pub fn move_entry(source: &Path, destination: &Path) -> bool { + if !entry_exists(source) || entry_exists(destination) { + return false; + } + std::fs::rename(source, destination).is_ok() +} + +/// Delete an entry, returning true if successful. +pub fn delete_entry(source: &Path) -> bool { + use std::fs::{remove_file, remove_dir_all}; + use std::io::ErrorKind; + + match remove_file(source) { + Ok(_) => true, + Err(error) => match error.kind() { + ErrorKind::NotFound => true, + ErrorKind::IsADirectory => match remove_dir_all(source) { + Ok(_) => true, + Err(error) => match error.kind() { + ErrorKind::NotFound => true, + _ => false, + } + } + _ => false, + } + } +} + +/// Returns true if an entry already exists at the given path. +fn entry_exists(source: &Path) -> bool { + std::fs::metadata(source).is_ok() +} diff --git a/src/devices/input_device.rs b/src/devices/input_device.rs new file mode 100644 index 0000000..83e8039 --- /dev/null +++ b/src/devices/input_device.rs @@ -0,0 +1,254 @@ +use crate::*; + +use std::collections::VecDeque; + + +pub struct InputDevice { + pub cursor: ScreenPosition, + pub x_read: u16, + pub y_read: u16, + pub h_scroll_read: i16, + pub v_scroll_read: i16, + pub h_scroll: f32, + pub v_scroll: f32, + pub pointer_buttons: u8, + pub pointer_active: bool, + + pub navigation: u8, + pub modifiers: u8, + pub characters: VecDeque<u8>, + pub gamepad_1: OwnedGamepad, + pub gamepad_2: OwnedGamepad, + pub gamepad_3: OwnedGamepad, + pub gamepad_4: OwnedGamepad, + + pub accessed: bool, + pub wake: bool, +} + + +impl Device for InputDevice { + fn read(&mut self, port: u8) -> u8 { + self.accessed = true; + match port { + 0x0 => { self.x_read = self.cursor.x; read_h!(self.x_read) }, + 0x1 => read_l!(self.cursor.x), + 0x2 => { self.y_read = self.cursor.y; read_h!(self.y_read) }, + 0x3 => read_l!(self.cursor.y), + 0x4 => { self.update_horizontal_scroll(); read_h!(self.h_scroll_read) }, + 0x5 => read_l!(self.h_scroll_read), + 0x6 => { self.update_vertical_scroll(); read_h!(self.v_scroll_read) }, + 0x7 => read_l!(self.v_scroll_read), + 0x8 => read_b!(self.pointer_active), + 0x9 => self.pointer_buttons, + 0xA => self.characters.pop_front().unwrap_or(0), + 0xB => self.modifiers, + 0xC => self.gamepad_1.state() | self.navigation, + 0xD => self.gamepad_2.state(), + 0xE => self.gamepad_3.state(), + 0xF => self.gamepad_4.state(), + _ => unreachable!(), + } + } + + fn write(&mut self, port: u8, _value: u8) -> Option<Signal> { + let signal = if self.accessed { None } else { Some(Signal::Break) }; + self.accessed = true; + match port { + 0x0 => (), + 0x1 => (), + 0x2 => (), + 0x3 => (), + 0x4 => (), + 0x5 => (), + 0x6 => (), + 0x7 => (), + 0x8 => (), + 0x9 => (), + 0xA => self.characters.clear(), + 0xB => (), + 0xC => (), + 0xD => (), + 0xE => (), + 0xF => (), + _ => unreachable!(), + }; + return signal; + } + + fn wake(&mut self) -> bool { + self.accessed = true; + std::mem::take(&mut self.wake) + } + + fn reset(&mut self) { + self.cursor = ScreenPosition::ZERO; + self.x_read = 0; + self.y_read = 0; + self.h_scroll_read = 0; + self.v_scroll_read = 0; + self.h_scroll = 0.0; + self.v_scroll = 0.0; + self.pointer_active = false; + self.pointer_buttons = 0; + + self.navigation = 0; + self.modifiers = 0; + self.characters.clear(); + self.gamepad_1.reset(); + self.gamepad_2.reset(); + self.gamepad_3.reset(); + self.gamepad_4.reset(); + + self.accessed = false; + self.wake = false; + } +} + + +impl InputDevice { + pub fn new() -> Self { + Self { + cursor: ScreenPosition::ZERO, + x_read: 0, + y_read: 0, + h_scroll_read: 0, + v_scroll_read: 0, + h_scroll: 0.0, + v_scroll: 0.0, + pointer_active: false, + pointer_buttons: 0, + + navigation: 0, + modifiers: 0, + characters: VecDeque::new(), + gamepad_1: OwnedGamepad::new(1), + gamepad_2: OwnedGamepad::new(2), + gamepad_3: OwnedGamepad::new(3), + gamepad_4: OwnedGamepad::new(4), + + accessed: false, + wake: false, + } + } + + pub fn on_gamepad_event(&mut self, event: gilrs::Event) { + if let Some(g) = self.gamepad_1.register(event.id) { + self.wake |= g.process_event(&event); return; } + if let Some(g) = self.gamepad_2.register(event.id) { + self.wake |= g.process_event(&event); return; } + if let Some(g) = self.gamepad_3.register(event.id) { + self.wake |= g.process_event(&event); return; } + if let Some(g) = self.gamepad_4.register(event.id) { + self.wake |= g.process_event(&event); return; } + } + + pub fn on_cursor_enter(&mut self) { + self.pointer_active = true; + self.wake = true; + } + + pub fn on_cursor_exit(&mut self) { + self.pointer_active = false; + self.wake = true; + } + + pub fn on_cursor_move(&mut self, position: Position) { + self.pointer_active = true; + let cursor_position = ScreenPosition { + x: position.x as i16 as u16, + y: position.y as i16 as u16, + }; + if self.cursor != cursor_position { + self.cursor = cursor_position; + self.wake = true; + } + } + + pub fn on_mouse_button(&mut self, button: MouseButton, action: Action) { + let mask = match button { + MouseButton::Left => 0x80, + MouseButton::Right => 0x40, + MouseButton::Middle => 0x20, + _ => return, + }; + let pointer_buttons = match action { + Action::Pressed => self.pointer_buttons | mask, + Action::Released => self.pointer_buttons & !mask, + }; + if self.pointer_buttons != pointer_buttons { + self.pointer_buttons = pointer_buttons; + self.wake = true; + } + } + + pub fn on_horizontal_scroll(&mut self, delta: f32) { + self.h_scroll += delta; + self.h_scroll = self.h_scroll.clamp(-32768.0, 32767.0); + } + + pub fn on_vertical_scroll(&mut self, delta: f32) { + self.v_scroll += delta; + self.v_scroll = self.v_scroll.clamp(i16::MIN as f32, i16::MAX as f32); + } + + pub fn update_horizontal_scroll(&mut self) { + self.h_scroll_read = self.h_scroll.trunc() as i16; + self.h_scroll -= self.h_scroll.trunc(); + } + + pub fn update_vertical_scroll(&mut self) { + self.v_scroll_read = self.v_scroll.trunc() as i16; + self.v_scroll -= self.v_scroll.trunc(); + } + + pub fn on_character(&mut self, character: char) { + let character = match character { + '\r' => '\n', + _ => character, + }; + let mut bytes = [0; 4]; + let string = character.encode_utf8(&mut bytes); + for byte in string.bytes() { + self.characters.push_back(byte); + } + self.wake = true; + } + + pub fn on_keypress(&mut self, key: KeyCode, action: Action) { + let shift = self.modifiers & 0x40 != 0; + let mask = match key { + KeyCode::ArrowUp => 0x80, // up + KeyCode::ArrowDown => 0x40, // down + KeyCode::ArrowLeft => 0x20, // left + KeyCode::ArrowRight => 0x10, // right + KeyCode::Enter => 0x08, // confirm + KeyCode::Escape => 0x04, // cancel + KeyCode::Tab => match shift { // shift + false => 0x02, // next + true => 0x01 // previous + }, + _ => return, + }; + let navigation = match action { + Action::Pressed => self.navigation | mask, + Action::Released => self.navigation & !mask, + }; + if self.navigation != navigation { + self.navigation = navigation; + self.wake = true; + } + } + + pub fn on_modifier(&mut self, state: ModifiersState) { + let mut modifiers = 0; + if state.control_key() { modifiers |= 0x80 } + if state.shift_key() { modifiers |= 0x40 } + if state.alt_key() { modifiers |= 0x20 } + if state.super_key() { modifiers |= 0x10 } + if self.modifiers != modifiers { + self.modifiers = modifiers; + self.wake = true; + } + } +} diff --git a/src/devices/math_device.rs b/src/devices/math_device.rs new file mode 100644 index 0000000..e7043b9 --- /dev/null +++ b/src/devices/math_device.rs @@ -0,0 +1,199 @@ +use crate::*; + +const ANGLE_SCALE: f64 = 10430.378350470453; // 65536 / 2Ï€ + + +pub struct MathDevice { + pub x: u16, + pub y: u16, + pub r: u16, + pub t: u16, + pub x_read: Option<u16>, + pub y_read: Option<u16>, + pub r_read: Option<u16>, + pub t_read: Option<u16>, + pub prod: Option<(u16, u16)>, // (low, high) + pub quot: Option<u16>, + pub rem: Option<u16>, +} + + +impl Device for MathDevice { + fn read(&mut self, port: u8) -> u8 { + match port { + 0x0 => read_h!(self.x()), + 0x1 => read_l!(self.x()), + 0x2 => read_h!(self.y()), + 0x3 => read_l!(self.y()), + 0x4 => read_h!(self.r()), + 0x5 => read_l!(self.r()), + 0x6 => read_h!(self.t()), + 0x7 => read_l!(self.t()), + 0x8 => read_h!(self.prod().1), + 0x9 => read_l!(self.prod().1), + 0xA => read_h!(self.prod().0), + 0xB => read_l!(self.prod().0), + 0xC => read_h!(self.quot()), + 0xD => read_l!(self.quot()), + 0xE => read_h!(self.rem()), + 0xF => read_l!(self.rem()), + _ => unreachable!(), + } + } + + fn write(&mut self, port: u8, value: u8) -> Option<Signal> { + match port { + 0x0 => { write_h!(self.x, value); self.clear_polar(); }, + 0x1 => { write_l!(self.x, value); self.clear_polar(); }, + 0x2 => { write_h!(self.y, value); self.clear_polar(); }, + 0x3 => { write_l!(self.y, value); self.clear_polar(); }, + 0x4 => { write_h!(self.r, value); self.clear_cartesian(); }, + 0x5 => { write_l!(self.r, value); self.clear_cartesian(); }, + 0x6 => { write_h!(self.t, value); self.clear_cartesian(); }, + 0x7 => { write_l!(self.t, value); self.clear_cartesian(); }, + 0x8 => (), + 0x9 => (), + 0xA => (), + 0xB => (), + 0xC => (), + 0xD => (), + 0xE => (), + 0xF => (), + _ => unreachable!(), + }; + return None; + } + + fn wake(&mut self) -> bool { + false + } + + fn reset(&mut self) { + self.x = 0; + self.y = 0; + self.r = 0; + self.t = 0; + self.clear_cartesian(); + self.clear_polar(); + } +} + + +impl MathDevice { + pub fn new() -> Self { + Self { + x: 0, + y: 0, + r: 0, + t: 0, + x_read: None, + y_read: None, + r_read: None, + t_read: None, + prod: None, + quot: None, + rem: None, + } + } + + pub fn clear_cartesian(&mut self) { + self.x_read = None; + self.y_read = None; + } + + pub fn clear_polar(&mut self) { + self.r_read = None; + self.t_read = None; + self.prod = None; + self.quot = None; + self.rem = None; + } + + pub fn x(&mut self) -> u16 { + match self.x_read { + Some(x) => x, + None => { + let r = self.r as f64; + let t = self.t as f64; + let angle = t / ANGLE_SCALE; + let x = angle.cos() * r; + self.x_read = match x > (i16::MIN as f64) && x < (i16::MAX as f64) { + true => Some(x as i16 as u16), + false => Some(0), + }; + self.x_read.unwrap() + } + } + } + + pub fn y(&mut self) -> u16 { + match self.y_read { + Some(y) => y, + None => { + let r = self.r as f64; + let t = self.t as f64; + let angle = t / ANGLE_SCALE; + let y = angle.sin() * r; + self.y_read = match y > (i16::MIN as f64) && y < (i16::MAX as f64) { + true => Some(y as i16 as u16), + false => Some(0), + }; + self.y_read.unwrap() + } + } + } + + pub fn r(&mut self) -> u16 { + match self.r_read { + Some(r) => r, + None => { + let sum = (self.x as f64).powi(2) + (self.y as f64).powi(2); + self.r_read = Some(sum.sqrt() as u16); + self.r_read.unwrap() + } + } + } + + pub fn t(&mut self) -> u16 { + match self.t_read { + Some(t) => t, + None => { + let x = self.x as i16 as f64; + let y = self.x as i16 as f64; + let angle = f64::atan2(y, x) * ANGLE_SCALE; + self.t_read = Some(angle as i16 as u16); + self.t_read.unwrap() + } + } + } + + pub fn prod(&mut self) -> (u16, u16) { + match self.prod { + Some(prod) => prod, + None => { + self.prod = Some(self.x.widening_mul(self.y)); + self.prod.unwrap() + } + } + } + + pub fn quot(&mut self) -> u16 { + match self.quot { + Some(quot) => quot, + None => { + self.quot = Some(self.x.checked_div(self.y).unwrap_or(0)); + self.quot.unwrap() + } + } + } + + pub fn rem(&mut self) -> u16 { + match self.rem { + Some(rem) => rem, + None => { + self.rem = Some(self.x.checked_rem(self.y).unwrap_or(0)); + self.rem.unwrap() + } + } + } +} diff --git a/src/devices/memory_device.rs b/src/devices/memory_device.rs new file mode 100644 index 0000000..d116ca7 --- /dev/null +++ b/src/devices/memory_device.rs @@ -0,0 +1,186 @@ +use crate::*; + +use std::cmp::min; + + +type Page = [u8; 256]; + + +pub struct MemoryDevice { + pub limit: u16, // maximum allocateable number of pages + pub pages: Vec<Page>, // all allocated pages + pub count_write: u16, // number of pages requested by program + pub count: usize, // number of pages allocated for use + pub copy_write: u16, + pub head_1: HeadAddress, + pub head_2: HeadAddress, +} + + +impl Device for MemoryDevice { + fn read(&mut self, port: u8) -> u8 { + match port { + 0x0 => read_h!(self.count), + 0x1 => read_l!(self.count), + 0x2 => read_h!(self.head_1.page), + 0x3 => read_l!(self.head_1.page), + 0x4 => read_h!(self.head_1.address), + 0x5 => read_l!(self.head_1.address), + 0x6 => self.read_head_1(), + 0x7 => self.read_head_1(), + 0x8 => 0x00, + 0x9 => 0x00, + 0xA => read_h!(self.head_2.page), + 0xB => read_l!(self.head_2.page), + 0xC => read_h!(self.head_2.address), + 0xD => read_l!(self.head_2.address), + 0xE => self.read_head_2(), + 0xF => self.read_head_2(), + _ => unreachable!(), + } + } + + fn write(&mut self, port: u8, value: u8) -> Option<Signal> { + match port { + 0x0 => write_h!(self.count_write, value), + 0x1 => { write_l!(self.count_write, value); self.allocate(); }, + 0x2 => write_h!(self.head_1.page, value), + 0x3 => write_l!(self.head_1.page, value), + 0x4 => write_h!(self.head_1.address, value), + 0x5 => write_l!(self.head_1.address, value), + 0x6 => self.write_head_1(value), + 0x7 => self.write_head_1(value), + 0x8 => write_h!(self.copy_write, value), + 0x9 => { write_l!(self.copy_write, value); self.copy(); }, + 0xA => write_h!(self.head_2.page, value), + 0xB => write_l!(self.head_2.page, value), + 0xC => write_h!(self.head_2.address, value), + 0xD => write_l!(self.head_2.address, value), + 0xE => self.write_head_2(value), + 0xF => self.write_head_2(value), + _ => unreachable!(), + }; + return None; + } + + fn wake(&mut self) -> bool { + false + } + + fn reset(&mut self) { + self.pages.clear(); + self.count_write = 0; + self.count = 0; + self.copy_write = 0; + self.head_1.reset(); + self.head_2.reset(); + } +} + + +impl MemoryDevice { + pub fn new() -> Self { + Self { + limit: u16::MAX, + pages: Vec::new(), + count_write: 0, + count: 0, + copy_write: 0, + head_1: HeadAddress::new(), + head_2: HeadAddress::new(), + } + } + + pub fn read_head_1(&mut self) -> u8 { + let (page_i, byte_i) = self.head_1.get_indices(); + self.read_byte(page_i, byte_i) + } + + pub fn read_head_2(&mut self) -> u8 { + let (page_i, byte_i) = self.head_2.get_indices(); + self.read_byte(page_i, byte_i) + } + + fn read_byte(&self, page_i: usize, byte_i: usize) -> u8 { + match self.pages.get(page_i) { + Some(page) => page[byte_i], + None => 0, + } + } + + pub fn write_head_1(&mut self, value: u8) { + let (page_i, byte_i) = self.head_1.get_indices(); + self.write_byte(page_i, byte_i, value); + } + + pub fn write_head_2(&mut self, value: u8) { + let (page_i, byte_i) = self.head_2.get_indices(); + self.write_byte(page_i, byte_i, value); + } + + fn write_byte(&mut self, page_i: usize, byte_i: usize, value: u8) { + match self.pages.get_mut(page_i) { + Some(page) => page[byte_i] = value, + None => if page_i < self.count { + self.pages.resize(page_i + 1, [0; 256]); + self.pages[page_i][byte_i] = value; + } + } + } + + pub fn allocate(&mut self) { + self.count = min(self.count_write, self.limit) as usize; + // Defer allocation of new pages. + self.pages.truncate(self.count as usize); + } + + pub fn copy(&mut self) { + let src = self.head_2.page as usize; + let dest = self.head_1.page as usize; + let n = self.copy_write as usize; + + // Pre-allocate destination pages as needed. + let allocate = min(dest + n, self.count); + if allocate > self.pages.len() { + self.pages.resize(allocate, [0; 256]); + } + + for i in 0..n { + let src_page = match self.pages.get(src + i) { + Some(src_page) => src_page.to_owned(), + None => [0; 256], + }; + match self.pages.get_mut(dest + i) { + Some(dest) => *dest = src_page, + None => break, + }; + } + } +} + + +pub struct HeadAddress { + pub page: u16, + pub address: u16, +} + +impl HeadAddress { + pub fn new() -> Self { + Self { + page: 0, + address: 0, + } + } + + pub fn reset(&mut self) { + self.page = 0; + self.address = 0; + } + + pub fn get_indices(&mut self) -> (usize, usize) { + let page_i = (self.page + (self.address / 256)) as usize; + let byte_i = (self.address % 256) as usize; + self.address = self.address.wrapping_add(1); + (page_i, byte_i) + } +} diff --git a/src/devices/mod.rs b/src/devices/mod.rs new file mode 100644 index 0000000..aa98a49 --- /dev/null +++ b/src/devices/mod.rs @@ -0,0 +1,17 @@ +mod system_device; +mod memory_device; +mod math_device; +mod clock_device; +mod input_device; +mod screen_device; +mod stream_device; +mod file_device; + +pub use system_device::*; +pub use memory_device::*; +pub use math_device::*; +pub use clock_device::*; +pub use input_device::*; +pub use screen_device::*; +pub use stream_device::*; +pub use file_device::*; diff --git a/src/devices/screen_device.rs b/src/devices/screen_device.rs new file mode 100644 index 0000000..483bcca --- /dev/null +++ b/src/devices/screen_device.rs @@ -0,0 +1,450 @@ +use crate::*; + +use geometry::*; +use phosphor::*; + + +pub type Sprite = [[u8; 8]; 8]; + +#[derive(Clone, Copy)] +pub enum Layer { Fg, Bg } + + +pub struct ScreenDevice { + /// Each byte represents a screen pixel, left-to-right and top-to-bottom. + // Only the bottom four bits of each byte are used. + pub fg: Vec<u8>, + pub bg: Vec<u8>, + pub dirty: bool, + + pub cursor: ScreenPosition, + pub vector: ScreenPosition, + + pub dimensions: ScreenDimensions, + pub dirty_dimensions: bool, + pub width_write: u16, + pub height_write: u16, + pub fixed_width: Option<u16>, + pub fixed_height: Option<u16>, + + pub palette_write: u16, + pub palette: [Colour; 16], + pub sprite_colours: u16, + pub sprite: SpriteBuffer, + + pub accessed: bool, + pub wake: bool, +} + + +impl HasDimensions<u16> for ScreenDevice { + fn dimensions(&self) -> ScreenDimensions { + self.dimensions + } +} + + +impl Device for ScreenDevice { + fn read(&mut self, port: u8) -> u8 { + self.accessed = true; + match port { + 0x0 => read_h!(self.cursor.x), + 0x1 => read_l!(self.cursor.x), + 0x2 => read_h!(self.cursor.y), + 0x3 => read_l!(self.cursor.y), + 0x4 => read_h!(self.dimensions.width), + 0x5 => read_l!(self.dimensions.width), + 0x6 => read_h!(self.dimensions.height), + 0x7 => read_l!(self.dimensions.height), + 0x8 => 0, + 0x9 => 0, + 0xA => 0, + 0xB => 0, + 0xC => 0, + 0xD => 0, + 0xE => 0, + 0xF => 0, + _ => unreachable!(), + } + } + + fn write(&mut self, port: u8, value: u8) -> Option<Signal> { + let signal = if self.accessed { None } else { Some(Signal::Break) }; + self.accessed = true; + match port { + 0x0 => write_h!(self.cursor.x, value), + 0x1 => write_l!(self.cursor.x, value), + 0x2 => write_h!(self.cursor.y, value), + 0x3 => write_l!(self.cursor.y, value), + 0x4 => write_h!(self.width_write, value), + 0x5 => { write_l!(self.width_write, value); self.resize_width() } + 0x6 => write_h!(self.height_write, value), + 0x7 => { write_l!(self.height_write, value); self.resize_height() } + 0x8 => write_h!(self.palette_write, value), + 0x9 => { write_l!(self.palette_write, value); self.set_palette() } + 0xA => write_h!(self.sprite_colours, value), + 0xB => write_l!(self.sprite_colours, value), + 0xC => self.sprite.push_byte(value), + 0xD => self.sprite.push_byte(value), + 0xE => self.draw_dispatch(value), + 0xF => self.move_cursor(value), + _ => unreachable!(), + }; + return signal; + } + + fn wake(&mut self) -> bool { + self.accessed = true; + std::mem::take(&mut self.wake) + } + + fn reset(&mut self) { + self.fg.clear(); + self.bg.clear(); + self.dirty = false; + + self.cursor = ScreenPosition::ZERO; + self.vector = ScreenPosition::ZERO; + + self.dirty_dimensions = true; + self.width_write = 0; + self.height_write = 0; + self.fixed_width = None; + self.fixed_height = None; + + self.palette_write = 0; + self.palette = [ + Colour::grey(0x00), Colour::grey(0xFF), Colour::grey(0x55), Colour::grey(0xAA), + Colour::grey(0x00), Colour::grey(0xFF), Colour::grey(0x55), Colour::grey(0xAA), + Colour::grey(0x00), Colour::grey(0xFF), Colour::grey(0x55), Colour::grey(0xAA), + Colour::grey(0x00), Colour::grey(0xFF), Colour::grey(0x55), Colour::grey(0xAA), + ]; + self.sprite_colours = 0; + self.sprite = SpriteBuffer::new(); + + self.accessed = false; + self.wake = false; + } +} + + +impl ScreenDevice { + pub fn new(config: &EmulatorConfig) -> Self { + let area = config.dimensions.area_usize(); + + Self { + fg: vec![0; area], + bg: vec![0; area], + dirty: false, + + cursor: ScreenPosition::ZERO, + vector: ScreenPosition::ZERO, + + dimensions: config.dimensions, + dirty_dimensions: true, + width_write: 0, + height_write: 0, + fixed_width: None, + fixed_height: None, + + palette_write: 0, + palette: [Colour::BLACK; 16], + sprite_colours: 0, + sprite: SpriteBuffer::new(), + + accessed: false, + wake: false, + } + } + + /// Resize screen to match window dimensions. + pub fn resize(&mut self, dimensions: phosphor::Dimensions) { + // Replace dimensions with fixed dimensions. + let screen_dimensions = ScreenDimensions { + width: match self.fixed_width { + Some(fixed_width) => fixed_width, + None => dimensions.width as u16, + }, + height: match self.fixed_height { + Some(fixed_height) => fixed_height, + None => dimensions.height as u16, + }, + }; + let old_dimensions = self.dimensions; + if self.dimensions != screen_dimensions { + self.dimensions = screen_dimensions; + self.resize_layers(old_dimensions); + self.wake = true; + } + } + + /// Internal resize. + fn resize_width(&mut self) { + self.fixed_width = Some(self.width_write); + self.dirty_dimensions = true; + let old_dimensions = self.dimensions; + if self.dimensions.width != self.width_write { + self.dimensions.width = self.width_write; + self.resize_layers(old_dimensions); + } + } + + /// Internal resize. + fn resize_height(&mut self) { + self.fixed_height = Some(self.height_write); + self.dirty_dimensions = true; + let old_dimensions = self.dimensions; + if self.dimensions.height != self.height_write { + self.dimensions.height = self.height_write; + self.resize_layers(old_dimensions); + } + } + + fn resize_layers(&mut self, old_dimensions: ScreenDimensions) { + use std::cmp::{min, Ordering}; + + let old_width = old_dimensions.width as usize; + let old_height = old_dimensions.height as usize; + let new_width = self.dimensions.width as usize; + let new_height = self.dimensions.height as usize; + let new_area = self.dimensions.area_usize(); + let y_range = 0..min(old_height, new_height); + let new_colour = match self.fg.last() { + None | Some(0) => *self.bg.last().unwrap_or(&0), + Some(colour) => *colour, + }; + + match new_width.cmp(&old_width) { + Ordering::Less => { + for y in y_range { + let src = y * old_width; + let dest = y * new_width; + let len = new_width; + self.fg.copy_within(src..src+len, dest); + self.bg.copy_within(src..src+len, dest); + } + self.fg.resize(new_area, 0); + self.bg.resize(new_area, new_colour); + }, + Ordering::Greater => { + self.fg.resize(new_area, 0); + self.bg.resize(new_area, new_colour); + for y in y_range.rev() { + let src = y * old_width; + let dest = y * new_width; + let len = old_width; + self.fg.copy_within(src..src+len, dest); + self.bg.copy_within(src..src+len, dest); + self.fg[dest+len..dest+new_width].fill(0); + self.bg[dest+len..dest+new_width].fill(new_colour); + } + }, + Ordering::Equal => { + self.fg.resize(new_area, 0); + self.bg.resize(new_area, new_colour); + }, + }; + + self.dirty = true; + } + + pub fn set_palette(&mut self) { + let i = (self.palette_write >> 12 ) as usize; + let r = (self.palette_write >> 8 & 0xF) as u8 * 17; + let g = (self.palette_write >> 4 & 0xF) as u8 * 17; + let b = (self.palette_write & 0xF) as u8 * 17; + let colour = Colour::from_rgb(r, g, b); + if self.palette[i] != colour { + self.palette[i] = colour; + self.dirty = true; + } + } + + pub fn draw_dispatch(&mut self, draw: u8) { + match draw >> 4 { + 0x0 => self.op_draw_pixel(Layer::Bg, draw), + 0x1 => self.op_draw_sprite(Layer::Bg, draw), + 0x2 => self.op_fill_layer(Layer::Bg, draw), + 0x3 => self.op_draw_sprite(Layer::Bg, draw), + 0x4 => self.op_draw_line(Layer::Bg, draw), + 0x5 => self.op_draw_line(Layer::Bg, draw), + 0x6 => self.op_draw_rect(Layer::Bg, draw), + 0x7 => self.op_draw_rect(Layer::Bg, draw), + 0x8 => self.op_draw_pixel(Layer::Fg, draw), + 0x9 => self.op_draw_sprite(Layer::Fg, draw), + 0xA => self.op_fill_layer(Layer::Fg, draw), + 0xB => self.op_draw_sprite(Layer::Fg, draw), + 0xC => self.op_draw_line(Layer::Fg, draw), + 0xD => self.op_draw_line(Layer::Fg, draw), + 0xE => self.op_draw_rect(Layer::Fg, draw), + 0xF => self.op_draw_rect(Layer::Fg, draw), + _ => unreachable!(), + } + self.vector = self.cursor; + self.dirty = true; + } + + pub fn move_cursor(&mut self, value: u8) { + let distance = (value & 0x3F) as u16; + match value >> 6 { + 0b00 => self.cursor.x = self.cursor.x.wrapping_add(distance), + 0b01 => self.cursor.y = self.cursor.y.wrapping_add(distance), + 0b10 => self.cursor.x = self.cursor.x.wrapping_sub(distance), + 0b11 => self.cursor.y = self.cursor.y.wrapping_sub(distance), + _ => unreachable!(), + }; + } + + /// Colour must already be masked by 0xF. + pub fn draw_pixel(&mut self, layer: Layer, x: u16, y: u16, colour: u8) { + if x < self.dimensions.width && y < self.dimensions.height { + let index = x as usize + (self.dimensions.width as usize * y as usize); + match layer { + Layer::Fg => self.fg[index] = colour, + Layer::Bg => self.bg[index] = colour, + }; + } + } + + fn op_draw_pixel(&mut self, layer: Layer, draw: u8) { + self.draw_pixel(layer, self.cursor.x, self.cursor.y, draw & 0xF); + } + + fn op_fill_layer(&mut self, layer: Layer, draw: u8) { + match layer { + Layer::Fg => self.fg.fill(draw & 0xF), + Layer::Bg => self.bg.fill(draw & 0xF), + } + } + + fn op_draw_sprite(&mut self, layer: Layer, draw: u8) { + let sprite = match draw & 0x20 != 0 { + true => self.sprite.read_2bit_sprite(draw), + false => self.sprite.read_1bit_sprite(draw), + }; + let colours = [ + (self.sprite_colours >> 12 & 0x000F) as u8, + (self.sprite_colours >> 8 & 0x000F) as u8, + (self.sprite_colours >> 4 & 0x000F) as u8, + (self.sprite_colours & 0x000F) as u8, + ]; + let cx = self.cursor.x; + let cy = self.cursor.y; + + if draw & 0x08 != 0 { + // Draw sprite with transparent background + for y in 0..8 { + for x in 0..8 { + let index = sprite[y as usize][x as usize] as usize; + if index != 0 { + let px = cx.wrapping_add(x); + let py = cy.wrapping_add(y); + self.draw_pixel(layer, px, py, colours[index]); + } + } + } + } else { + // Draw sprite with opaque background + for y in 0..8 { + for x in 0..8 { + let index = sprite[y as usize][x as usize] as usize; + let px = cx.wrapping_add(x); + let py = cy.wrapping_add(y); + self.draw_pixel(layer, px, py, colours[index]); + } + } + } + } + + fn op_draw_line(&mut self, layer: Layer, draw: u8) { + let mut x: i16 = self.cursor.x as i16; + let mut y: i16 = self.cursor.y as i16; + let x_end: i16 = self.vector.x as i16; + let y_end: i16 = self.vector.y as i16; + + let dx: i32 = ((x_end as i32) - (x as i32)).abs(); + let dy: i32 = -((y_end as i32) - (y as i32)).abs(); + let sx: i16 = if x < x_end { 1 } else { -1 }; + let sy: i16 = if y < y_end { 1 } else { -1 }; + let mut e1: i32 = dx + dy; + + if draw & 0x10 != 0 { + // Draw 1-bit textured line. + let sprite = self.sprite.read_1bit_sprite(draw); + let c1 = (self.sprite_colours >> 8 & 0xF) as u8; + let c0 = (self.sprite_colours >> 12 & 0xF) as u8; + let opaque = draw & 0x08 == 0; + loop { + let sprite_pixel = sprite[(y as usize) % 8][(x as usize) % 8]; + if sprite_pixel != 0 { self.draw_pixel(layer, x as u16, y as u16, c1); } + else if opaque { self.draw_pixel(layer, x as u16, y as u16, c0); } + if x == x_end && y == y_end { break; } + let e2 = e1 << 1; + if e2 >= dy { e1 += dy; x += sx; } + if e2 <= dx { e1 += dx; y += sy; } + } + } else { + // Draw solid line. + let colour = draw & 0xF; + loop { + self.draw_pixel(layer, x as u16, y as u16, colour); + if x == x_end && y == y_end { break; } + let e2 = e1 << 1; + if e2 >= dy { e1 += dy; x += sx; } + if e2 <= dx { e1 += dx; y += sy; } + } + } + } + + fn op_draw_rect(&mut self, layer: Layer, draw: u8) { + macro_rules! clamp { + ($v:expr, $max:expr) => { + if $v > 0x7FFF { 0 } else if $v > $max { $max } else { $v } + }; + } + macro_rules! out_of_bounds { + ($axis:ident, $max:expr) => {{ + let c = self.cursor.$axis; + let v = self.vector.$axis; + c >= $max && v >= $max && (c >= 0x8000) == (v >= 0x8000) + }}; + } + + let out_of_bounds_x = out_of_bounds!(x, self.dimensions.width); + let out_of_bounds_y = out_of_bounds!(y, self.dimensions.height); + if out_of_bounds_x || out_of_bounds_y { return; } + + // Get bounding box. + let mut l = clamp!(self.vector.x, self.dimensions.width -1); + let mut r = clamp!(self.cursor.x, self.dimensions.width -1); + let mut t = clamp!(self.vector.y, self.dimensions.height -1); + let mut b = clamp!(self.cursor.y, self.dimensions.height -1); + if l > r { std::mem::swap(&mut l, &mut r) }; + if t > b { std::mem::swap(&mut t, &mut b) }; + + if draw & 0x10 != 0 { + // Draw 1-bit textured rectangle. + let sprite = self.sprite.read_1bit_sprite(draw); + let c1 = (self.sprite_colours >> 8 & 0xF) as u8; + let c0 = (self.sprite_colours >> 12 & 0xF) as u8; + let opaque = draw & 0x08 == 0; + for y in t..=b { + for x in l..=r { + let sprite_colour = sprite[(y as usize) % 8][(x as usize) % 8]; + if sprite_colour != 0 { self.draw_pixel(layer, x, y, c1); } + else if opaque { self.draw_pixel(layer, x, y, c0); } + } + } + } else { + // Draw solid rectangle. + let colour = draw & 0xF; + for y in t..=b { + for x in l..=r { + self.draw_pixel(layer, x, y, colour); + } + } + } + } +} + + diff --git a/src/devices/stream_device.rs b/src/devices/stream_device.rs new file mode 100644 index 0000000..1e67166 --- /dev/null +++ b/src/devices/stream_device.rs @@ -0,0 +1,239 @@ +use crate::*; + +use std::collections::VecDeque; +use std::io::{BufRead, Stdout, Write}; +use std::sync::mpsc::{self, TryRecvError}; + + +pub struct StreamDevice { + /// True if a source is connected to stdin. + stdin_connected: bool, + /// True if a transmission is in progress. + stdin_control: bool, + stdin_rx: mpsc::Receiver<Vec<u8>>, + /// Bytes received in the current transmission. + stdin_queue: VecDeque<u8>, + /// Bytes received since stdin end-of-transmission. + stdin_excess: VecDeque<u8>, + + stdout: Stdout, + /// True if a sink is connected to stdout. + stdout_connected: bool, + + /// True if stdin is transmission-encoded. + decode_stdin: bool, + /// True if stdout should be transmission-encoded. + encode_stdout: bool, + /// Half-byte buffer for decoding stdin. + decode_buffer: Option<u8>, + + wake: bool, +} + + +impl Device for StreamDevice { + fn read(&mut self, port: u8) -> u8 { + match port { + 0x0 => read_b!(self.stdin_connected), + 0x1 => read_b!(self.stdout_connected), + 0x2 => read_b!(self.stdin_control), + 0x3 => read_b!(true), + 0x4 => self.stdin_length(), + 0x5 => read_b!(true), + 0x6 => self.stdin_read(), + 0x7 => self.stdin_read(), + 0x8 => todo!(), + 0x9 => todo!(), + 0xA => todo!(), + 0xB => todo!(), + 0xC => todo!(), + 0xD => todo!(), + 0xE => todo!(), + 0xF => todo!(), + _ => unreachable!(), + } + } + + fn write(&mut self, port: u8, value: u8) -> Option<Signal> { + match port { + 0x0 => (), + 0x1 => (), + 0x2 => self.stdin_start_transmission(), + 0x3 => self.stdout_end_transmission(), + 0x4 => (), + 0x5 => (), + 0x6 => self.stdout_write(value), + 0x7 => self.stdout_write(value), + 0x8 => todo!(), + 0x9 => todo!(), + 0xA => todo!(), + 0xB => todo!(), + 0xC => todo!(), + 0xD => todo!(), + 0xE => todo!(), + 0xF => todo!(), + _ => unreachable!(), + }; + return None; + } + + fn wake(&mut self) -> bool { + self.fetch_stdin_data(); + std::mem::take(&mut self.wake) + } + + fn reset(&mut self) { + todo!() + } +} + + +impl StreamDevice { + pub fn new(config: &EmulatorConfig) -> Self { + // Spawn a thread to enable non-blocking reads of stdin. + let (stdin_tx, stdin_rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || loop { + let mut stdin = std::io::stdin().lock(); + match stdin.fill_buf() { + Ok(buf) if !buf.is_empty() => { + let length = buf.len(); + stdin_tx.send(buf.to_vec()).unwrap(); + stdin.consume(length); + } + _ => break, + }; + }); + + Self { + stdin_connected: true, + stdin_control: false, + stdin_rx, + stdin_queue: VecDeque::new(), + stdin_excess: VecDeque::new(), + + stdout: std::io::stdout(), + stdout_connected: true, + + decode_stdin: config.decode_stdin, + encode_stdout: config.encode_stdout, + decode_buffer: None, + + wake: true, + } + } + + pub fn stdin_length(&mut self) -> u8 { + self.fetch_stdin_data(); + self.stdin_queue.len().try_into().unwrap_or(u8::MAX) + } + + /// Start a transmission on stdin. + pub fn stdin_start_transmission(&mut self) { + self.stdin_control = true; + } + + pub fn stdin_read(&mut self) -> u8 { + self.fetch_stdin_data(); + self.stdin_queue.pop_front().unwrap_or(0) + } + + pub fn stdout_write(&mut self, value: u8) { + macro_rules! hex { + ($value:expr) => { match $value { + 0x0..=0x9 => $value + b'0', + 0xA..=0xF => $value - 0x0A + b'A', + _ => unreachable!("Cannot encode value as hex digit: 0x{:02X}", $value), + } }; + } + if self.encode_stdout { + let encoded = [hex!(value >> 4), hex!(value & 0xF), b' ']; + self.stdout_write_raw(&encoded); + } else { + self.stdout_write_raw(&[value]); + }; + } + + fn stdout_write_raw(&mut self, bytes: &[u8]) { + if let Err(_) = self.stdout.write_all(bytes) { + if self.stdout_connected { + self.stdout_connected = false; + self.wake = true; // wake because stdout was disconnected. + } + } + } + + /// End the current transmission on stdout. + pub fn stdout_end_transmission(&mut self) { + self.stdout_write_raw(&[b'\n']); + } + + /// Fetch all pending data from stdin. + pub fn fetch_stdin_data(&mut self) { + while self.stdin_control { + match self.stdin_excess.pop_front() { + Some(byte) => self.fetch_byte(byte), + None => break, + } + } + loop { + match self.stdin_rx.try_recv() { + Ok(tx) => { + for byte in tx { + match self.stdin_control { + true => self.fetch_byte(byte), + false => self.stdin_excess.push_back(byte), + } + } + } + Err(TryRecvError::Empty) => { + break; + } + Err(TryRecvError::Disconnected) => { + self.stdin_control = false; + if self.stdin_connected { + self.stdin_connected = false; + self.wake = true; // wake because stdin was disconnected. + } + break; + } + } + } + } + + fn fetch_byte(&mut self, byte: u8) { + if self.decode_stdin { + let decoded = match byte { + b'0'..=b'9' => byte - b'0', + b'a'..=b'f' => byte - b'a' + 0x0A, + b'A'..=b'F' => byte - b'A' + 0x0A, + b'\n' => { + self.decode_buffer = None; + self.stdin_control = false; + self.wake = true; // wake because a transmission ended. + return; + }, + _ => return, + }; + if let Some(high) = std::mem::take(&mut self.decode_buffer) { + self.stdin_queue.push_back((high << 4) | decoded); + self.wake = true; // wake because a byte was received. + } else { + self.decode_buffer = Some(decoded); + } + } else { + self.stdin_queue.push_back(byte); + self.wake = true; // wake because a byte was received. + } + } + + pub fn flush(&mut self) { + let _ = self.stdout.flush(); + } +} + + +impl Drop for StreamDevice { + fn drop(&mut self) { + self.flush(); + } +} diff --git a/src/devices/system_device.rs b/src/devices/system_device.rs new file mode 100644 index 0000000..097c616 --- /dev/null +++ b/src/devices/system_device.rs @@ -0,0 +1,119 @@ +use crate::*; + + +pub struct SystemDevice { + /// Name and version of this system. + pub name: StringBuffer, + /// Authors of this system. + pub authors: StringBuffer, + /// Mask of all devices permitted to wake from sleep. + pub wake_mask: u16, + /// Slot number of device that most recently woke the system. + pub wake_slot: u8, + /// True if the system has been put to sleep. + pub asleep: bool, + /// Mask of all connected devices. + pub connected_devices: u16, + /// Name of the first custom device. + pub custom1: StringBuffer, + /// Name of the second custom device. + pub custom2: StringBuffer, + /// Name of the third custom device. + pub custom3: StringBuffer, + /// Name of the fourth custom device. + pub custom4: StringBuffer, +} + + +impl Device for SystemDevice { + fn read(&mut self, port: u8) -> u8 { + match port { + 0x0 => 0x00, + 0x1 => 0x00, + 0x2 => self.wake_slot, + 0x3 => 0x00, + 0x4 => self.custom1.read(), + 0x5 => self.custom2.read(), + 0x6 => self.custom3.read(), + 0x7 => self.custom4.read(), + 0x8 => self.name.read(), + 0x9 => self.authors.read(), + 0xA => 0x00, + 0xB => 0x00, + 0xC => 0x00, + 0xD => 0x00, + 0xE => read_h!(self.connected_devices), + 0xF => read_l!(self.connected_devices), + _ => unreachable!(), + } + } + + fn write(&mut self, port: u8, value: u8) -> Option<Signal> { + match port { + 0x0 => write_h!(self.wake_mask, value), + 0x1 => { + write_l!(self.wake_mask, value); + self.asleep = true; + return Some(Signal::Sleep); + } + 0x2 => (), + 0x3 => match value { + 0 => return Some(Signal::Reset), + _ => return Some(Signal::Fork), + } + 0x4 => self.custom1.restart(), + 0x5 => self.custom2.restart(), + 0x6 => self.custom3.restart(), + 0x7 => self.custom4.restart(), + 0x8 => self.name.restart(), + 0x9 => self.authors.restart(), + 0xA => (), + 0xB => (), + 0xC => (), + 0xD => (), + 0xE => (), + 0xF => (), + _ => unreachable!(), + }; + return None; + } + + fn wake(&mut self) -> bool { + true + } + + fn reset(&mut self) { + self.wake_mask = 0; + self.wake_slot = 0; + self.custom1.restart(); + self.custom2.restart(); + self.custom3.restart(); + self.custom4.restart(); + self.name.restart(); + self.authors.restart(); + } +} + + +impl SystemDevice { + pub fn new(connected_devices: u16) -> Self { + let pkg_name = env!("CARGO_PKG_NAME"); + let pkg_version = env!("CARGO_PKG_VERSION"); + let pkg_authors = env!("CARGO_PKG_AUTHORS"); + let name = format!("{pkg_name}/{pkg_version}"); + let authors = pkg_authors.replace(':', "\n"); + + Self { + name: StringBuffer::from_str(&name), + authors: StringBuffer::from_str(&authors), + wake_mask: 0, + wake_slot: 0, + asleep: false, + connected_devices, + custom1: StringBuffer::new(), + custom2: StringBuffer::new(), + custom3: StringBuffer::new(), + custom4: StringBuffer::new(), + } + } +} diff --git a/src/emulators/graphical_emulator.rs b/src/emulators/graphical_emulator.rs new file mode 100644 index 0000000..598a5a2 --- /dev/null +++ b/src/emulators/graphical_emulator.rs @@ -0,0 +1,339 @@ +use crate::*; + +use gilrs::Gilrs; + + +pub struct GraphicalEmulator { + pub br: BedrockEmulator<GraphicalDeviceBus>, + pub debug: DebugState, + pub gilrs: Option<Gilrs>, + + pub fullscreen: bool, + pub scale: u32, + pub render_mark: Instant, // last time screen was rendered + pub frame_mark: Instant, // refreshes when clean + pub replace_palette: bool, + pub config: EmulatorConfig, +} + +impl GraphicalEmulator { + pub fn new(config: EmulatorConfig, debug: bool) -> Self { + let gilrs = match Gilrs::new() { + Ok(gilrs) => Some(gilrs), + Err(err) => { + info!("Could not start gamepad listener: {}", err); + None + } + }; + + Self { + br: BedrockEmulator::new(GraphicalDeviceBus::new(&config)), + debug: DebugState::new(debug, config.symbols_path.as_ref()), + gilrs, + + fullscreen: config.fullscreen, + scale: config.zoom.into(), + replace_palette: config.palette.is_some(), + render_mark: Instant::now(), + frame_mark: Instant::now(), + config, + } + } + + pub fn load_program(&mut self, bytecode: &[u8]) { + self.br.core.mem.load_program(bytecode); + } + + pub fn run(self, mut phosphor: Phosphor, visible: bool) { + let window = WindowBuilder { + dimensions: Some(self.dimensions()), + size_bounds: Some(self.size_bounds()), + fullscreen: self.fullscreen, + scale: self.scale, + title: Some(self.config.title.clone()), + cursor: match self.config.show_cursor { + true => Some(CursorIcon::Default), + false => None, + }, + icon: self.config.icon.clone(), + program: Box::new(self), + visible, + }; + + phosphor.add_window(window); + phosphor.run().unwrap(); + } + + pub fn size_bounds(&self) -> SizeBounds { + match self.fullscreen { + true => SizeBounds { + min_width: None, + max_width: None, + min_height: None, + max_height: None, + }, + false => SizeBounds { + min_width: self.br.dev.screen.fixed_width.map(u32::from), + max_width: self.br.dev.screen.fixed_width.map(u32::from), + min_height: self.br.dev.screen.fixed_height.map(u32::from), + max_height: self.br.dev.screen.fixed_height.map(u32::from), + }, + } + } + + pub fn dimensions(&self) -> Dimensions { + Dimensions { + width: self.br.dev.screen.dimensions.width.into(), + height: self.br.dev.screen.dimensions.height.into(), + } + } +} + + +pub struct GraphicalDeviceBus { + pub system: SystemDevice, + pub memory: MemoryDevice, + pub math: MathDevice, + pub clock: ClockDevice, + pub input: InputDevice, + pub screen: ScreenDevice, + pub stream: StreamDevice, + pub file: FileDevice, + pub wake_queue: WakeQueue, +} + +impl GraphicalDeviceBus { + pub fn new(config: &EmulatorConfig) -> Self { + Self { + system: SystemDevice::new(0b1111_1100_1100_0000), + memory: MemoryDevice::new(), + math: MathDevice::new(), + clock: ClockDevice::new(), + input: InputDevice::new(), + screen: ScreenDevice::new(&config), + stream: StreamDevice::new(&config), + file: FileDevice::new(), + wake_queue: WakeQueue::new(), + } + } +} + + +impl DeviceBus for GraphicalDeviceBus { + fn get_device(&mut self, slot: u8) -> Option<&mut dyn Device> { + match slot { + 0x0 => Some(&mut self.system), + 0x1 => Some(&mut self.memory), + 0x2 => Some(&mut self.math ), + 0x3 => Some(&mut self.clock ), + 0x4 => Some(&mut self.input ), + 0x5 => Some(&mut self.screen), + 0x8 => Some(&mut self.stream), + 0x9 => Some(&mut self.file ), + _ => None + } + } + + fn wake(&mut self) -> bool { + for slot in self.wake_queue.iter(self.system.wake_mask) { + if let Some(device) = self.get_device(slot) { + if device.wake() { + self.system.wake_slot = slot; + self.system.asleep = false; + self.wake_queue.wake(slot); + return true; + } + } + } + return false; + } +} + + +impl WindowProgram for GraphicalEmulator { + fn handle_event(&mut self, event: Event, r: &mut EventWriter<Request>) { + match event { + Event::CloseRequest => r.write(Request::CloseWindow), + Event::CursorEnter => self.br.dev.input.on_cursor_enter(), + Event::CursorExit => self.br.dev.input.on_cursor_exit(), + Event::CursorMove(p) => self.br.dev.input.on_cursor_move(p), + Event::Resize(d) => self.br.dev.screen.resize(d), + Event::CharacterInput(c) => self.br.dev.input.on_character(c), + Event::ModifierChange(m) => self.br.dev.input.on_modifier(m), + Event::MouseButton { button, action } => self.br.dev.input.on_mouse_button(button, action), + Event::FocusChange(_) => (), + Event::Initialise => (), + Event::ScrollLines { axis, distance } => match axis { + Axis::Horizontal => self.br.dev.input.on_horizontal_scroll(distance / 20.0), + Axis::Vertical => self.br.dev.input.on_vertical_scroll(distance / 20.0), + } + Event::ScrollPixels { axis, distance } => match axis { + Axis::Horizontal => self.br.dev.input.on_horizontal_scroll(distance), + Axis::Vertical => self.br.dev.input.on_vertical_scroll(distance), + } + + Event::Close => (), + Event::KeyboardInput { key, action } => { + self.br.dev.input.on_keypress(key, action); + if action == Action::Pressed { + match key { + KeyCode::F2 => { + self.replace_palette = !self.replace_palette; + r.write(Request::Redraw); + }, + KeyCode::F5 => { + self.scale = std::cmp::max(1, self.scale.saturating_sub(1)); + r.write(Request::SetPixelScale(self.scale)); + }, + KeyCode::F6 => { + self.scale = self.scale.saturating_add(1); + r.write(Request::SetPixelScale(self.scale)); + }, + KeyCode::F11 => { + self.fullscreen = !self.fullscreen; + r.write(Request::SetFullscreen(self.fullscreen)); + }, + _ => (), + } + } + } + _ => (), + } + } + + fn process(&mut self, requests: &mut EventWriter<Request>) { + self.br.dev.stream.flush(); + if let Some(gilrs) = &mut self.gilrs { + while let Some(event) = gilrs.next_event() { + self.br.dev.input.on_gamepad_event(event); + } + } + + if self.br.dev.system.asleep { + // Stay asleep if there are no pending wake events. + if !self.br.dev.wake() { + if self.br.dev.screen.dirty { + requests.write(Request::Redraw); + } + std::thread::sleep(TICK_DURATION); + return; + } + + // Wait for the current frame to be rendered. + if self.br.dev.screen.dirty { + requests.write(Request::Redraw); + std::thread::sleep(TICK_DURATION); + return; + } + } + + // Run the processor for the remainder of the frame. + let frame_end = Instant::now() + TICK_DURATION; + while Instant::now() < frame_end { + if let Some(signal) = self.br.evaluate(BATCH_SIZE, self.debug.enabled) { + match signal { + Signal::Break => { + if self.br.dev.input.accessed || self.br.dev.screen.accessed { + requests.write(Request::SetVisible(true)); + } + } + Signal::Fork | Signal::Reset => { + self.br.reset(); + } + Signal::Sleep => { + self.br.dev.system.asleep = true; + break; + } + Signal::Halt => { + self.br.dev.stream.flush(); + info!("Program halted, exiting."); + self.debug.debug_full(&self.br.core); + requests.write(Request::CloseWindow); + break; + } + Signal::Debug(debug_signal) => match debug_signal { + Debug::Debug1 => self.debug.debug_full(&self.br.core), + Debug::Debug2 => self.debug.debug_timing(&self.br.core), + _ => (), + } + } + } + } + + if std::mem::take(&mut self.br.dev.screen.dirty_dimensions) { + requests.write(Request::SetSizeBounds(self.size_bounds())); + } + + if self.br.dev.screen.dirty { + if self.br.dev.system.asleep { + requests.write(Request::Redraw); + } else if self.frame_mark.elapsed() > MAX_FRAME_DURATION { + requests.write(Request::Redraw); + } + } else { + self.frame_mark = Instant::now(); + } + } + + fn render(&mut self, buffer: &mut Buffer, _full: bool) { + let screen = &mut self.br.dev.screen; + + // Generate table for calculating pixel colours from layer values. + // A given screen pixel will be rendered as the colour given by + // table[fg][bg], where fg and bg are the corresponding layer values. + let mut table = [Colour::BLACK; 256]; + let palette = match self.config.palette { + Some(palette) => match self.replace_palette { + true => palette, + false => screen.palette, + } + None => screen.palette, + }; + table[0..16].clone_from_slice(&palette); + for i in 1..16 { table[i*16..(i+1)*16].fill(palette[i]); } + + // Copy pixels to buffer when it is the same size as the screen. + if buffer.area_usize() == screen.area_usize() { + for (i, colour) in buffer.iter_mut().enumerate() { + let fg = screen.fg[i]; + let bg = screen.bg[i]; + let index = unsafe { fg.unchecked_shl(4) | bg }; + *colour = table[index as usize]; + // TODO: merge fg and bg: *colour = table[screen.bg[i] as usize]; + } + // Copy pixels to buffer when it is a different size to the screen. + } else { + let buffer_width = buffer.width() as usize; + let buffer_height = buffer.height() as usize; + let screen_width = screen.width() as usize; + let screen_height = screen.height() as usize; + let width = std::cmp::min(buffer_width, screen_width ); + let height = std::cmp::min(buffer_height, screen_height); + + let mut bi = 0; + let mut si = 0; + for _ in 0..height { + let bi_next = bi + buffer_width; + let si_next = si + screen_width; + for _ in 0..width { + let fg = screen.fg[si]; + let bg = screen.bg[si]; + let index = unsafe { fg.unchecked_shl(4) | bg }; + buffer[bi] = table[index as usize]; + bi += 1; + si += 1; + } + // Fill remaining right edge with background colour. + buffer[bi..bi_next].fill(table[0]); + bi = bi_next; + si = si_next; + } + // Fill remaining bottom edge with background colour. + buffer[bi..].fill(table[0]); + } + + screen.dirty = false; + self.render_mark = Instant::now(); + self.frame_mark = Instant::now(); + } +} diff --git a/src/emulators/headless_emulator.rs b/src/emulators/headless_emulator.rs new file mode 100644 index 0000000..cac58cf --- /dev/null +++ b/src/emulators/headless_emulator.rs @@ -0,0 +1,105 @@ +use crate::*; + + +pub struct HeadlessEmulator { + pub br: BedrockEmulator<HeadlessDeviceBus>, + pub debug: DebugState, +} + +impl HeadlessEmulator { + pub fn new(config: &EmulatorConfig, debug: bool) -> Self { + Self { + br: BedrockEmulator::new(HeadlessDeviceBus::new(config)), + debug: DebugState::new(debug, config.symbols_path.as_ref()), + } + } + + pub fn load_program(&mut self, bytecode: &[u8]) { + self.br.core.mem.load_program(bytecode); + } + + fn sleep(&mut self) { + loop { + if self.br.dev.wake() { break; } + std::thread::sleep(TICK_DURATION); + } + } + + fn halt(&mut self) { + self.br.dev.stream.flush(); + info!("Program halted, exiting."); + self.debug.debug_full(&self.br.core); + std::process::exit(0); + } + + pub fn run(&mut self) -> ! { + loop { + if let Some(signal) = self.br.evaluate(BATCH_SIZE, self.debug.enabled) { + match signal { + Signal::Fork | Signal::Reset => self.br.reset(), + Signal::Sleep => self.sleep(), + Signal::Halt => self.halt(), + Signal::Debug(debug_signal) => match debug_signal { + Debug::Debug1 => self.debug.debug_full(&self.br.core), + Debug::Debug2 => self.debug.debug_timing(&self.br.core), + _ => (), + } + _ => (), + } + } + } + } +} + + +pub struct HeadlessDeviceBus { + pub system: SystemDevice, + pub memory: MemoryDevice, + pub math: MathDevice, + pub clock: ClockDevice, + pub stream: StreamDevice, + pub file: FileDevice, + pub wake_queue: WakeQueue, +} + +impl HeadlessDeviceBus { + pub fn new(config: &EmulatorConfig) -> Self { + Self { + system: SystemDevice::new(0b1111_0000_1100_0000), + memory: MemoryDevice::new(), + math: MathDevice::new(), + clock: ClockDevice::new(), + stream: StreamDevice::new(&config), + file: FileDevice::new(), + wake_queue: WakeQueue::new(), + } + } +} + +impl DeviceBus for HeadlessDeviceBus { + fn get_device(&mut self, slot: u8) -> Option<&mut dyn Device> { + match slot { + 0x0 => Some(&mut self.system), + 0x1 => Some(&mut self.memory), + 0x2 => Some(&mut self.math ), + 0x3 => Some(&mut self.clock ), + 0x8 => Some(&mut self.stream), + 0x9 => Some(&mut self.file ), + _ => None + } + } + + fn wake(&mut self) -> bool { + for slot in self.wake_queue.iter(self.system.wake_mask) { + if let Some(device) = self.get_device(slot) { + if device.wake() { + self.system.wake_slot = slot; + self.system.asleep = false; + self.wake_queue.wake(slot); + return true; + } + } + } + return false; + } +} diff --git a/src/emulators/mod.rs b/src/emulators/mod.rs new file mode 100644 index 0000000..8f04e4d --- /dev/null +++ b/src/emulators/mod.rs @@ -0,0 +1,21 @@ +mod headless_emulator; +mod graphical_emulator; + +pub use headless_emulator::*; +pub use graphical_emulator::*; + +use crate::*; + + +pub struct EmulatorConfig { + pub dimensions: ScreenDimensions, + pub fullscreen: bool, + pub zoom: NonZeroU32, + pub palette: Option<[Colour; 16]>, + pub show_cursor: bool, + pub decode_stdin: bool, + pub encode_stdout: bool, + pub symbols_path: Option<PathBuf>, + pub title: String, + pub icon: Option<Icon>, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f260042 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,31 @@ +#![feature(bigint_helper_methods)] +#![feature(seek_stream_len)] +#![feature(unchecked_shifts)] + +mod debug; +mod devices; +mod emulators; +mod types; + +pub use debug::*; +pub use devices::*; +pub use emulators::*; +pub use types::*; + +use bedrock_core::*; +use log::*; +use phosphor::*; + +use std::num::NonZeroU32; +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; + +pub const BATCH_SIZE: usize = 1000; +pub const TICK_DURATION: Duration = Duration::from_nanos( 1_000_000_000/256 ); +pub const MIN_FRAME_DURATION: Duration = Duration::from_millis( 10 ); +pub const MAX_FRAME_DURATION: Duration = Duration::from_millis( 500 ); +pub const DEFAULT_SCREEN_SIZE: ScreenDimensions = ScreenDimensions::new(800,600); +pub const DEFAULT_SCREEN_SCALE: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1) }; + +pub type ScreenPosition = geometry::Point<u16>; +pub type ScreenDimensions = geometry::Dimensions<u16>; diff --git a/src/types/buffered_file.rs b/src/types/buffered_file.rs new file mode 100644 index 0000000..5cdf0ea --- /dev/null +++ b/src/types/buffered_file.rs @@ -0,0 +1,143 @@ +use std::fs::File; +use std::io::{BufReader, BufWriter, Read, Write}; +use std::io::{ErrorKind, Seek, SeekFrom}; + + +pub struct BufferedFile { + file: AccessMode, +} + +impl BufferedFile { + pub fn new(file: File) -> Self { + Self { + file: AccessMode::Read(BufReader::new(file)), + } + } + + pub fn close(&mut self) { + self.file = AccessMode::None; + } + + pub fn read(&mut self) -> u8 { + let mut buffer = [0u8; 1]; + + let read_result = match &mut self.file { + AccessMode::Read(reader) => reader.read_exact(&mut buffer), + AccessMode::Write(writer) => { + let address = writer.stream_position().unwrap(); + let file = std::mem::take(&mut self.file).unwrap(); + let mut reader = BufReader::new(file); + reader.seek(SeekFrom::Start(address)).unwrap(); + let read_result = reader.read_exact(&mut buffer); + self.file = AccessMode::Read(reader); + read_result + } + AccessMode::None => unreachable!(), + }; + + match read_result { + Ok(_) => buffer[0], + Err(error) => match error.kind() { + ErrorKind::UnexpectedEof => 0, + _ => { log::error!("BufferedFile::read: {error:?}"); 0 }, + } + } + } + + pub fn write(&mut self, byte: u8) { + let mut buffer = [byte; 1]; + + let write_result = match &mut self.file { + AccessMode::Write(writer) => writer.write_all(&mut buffer), + AccessMode::Read(reader) => { + let address = reader.stream_position().unwrap(); + let file = std::mem::take(&mut self.file).unwrap(); + let mut writer = BufWriter::new(file); + writer.seek(SeekFrom::Start(address)).unwrap(); + let write_result = writer.write_all(&mut buffer); + self.file = AccessMode::Write(writer); + write_result + } + AccessMode::None => unreachable!(), + }; + + write_result.unwrap(); + } + + pub fn pointer(&mut self) -> u32 { + let position = match &mut self.file { + AccessMode::Read(reader) => reader.stream_position(), + AccessMode::Write(writer) => writer.stream_position(), + AccessMode::None => unreachable!(), + }; + u32::try_from(position.unwrap()).unwrap_or(u32::MAX) + } + + pub fn set_pointer(&mut self, pointer: u32) { + let position = SeekFrom::Start(pointer as u64); + match &mut self.file { + AccessMode::Read(reader) => reader.seek(position).unwrap(), + AccessMode::Write(writer) => writer.seek(position).unwrap(), + AccessMode::None => unreachable!(), + }; + } + + pub fn length(&mut self) -> u32 { + let length = match &mut self.file { + AccessMode::Read(reader) => reader.stream_len(), + AccessMode::Write(writer) => writer.stream_len(), + AccessMode::None => unreachable!(), + }; + u32::try_from(length.unwrap()).unwrap_or(u32::MAX) + } + + pub fn set_length(&mut self, length: u32) { + match &mut self.file { + AccessMode::Read(_) => { + let file = std::mem::take(&mut self.file).unwrap(); + file.set_len(length as u64).unwrap(); + self.file = AccessMode::Read(BufReader::new(file)); + } + AccessMode::Write(_) => { + let file = std::mem::take(&mut self.file).unwrap(); + file.set_len(length as u64).unwrap(); + self.file = AccessMode::Read(BufReader::new(file)); + } + AccessMode::None => unreachable!(), + }; + } + + pub fn flush(&mut self) { + if let AccessMode::Write(writer) = &mut self.file { + let _ = writer.flush(); + } + } +} + +impl Drop for BufferedFile { + fn drop(&mut self) { + self.flush() + } +} + +enum AccessMode { + Read(BufReader<File>), + Write(BufWriter<File>), + None, +} + +impl AccessMode { + pub fn unwrap(self) -> File { + match self { + Self::Read(reader) => reader.into_inner(), + Self::Write(writer) => writer.into_inner().unwrap(), + Self::None => unreachable!(), + } + } +} + +impl Default for AccessMode { + fn default() -> Self { + Self::None + } +} diff --git a/src/types/controller.rs b/src/types/controller.rs new file mode 100644 index 0000000..76b77e3 --- /dev/null +++ b/src/types/controller.rs @@ -0,0 +1,160 @@ +use crate::*; + +pub use gilrs::{Gilrs, GamepadId}; + + +pub struct OwnedGamepad { + tag: usize, + id: Option<GamepadId>, + gamepad: Gamepad, +} + +impl OwnedGamepad { + pub fn new(tag: usize) -> Self { + Self { tag, id: None, gamepad: Gamepad::new() } + } + + /// Returns Some if the ID owns this gamepad. + pub fn register(&mut self, new_id: GamepadId) -> Option<&mut Gamepad> { + if let Some(id) = self.id { + match id == new_id { + true => Some(&mut self.gamepad), + false => None, + } + } else { + self.id = Some(new_id); + info!("Registered gamepad {}", self.tag); + Some(&mut self.gamepad) + } + } + + pub fn state(&self) -> u8 { + self.gamepad.state + } + + pub fn reset(&mut self) { + self.gamepad.reset(); + } +} + + +pub struct Gamepad { + pub state: u8, + l_up: bool, + l_down: bool, + l_left: bool, + l_right: bool, + r_up: bool, + r_down: bool, + r_left: bool, + r_right: bool, + d_up: bool, + d_down: bool, + d_left: bool, + d_right: bool, + a: bool, + b: bool, + x: bool, + y: bool, +} + +impl Gamepad { + pub fn new() -> Self { + Self { + state: 0, + l_up: false, + l_down: false, + l_left: false, + l_right: false, + r_up: false, + r_down: false, + r_left: false, + r_right: false, + d_up: false, + d_down: false, + d_left: false, + d_right: false, + a: false, + b: false, + x: false, + y: false, + } + } + + pub fn reset(&mut self) { + self.state = 0; + self.l_up = false; + self.l_down = false; + self.l_left = false; + self.l_right = false; + self.r_up = false; + self.r_down = false; + self.r_left = false; + self.r_right = false; + self.d_up = false; + self.d_down = false; + self.d_left = false; + self.d_right = false; + self.a = false; + self.b = false; + self.x = false; + self.y = false; + } + + // Returns true if the state changed. + pub fn process_event(&mut self, event: &gilrs::Event) -> bool { + macro_rules! schmitt { + ($name_neg:ident, $name_pos:ident, $v:expr) => {{ + if self.$name_neg { if $v > -0.40 { self.$name_neg = false; } } + else { if $v < -0.50 { self.$name_neg = true; } } + if self.$name_pos { if $v < 0.40 { self.$name_pos = false; } } + else { if $v > 0.50 { self.$name_pos = true; } } + }}; + } + + match event.event { + gilrs::EventType::ButtonPressed(button, _) => match button { + gilrs::Button::South => self.a = true, + gilrs::Button::East => self.b = true, + gilrs::Button::West => self.x = true, + gilrs::Button::North => self.y = true, + gilrs::Button::DPadUp => self.d_up = true, + gilrs::Button::DPadDown => self.d_down = true, + gilrs::Button::DPadLeft => self.d_left = true, + gilrs::Button::DPadRight => self.d_right = true, + _ => (), + } + gilrs::EventType::ButtonReleased(button, _) => match button { + gilrs::Button::South => self.a = false, + gilrs::Button::East => self.b = false, + gilrs::Button::West => self.x = false, + gilrs::Button::North => self.y = false, + gilrs::Button::DPadUp => self.d_up = false, + gilrs::Button::DPadDown => self.d_down = false, + gilrs::Button::DPadLeft => self.d_left = false, + gilrs::Button::DPadRight => self.d_right = false, + _ => (), + } + gilrs::EventType::AxisChanged(axis, v, _) => match axis { + gilrs::Axis::LeftStickX => schmitt!(l_left, l_right, v), + gilrs::Axis::LeftStickY => schmitt!(l_down, l_up, v), + gilrs::Axis::RightStickX => schmitt!(r_left, r_right, v), + gilrs::Axis::RightStickY => schmitt!(r_down, r_up, v), + _ => (), + } + _ => (), + } + + let old_state = self.state; + self.state = 0; + if self.l_up | self.r_up | self.d_up { self.state |= 0x80; } + if self.l_down | self.r_down | self.d_down { self.state |= 0x40; } + if self.l_left | self.r_left | self.d_left { self.state |= 0x20; } + if self.l_right | self.r_right | self.d_right { self.state |= 0x10; } + if self.a { self.state |= 0x08; } + if self.b { self.state |= 0x04; } + if self.x { self.state |= 0x02; } + if self.y { self.state |= 0x01; } + old_state != self.state + } +} diff --git a/src/types/directory_listing.rs b/src/types/directory_listing.rs new file mode 100644 index 0000000..f079217 --- /dev/null +++ b/src/types/directory_listing.rs @@ -0,0 +1,120 @@ +use crate::*; + + +pub struct DirectoryListing { + children: Vec<BedrockFilePath>, + length: u32, + selected: Option<u32>, + child_path_buffer: BedrockPathBuffer, +} + + +impl DirectoryListing { + pub fn from_path(path: &BedrockFilePath) -> Option<Self> { + macro_rules! unres { + ($result:expr) => { match $result { Ok(v) => v, Err(_) => continue} }; + } + macro_rules! unopt { + ($option:expr) => { match $option { Some(v) => v, None => continue} }; + } + + #[cfg(target_family = "windows")] { + if path.as_path().components().count() == 0 { + return Some(Self::construct_virtual_root()) + } + } + + let mut children = Vec::new(); + if let Ok(dir_listing) = std::fs::read_dir(path.as_path()) { + for (i, entry_result) in dir_listing.enumerate() { + // Firebreak to prevent emulator from consuming an absurd amount + // of memory when opening too large of a directory. + if i == (u16::MAX as usize) { + break; + } + + let entry = unres!(entry_result); + let entry_path = unopt!(BedrockFilePath::from_path(&entry.path(), path.base())); + if entry_path.is_hidden() { + continue; + } + + children.push(entry_path); + } + } + + children.sort(); + let length = u32::try_from(children.len()).ok()?; + let selected = None; + let child_path_buffer = BedrockPathBuffer::new(); + Some( Self { children, length, selected, child_path_buffer } ) + } + + /// Generate entries for a virtual root directory. + #[cfg(target_family = "windows")] + fn construct_virtual_root() -> Self { + let mut children = Vec::new(); + let base = PathBuf::from(""); + let drive_bits = unsafe { + windows::Win32::Storage::FileSystem::GetLogicalDrives() + }; + for i in 0..26 { + if drive_bits & (0x1 << i) != 0 { + let letter: char = (b'A' + i).into(); + let path = PathBuf::from(format!("{letter}:/")); + if let Some(drive) = BedrockFilePath::from_path(&path, &base) { + children.push(drive); + } + } + } + + let length = children.len() as u32; + let selected = None; + let child_path_buffer = BedrockPathBuffer::new(); + Self { children, length, selected, child_path_buffer } + } + + /// Attempts to return a directory child by index. + pub fn get(&self, index: u32) -> Option<&BedrockFilePath> { + self.children.get(index as usize) + } + + pub fn length(&self) -> u32 { + self.length + } + + /// Returns the index of the selected child, or zero if no child is selected. + pub fn selected(&self) -> u32 { + self.selected.unwrap_or(0) + } + + /// Attempts to select a child by index. + pub fn set_selected(&mut self, index: u32) { + if let Some(child) = self.get(index) { + let buffer = child.as_buffer(); + self.child_path_buffer.populate(buffer); + self.selected = Some(index); + } else { + self.child_path_buffer.clear(); + self.selected = None; + } + } + + pub fn deselect_child(&mut self) { + self.child_path_buffer.clear(); + self.selected = None; + } + + pub fn child_path_buffer(&mut self) -> &mut BedrockPathBuffer { + &mut self.child_path_buffer + } + + pub fn child_type(&self) -> Option<EntryType> { + self.selected.and_then(|s| self.get(s).and_then(|i| i.entry_type())) + } + + pub fn child_path(&self) -> Option<BedrockFilePath> { + self.selected.and_then(|s| self.get(s).and_then(|i| Some(i.clone()))) + } +} + diff --git a/src/types/entry_type.rs b/src/types/entry_type.rs new file mode 100644 index 0000000..6a9ac2d --- /dev/null +++ b/src/types/entry_type.rs @@ -0,0 +1,37 @@ +use crate::*; + +use std::cmp::Ordering; + + +pub enum Entry { + File(BufferedFile), + Directory(DirectoryListing), +} + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum EntryType { + File, + Directory, +} + +impl PartialOrd for EntryType { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + match (self, other) { + (EntryType::Directory, EntryType::Directory) => Some(Ordering::Equal ), + (EntryType::Directory, EntryType::File ) => Some(Ordering::Less ), + (EntryType::File, EntryType::Directory) => Some(Ordering::Greater), + (EntryType::File, EntryType::File ) => Some(Ordering::Equal ), + } + } +} + +impl Ord for EntryType { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + (EntryType::Directory, EntryType::Directory) => Ordering::Equal , + (EntryType::Directory, EntryType::File ) => Ordering::Less , + (EntryType::File, EntryType::Directory) => Ordering::Greater, + (EntryType::File, EntryType::File ) => Ordering::Equal , + } + } +} diff --git a/src/types/file_path.rs b/src/types/file_path.rs new file mode 100644 index 0000000..7e6dbe8 --- /dev/null +++ b/src/types/file_path.rs @@ -0,0 +1,288 @@ +use crate::*; + +use std::cmp::Ordering; +use std::ffi::OsString; +use std::path::Component; + + +#[derive(Clone)] +pub struct BedrockFilePath { + /// Sandbox directory + base: PathBuf, + /// Path relative to sandbox directory + relative: PathBuf, + bytes: Vec<u8>, + entry_type: Option<EntryType>, +} + +impl BedrockFilePath { + pub fn from_buffer(buffer: [u8; 256], base: &Path) -> Option<Self> { + let base = base.to_path_buf(); + let relative = buffer_to_path(buffer)?; + let bytes = path_to_bytes(&relative)?; + let entry_type = get_entry_type(base.join(&relative)); + assert_path_is_safe(&relative, &base)?; + Some(Self { base, relative, bytes, entry_type }) + } + + /// Construct an instance from an absolute path and a prefix of that path. + pub fn from_path(path: &Path, base: &Path) -> Option<Self> { + let base = base.to_path_buf(); + let relative = path.strip_prefix(&base).ok()?.to_path_buf(); + let bytes = path_to_bytes(&relative)?; + let entry_type = get_entry_type(base.join(&relative)); + assert_path_is_safe(&relative, &base)?; + Some( Self { base, relative, bytes, entry_type } ) + } + + /// Get the base path used by this path. + pub fn base(&self) -> &Path { + &self.base + } + + /// Get this path as a Bedrock-style path, which can be passed to + /// a Bedrock program. + pub fn as_bytes(&self) -> &[u8] { + &self.bytes + } + + /// Get this path as a byte buffer, from which a CircularPathBuffer + /// can be populated. + pub fn as_buffer(&self) -> [u8; 256] { + let mut buffer: [u8; 256] = [0; 256]; + buffer[..self.bytes.len()].copy_from_slice(&self.bytes); + return buffer; + } + + /// Get this path as an absolute operating-system path, which can + /// be used to open a file or directory. + pub fn as_path(&self) -> PathBuf { + self.base.join(&self.relative) + } + + /// Get the entry type of this path. + pub fn entry_type(&self) -> Option<EntryType> { + self.entry_type + } + + /// Get a path which represents the parent of this path. + pub fn parent(&self) -> Option<Self> { + #[cfg(target_family = "unix")] { + Self::from_path(self.as_path().parent()?, &self.base) + } + #[cfg(target_family = "windows")] { + if self.base.components().count() != 0 { + // Sandboxed path, cannot ascend to a virtual root directory. + Self::from_path(self.as_path().parent()?, &self.base) + } else { + // Unsandboxed path, we can ascend to a virtual root directory. + match self.as_path().parent() { + // Ascend to concrete parent directory. + Some(parent) => Self::from_path(parent, &self.base), + // Ascend into a virtual root directory. + None => { + if self.relative.components().count() != 0 { + // Ascend from concrete path to virtual root. + let blank = PathBuf::from(""); + BedrockFilePath::from_path(&blank, &blank) + } else { + // Cannot ascend above the virtual root. + None + } + }, + } + } + } + } + + /// Returns true if the file would be hidden by the default file browser. + pub fn is_hidden(&self) -> bool { + #[cfg(target_family = "unix")] { + if let Some(stem) = self.relative.file_stem() { + if let Some(string) = stem.to_str() { + return string.starts_with('.'); + } + } + } + #[cfg(target_family = "windows")] { + use std::os::windows::fs::MetadataExt; + // See https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants + // const FILE_ATTRIBUTE_HIDDEN: u32 = 0x00000002; + // const FILE_ATTRIBUTE_NOT_CONTENT_INDEXED: u32 = 0x00002000; + if let Ok(metadata) = std::fs::metadata(self.as_path()) { + return metadata.file_attributes() & 0x2002 != 0; + } + } + return false; + } +} + + +/// Converts the contents of a CircularPathBuffer to a relative path. +fn buffer_to_path(bytes: [u8; 256]) -> Option<PathBuf> { + // The buffer must be non-empty and slash-prefixed. + if bytes[0] != ('/' as u8) { + return None; + } + + // Find the index of the first null byte. + let mut null_i = None; + for (i, b) in bytes.iter().enumerate() { + if *b == 0x00 { + null_i = Some(i); + break; + } + } + // Take a slice, excluding the leading slash, up to the trailing null. + let slice = &bytes[1..null_i?]; + + #[cfg(target_family = "unix")] { + use std::os::unix::ffi::OsStringExt; + let vec = Vec::from(slice); + return Some(OsString::from_vec(vec).into()) + } + #[cfg(target_family = "windows")] { + use std::os::windows::ffi::OsStringExt; + let mut string = String::from_utf8_lossy(slice).to_string(); + // Convert drive-current-directory paths to drive-root paths. This is + // needed because the paths C: and C:/ point to separate directories, + // but trailing forward-slashes are optional in Bedrock. + if string.ends_with(':') { + string.push('/'); + } + let utf16: Vec<u16> = string.replace(r"/", r"\").encode_utf16().collect(); + return Some(OsString::from_wide(&utf16).into()) + } +} + +/// Convert an operating system path to a Bedrock-style byte path. +/// +/// A byte path contains at most 255 bytes, and is not null-terminated. +fn path_to_bytes(path: &Path) -> Option<Vec<u8>> { + #[cfg(target_family = "unix")] + let string = path.as_os_str().to_str()?.to_string(); + #[cfg(target_family = "windows")] + let string = path.as_os_str().to_str()?.replace(r"\", r"/"); + + // Remove any trailing forward-slash and add a leading forward-slash. + let mut prefixed_string = String::from("/"); + prefixed_string.push_str(string.trim_end_matches('/')); + let slice = prefixed_string.as_bytes(); + + // Error if bytes does not fit into a CircularPathBuffer. + if slice.len() > 255 { return None; } + + Some(Vec::from(slice)) +} + +/// Returns true if a relative path can be safely attached to a base without +/// breaking out of the sandbox. +fn assert_path_is_safe(relative: &Path, _base: &Path) -> Option<()> { + #[cfg(target_family = "unix")] { + // Error if path contains special components. + for component in relative.components() { + match component { + Component::Normal(_) => continue, + _ => return None, + } + } + } + #[cfg(target_family = "windows")] { + // If the base path is empty, the relative path needs to be able to + // contain the prefix and root element. If the base path is not + // empty, the relative path must not contain these elements else + // they will override the base path when joined. + if _base.components().count() != 0 { + for component in relative.components() { + match component { + Component::Normal(_) => continue, + _ => return None, + } + } + } + } + return Some(()); +} + +fn get_entry_type(absolute: PathBuf) -> Option<EntryType> { + #[cfg(target_family = "windows")] { + // If path is empty, this is a virtual root directory. + if absolute.components().count() == 0 { + return Some(EntryType::Directory) + } + } + let metadata = std::fs::metadata(absolute).ok()?; + if metadata.is_file() { + Some(EntryType::File) + } else if metadata.is_dir() { + Some(EntryType::Directory) + } else { + None + } +} + +impl std::fmt::Debug for BedrockFilePath { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + self.as_path().fmt(f) + } +} + +// --------------------------------------------------------------------------- + +impl PartialEq for BedrockFilePath { + fn eq(&self, other: &Self) -> bool { + self.bytes == other.bytes && self.entry_type == other.entry_type + } +} + +impl Eq for BedrockFilePath {} + +impl PartialOrd for BedrockFilePath { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for BedrockFilePath { + fn cmp(&self, other: &Self) -> Ordering { + match self.entry_type.cmp(&other.entry_type) { + Ordering::Equal => compare_ascii_slices(&self.bytes, &other.bytes), + ordering => ordering, + } + } +} + +/// Compare two ASCII byte-slices in case-agnostic alphabetic order. +fn compare_ascii_slices(left: &[u8], right: &[u8]) -> Ordering { + let l = std::cmp::min(left.len(), right.len()); + let lhs = &left[..l]; + let rhs = &right[..l]; + + for i in 0..l { + let a = remap_ascii(lhs[i]); + let b = remap_ascii(rhs[i]); + match a.cmp(&b) { + Ordering::Equal => (), + non_eq => return non_eq, + } + } + + left.len().cmp(&right.len()) +} + +/// Remap ASCII values so that they sort in case-agnostic alphabetic order: +/// +/// ```text +/// !"#$%&'()*+,-./0123456789:;<=>? +/// @`AaBbCcDdEeFfGgHhIiJjKkLlMmNnOo +/// PpQqRrSsTtUuVvWwXxYyZz[{\|]}^~_ +/// ``` +fn remap_ascii(c: u8) -> u8 { + if 0x40 <= c && c <= 0x5F { + (c - 0x40) * 2 + 0x40 + } else if 0x60 <= c && c <= 0x7F { + (c - 0x60) * 2 + 0x41 + } else { + c + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..1cc90d3 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,19 @@ +mod buffered_file; +mod controller; +mod directory_listing; +mod entry_type; +mod file_path; +mod path_buffer; +mod sprite_buffer; +mod string_buffer; +mod wake_queue; + +pub use buffered_file::*; +pub use controller::*; +pub use directory_listing::*; +pub use entry_type::*; +pub use file_path::*; +pub use path_buffer::*; +pub use sprite_buffer::*; +pub use string_buffer::*; +pub use wake_queue::*; diff --git a/src/types/path_buffer.rs b/src/types/path_buffer.rs new file mode 100644 index 0000000..d6a0861 --- /dev/null +++ b/src/types/path_buffer.rs @@ -0,0 +1,60 @@ +pub struct BedrockPathBuffer { + buffer: [u8; 256], + pointer: u8, +} + +impl BedrockPathBuffer { + pub fn new() -> Self { + Self { buffer: [0; 256] , pointer: 0 } + } + + /// Clear the buffer, returning the previous buffer contents. + pub fn clear(&mut self) -> [u8; 256] { + self.pointer = 0; + std::mem::replace(&mut self.buffer, [0; 256]) + } + + /// Reset the pointer and hot-swap the byte buffer. + pub fn populate(&mut self, buffer: [u8; 256]) { + self.pointer = 0; + self.buffer = buffer; + } + + /// Move internal pointer to the start of the path or file name. + /// + /// If value is non-zero, the pointer will be moved to the byte + /// directly following the final forward-slash. + pub fn set_pointer(&mut self, value: u8) { + self.pointer = 0; + // Set the pointer to the start of the filename if value is truthy. + if value != 0x00 { + for (i, c) in self.buffer.iter().enumerate() { + match c { + b'/' => self.pointer = (i as u8).saturating_add(1), + 0x00 => break, + _ => continue, + } + } + } + } + + /// Read a single byte from the buffer. + pub fn read(&mut self) -> u8 { + let pointer = self.pointer as usize; + self.pointer = self.pointer.wrapping_add(1); + self.buffer[pointer] + } + + /// Write a single byte to the buffer. + /// + /// If a null-byte is written, the buffer will be cleared and returned. + pub fn write(&mut self, byte: u8) -> Option<[u8; 256]> { + if byte == 0x00 { + Some(self.clear()) + } else { + self.buffer[self.pointer as usize] = byte; + self.pointer = self.pointer.saturating_add(1); + None + } + } +} diff --git a/src/types/sprite_buffer.rs b/src/types/sprite_buffer.rs new file mode 100644 index 0000000..74c7b55 --- /dev/null +++ b/src/types/sprite_buffer.rs @@ -0,0 +1,85 @@ +use crate::*; + + +pub struct SpriteBuffer { + pub mem: [u8; 16], + pub pointer: usize, + pub cached: Option<(Sprite, u8)>, +} + +impl SpriteBuffer { + pub fn new() -> Self { + Self { + mem: [0; 16], + pointer: 0, + cached: None, + } + } + + pub fn push_byte(&mut self, byte: u8) { + self.mem[self.pointer] = byte; + self.pointer = (self.pointer + 1) % 16; + self.cached = None; + } + + pub fn read_1bit_sprite(&mut self, draw: u8) -> Sprite { + if let Some((sprite, transform)) = self.cached { + if transform == (draw & 0x77) { + return sprite; + } + } + macro_rules! c { + ($v:ident=mem[$p:ident++]) => { let $v = self.mem[$p % 16]; $p = $p.wrapping_add(1); }; + ($v:ident=mem[--$p:ident]) => { $p = $p.wrapping_sub(1); let $v = self.mem[$p % 16]; }; + } + let mut sprite = [[0; 8]; 8]; + let mut p = match draw & 0x02 != 0 { + true => self.pointer, + false => self.pointer + 8, + }; + match draw & 0x07 { + 0x0 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[y][x] = l>>(7-x) & 1; } } }, + 0x1 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[y][x] = l>>( x) & 1; } } }, + 0x2 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[y][x] = l>>(7-x) & 1; } } }, + 0x3 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[y][x] = l>>( x) & 1; } } }, + 0x4 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[x][y] = l>>(7-x) & 1; } } }, + 0x5 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[x][y] = l>>( x) & 1; } } }, + 0x6 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[x][y] = l>>(7-x) & 1; } } }, + 0x7 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[x][y] = l>>( x) & 1; } } }, + _ => unreachable!(), + } + self.cached = Some((sprite, draw & 0x77)); + return sprite; + } + + pub fn read_2bit_sprite(&mut self, draw: u8) -> Sprite { + if let Some((sprite, transform)) = self.cached { + if transform == (draw & 0x77) { + return sprite; + } + } + macro_rules! c { + ($v:ident=mem[$p:ident++]) => { let $v = self.mem[$p % 16]; $p = $p.wrapping_add(1); }; + ($v:ident=mem[--$p:ident]) => { $p = $p.wrapping_sub(1); let $v = self.mem[$p % 16]; }; + } + let mut sprite = [[0; 8]; 8]; + let mut p = match draw & 0x02 != 0 { + true => self.pointer, + false => self.pointer + 8, + }; + let mut s = p + 8; + match draw & 0x07 { + 0x0 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i=7-x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } }, + 0x1 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i= x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } }, + 0x2 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i=7-x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } }, + 0x3 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i= x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } }, + 0x4 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i=7-x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } }, + 0x5 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i= x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } }, + 0x6 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i=7-x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } }, + 0x7 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i= x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } }, + _ => unreachable!(), + } + self.cached = Some((sprite, draw & 0x77)); + return sprite; + } +} diff --git a/src/types/string_buffer.rs b/src/types/string_buffer.rs new file mode 100644 index 0000000..7751d9f --- /dev/null +++ b/src/types/string_buffer.rs @@ -0,0 +1,37 @@ +pub struct StringBuffer { + pub bytes: Vec<u8>, + pub pointer: usize, +} + +impl StringBuffer { + pub fn new() -> Self { + Self { + bytes: Vec::new(), + pointer: 0, + } + } + + pub fn from_str(text: &str) -> Self { + let mut new = Self::new(); + new.set_str(text); + new + } + + pub fn set_str(&mut self, text: &str) { + self.bytes = text.bytes().collect(); + self.pointer = 0; + } + + pub fn read(&mut self) -> u8 { + if let Some(byte) = self.bytes.get(self.pointer) { + self.pointer += 1; + *byte + } else { + 0 + } + } + + pub fn restart(&mut self) { + self.pointer = 0; + } +} diff --git a/src/types/wake_queue.rs b/src/types/wake_queue.rs new file mode 100644 index 0000000..41d815b --- /dev/null +++ b/src/types/wake_queue.rs @@ -0,0 +1,51 @@ +pub struct WakeQueue { + queue: Vec<u8>, +} + + +impl WakeQueue { + pub fn new() -> Self { + Self { + queue: [0x1,0x2,0x3,0x4,0x5,0x6,0x7,0x8,0x9,0xA,0xB,0xC,0xD,0xE,0xF].into(), + } + } + + /// Iterate over all masked devices in last-woken order. + pub fn iter(&self, mask: u16) -> WakeIterator { + let mut queue = Vec::new(); + for i in &self.queue { + if mask & (0x8000 >> i) != 0 { + queue.push(*i); + } + } + // Add system device last. + if mask & 0x8000 != 0 { + queue.push(0); + } + WakeIterator { queue, pointer: 0 } + } + + /// Push a device to the back of the queue. + pub fn wake(&mut self, id: u8) { + if let Some(index) = self.queue.iter().position(|e| *e == id) { + self.queue.remove(index); + self.queue.push(id); + } + } +} + + +pub struct WakeIterator { + queue: Vec<u8>, + pointer: usize, +} + +impl Iterator for WakeIterator { + type Item = u8; + + fn next(&mut self) -> Option<u8> { + let pointer = self.pointer; + self.pointer += 1; + self.queue.get(pointer).copied() + } +} |